var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import { getRecordAuthor } from '@enbox/agent';
import { Record } from './record.js';
/**
 * A `CustomEvent` subclass carrying a {@link RecordChange} as its `detail`.
 *
 * Dispatched on the {@link LiveQuery} `EventTarget` for both the specific
 * change-type event (`create`, `update`, `delete`) and the catch-all `change`
 * event.
 */
export class RecordChangeEvent extends CustomEvent {
    constructor(change) {
        super(change.type, { detail: change });
    }
}
/**
 * A live query that combines an initial snapshot of matching records with a
 * real-time stream of deduplicated, semantically-typed change events.
 *
 * `LiveQuery` extends `EventTarget` so that standard `addEventListener` /
 * `removeEventListener` work out of the box. For convenience, the typed
 * {@link LiveQuery.on | `.on()`} method provides a cleaner API that returns an
 * unsubscribe function.
 *
 * ### Events
 *
 * | Event name | `detail` type | Description |
 * |---|---|---|
 * | `create` | {@link RecordChange} | A new record was written |
 * | `update` | {@link RecordChange} | An existing record was updated |
 * | `delete` | {@link RecordChange} | A record was deleted |
 * | `change` | {@link RecordChange} | Catch-all for any of the above |
 * | `disconnected` | — | Transport connection lost |
 * | `reconnecting` | `{ attempt: number }` | Reconnection attempt in progress |
 * | `reconnected` | — | Connection restored, subscription resubscribed |
 * | `eose` | — | End-of-stored-events: catch-up replay complete, events are now live |
 * | `error` | {@link LiveQueryError} | Terminal subscription error; no further events will be delivered |
 *
 * @example
 * ```ts
 * const { liveQuery } = await dwn.records.subscribe({
 *   message: {
 *     filter: {
 *       protocol     : chatProtocol.protocol,
 *       protocolPath : 'thread/message',
 *     }
 *   }
 * });
 *
 * // Initial state
 * for (const record of liveQuery.records) {
 *   renderMessage(record);
 * }
 *
 * // Real-time changes
 * liveQuery.on('create', (record) => appendMessage(record));
 * liveQuery.on('update', (record) => refreshMessage(record));
 * liveQuery.on('delete', (record) => removeMessage(record));
 *
 * // Connection lifecycle
 * liveQuery.on('disconnected', () => showOfflineIndicator());
 * liveQuery.on('reconnected', () => hideOfflineIndicator());
 * liveQuery.on('eose', () => console.log('catch-up complete'));
 *
 * // Cleanup
 * await liveQuery.close();
 * ```
 */
export class LiveQuery extends EventTarget {
    constructor(options) {
        super();
        /** Whether the live query has been closed. */
        this._closed = false;
        /** Whether the transport connection is currently active. */
        this._connected = true;
        const { agent, connectedDid, cursor, delegateDid, protocolRole, remoteOrigin, permissionsApi, initialEntries, subscription, } = options;
        this._subscription = subscription;
        this.cursor = cursor;
        // Build Record objects from the initial snapshot entries (same logic as records.query()).
        this.records = initialEntries.map((entry) => new Record(agent, Object.assign({ author: getRecordAuthor(entry), connectedDid,
            remoteOrigin,
            delegateDid,
            protocolRole }, entry), permissionsApi));
        // Seed the known-records map with recordId -> messageTimestamp for dedup.
        this._knownRecords = new Map();
        for (const record of this.records) {
            this._knownRecords.set(record.id, record.timestamp);
        }
    }
    /** Whether the transport connection is currently active. */
    get isConnected() {
        return this._connected;
    }
    /**
     * Process an incoming live event from the DWN subscription.
     * Deduplicates against the initial snapshot and classifies the change type.
     *
     * @internal — Called by `DwnApi.records.subscribe()` when wiring up the subscription handler.
     */
    handleEvent(record) {
        if (this._closed) {
            return;
        }
        let changeType;
        if (record.deleted) {
            changeType = 'delete';
            this._knownRecords.delete(record.id);
        }
        else {
            const knownTimestamp = this._knownRecords.get(record.id);
            if (knownTimestamp === undefined) {
                changeType = 'create';
            }
            else {
                // We've seen this recordId before (either from snapshot or a prior event).
                if (record.timestamp <= knownTimestamp) {
                    // Duplicate or stale event from the overlap window — skip.
                    return;
                }
                changeType = 'update';
            }
            // Update the known state.
            this._knownRecords.set(record.id, record.timestamp);
        }
        const change = { type: changeType, record };
        // Dispatch the specific event (create/update/delete).
        this.dispatchEvent(new RecordChangeEvent(change));
        // Dispatch the catch-all change event.
        this.dispatchEvent(new CustomEvent('change', { detail: change }));
    }
    handleLifecycleEvent(type, detail) {
        if (this._closed) {
            return;
        }
        if (type === 'disconnected') {
            this._connected = false;
        }
        else if (type === 'reconnected') {
            this._connected = true;
        }
        this.dispatchEvent(new CustomEvent(type, { detail }));
    }
    /**
     * Handle a terminal DWN subscription error.
     *
     * @internal — Called by `DwnApi.records.subscribe()` before closing the
     * underlying subscription.
     */
    handleError(error) {
        if (this._closed) {
            return;
        }
        this._connected = false;
        this.dispatchEvent(new CustomEvent('error', { detail: error }));
    }
    on(event, handler) {
        const wrapper = (e) => {
            const detail = e.detail;
            if (event === 'change') {
                handler(detail);
            }
            else if (event === 'create' || event === 'update' || event === 'delete') {
                handler(detail.record);
            }
            else if (event === 'reconnecting') {
                handler(detail);
            }
            else if (event === 'error') {
                handler(detail);
            }
            else {
                // disconnected, reconnected, eose — no payload
                handler();
            }
        };
        this.addEventListener(event, wrapper);
        return () => { this.removeEventListener(event, wrapper); };
    }
    /**
     * Close the underlying subscription and stop dispatching events.
     */
    close() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this._closed) {
                return;
            }
            this._closed = true;
            yield this._subscription.close();
        });
    }
}
//# sourceMappingURL=live-query.js.map