/**
 * A protocol-scoped API returned by {@link Web5.using}.
 *
 * `TypedWeb5` is the **primary developer interface** for interacting with
 * protocol-backed records. It auto-injects the protocol URI, protocolPath,
 * and schema into every operation, and provides compile-time path
 * autocompletion plus typed data payloads via the schema map.
 *
 * All record-returning methods wrap the underlying `Record` instances in
 * {@link TypedRecord} so that type information flows through reads, queries,
 * updates, and subscriptions without manual casts.
 *
 * @example
 * ```ts
 * const social = web5.using(SocialProtocol);
 *
 * // Install the protocol
 * await social.configure();
 *
 * // Create — path and data type are checked at compile time
 * const { record } = await social.records.create('thread', {
 *   data: { title: 'Hello World', body: '...' },
 * });
 * // record is TypedRecord<ThreadData>
 *
 * const data = await record.data.json(); // ThreadData — no cast needed
 *
 * // Query — protocol and protocolPath are auto-injected
 * const { records } = await social.records.query('thread');
 * // records is TypedRecord<ThreadData>[]
 *
 * // Subscribe — real-time changes via TypedLiveQuery
 * const { liveQuery } = await social.records.subscribe('thread/reply');
 * liveQuery.on('create', (record) => {
 *   // record is TypedRecord<ReplyData>
 * });
 * ```
 */
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 { TypedLiveQuery } from './typed-live-query.js';
import { TypedRecord } from './typed-record.js';
// ---------------------------------------------------------------------------
// TypedWeb5 class
// ---------------------------------------------------------------------------
/**
 * A protocol-scoped API that auto-injects `protocol`, `protocolPath`, and
 * `schema` into every DWN operation.
 *
 * All record-returning methods wrap results in {@link TypedRecord} so that
 * the data type `T` (resolved from the schema map) flows end-to-end — from
 * write through read, query, update, and subscribe — without manual casts.
 *
 * Obtain an instance via `web5.using(typedProtocol)`.
 *
 * @example
 * ```ts
 * const social = web5.using(SocialProtocol);
 *
 * await social.configure();
 *
 * const { record } = await social.records.create('friend', {
 *   data: { did: 'did:example:alice', alias: 'Alice' },
 * });
 * const data = await record.data.json(); // FriendData — no cast
 *
 * const { records } = await social.records.query('friend', {
 *   filter: { tags: { did: 'did:example:alice' } },
 * });
 * for (const r of records) {
 *   const d = await r.data.json(); // FriendData
 * }
 * ```
 */
export class TypedWeb5 {
    constructor(dwn, protocol) {
        this._configured = false;
        this._dwn = dwn;
        this._definition = protocol.definition;
        this._validPaths = collectPaths(this._definition.structure);
    }
    /** The protocol URI. */
    get protocol() {
        return this._definition.protocol;
    }
    /** The raw protocol definition. */
    get definition() {
        return this._definition;
    }
    /**
     * Configures (installs) this protocol on the local DWN.
     *
     * If the protocol is already installed with an identical definition,
     * this is a no-op and returns the existing protocol. If the definition
     * has changed (e.g. new types, modified structure), the protocol is
     * re-configured with the updated definition.
     *
     * @param options - Optional overrides like `encryption`.
     */
    configure(options) {
        return __awaiter(this, void 0, void 0, function* () {
            // Query for an existing installation of this protocol.
            const { protocols } = yield this._dwn.protocols.query({
                filter: { protocol: this._definition.protocol },
            });
            // If already installed with the same definition, return it as-is.
            if (protocols.length > 0) {
                const existing = protocols[0];
                if (definitionsEqual(existing.definition, this._definition)) {
                    this._configured = true;
                    return { status: { code: 200, detail: 'OK' }, protocol: existing };
                }
            }
            // Not installed or definition has changed — configure the new version.
            const result = yield this._dwn.protocols.configure({
                definition: this._definition,
                encryption: options === null || options === void 0 ? void 0 : options.encryption,
            });
            if (result.status.code === 202) {
                this._configured = true;
            }
            return result;
        });
    }
    /** Whether the protocol has been configured (installed) on the local DWN. */
    get isConfigured() {
        return this._configured;
    }
    /**
     * Validates that the protocol has been configured and that the path is
     * recognized. Throws a descriptive error if either check fails.
     */
    _assertReady(path) {
        if (!this._configured) {
            throw new Error(`TypedWeb5: protocol '${this._definition.protocol}' has not been configured. ` +
                'Call configure() before performing record operations.');
        }
        if (!this._validPaths.has(path)) {
            throw new Error(`TypedWeb5: invalid protocol path '${path}'. ` +
                `Valid paths are: ${[...this._validPaths].join(', ')}.`);
        }
    }
    /**
     * Protocol-scoped record operations.
     *
     * Every method auto-injects the protocol URI, protocolPath, and schema
     * from the protocol definition. Path parameters provide compile-time
     * autocompletion via `ProtocolPaths<D>`.
     *
     * All methods return {@link TypedRecord} or {@link TypedLiveQuery} instances
     * that carry the resolved data type from the schema map.
     */
    get records() {
        if (this._records !== undefined) {
            return this._records;
        }
        const cached = {
            /**
             * Create a new record at the given protocol path.
             *
             * @param path - The protocol path (e.g. `'friend'`, `'group/member'`).
             * @param request - Create options including typed `data`.
             */
            create: (path, request) => __awaiter(this, void 0, void 0, function* () {
                var _a, _b;
                const normalizedPath = normalizePath(path);
                this._assertReady(normalizedPath);
                const typeName = lastSegment(normalizedPath);
                const typeEntry = this._definition.types[typeName];
                const { status, record } = yield this._dwn.records.write(Object.assign(Object.assign({ data: request.data, store: request.store, encryption: request.encryption, parentContextId: request.parentContextId, published: request.published, datePublished: request.datePublished, recipient: request.recipient, protocolRole: request.protocolRole, tags: request.tags, protocol: this._definition.protocol, protocolPath: normalizedPath }, ((typeEntry === null || typeEntry === void 0 ? void 0 : typeEntry.schema) !== undefined ? { schema: typeEntry.schema } : {})), { dataFormat: (_a = request.dataFormat) !== null && _a !== void 0 ? _a : (_b = typeEntry === null || typeEntry === void 0 ? void 0 : typeEntry.dataFormats) === null || _b === void 0 ? void 0 : _b[0] }));
                return {
                    status,
                    record: new TypedRecord(record),
                };
            }),
            /**
             * Query records at the given protocol path.
             *
             * @param path - The protocol path to query.
             * @param request - Optional filter, sort, and pagination.
             */
            query: (path, request) => __awaiter(this, void 0, void 0, function* () {
                const normalizedPath = normalizePath(path);
                this._assertReady(normalizedPath);
                const typeName = lastSegment(normalizedPath);
                const typeEntry = this._definition.types[typeName];
                const { status, records, cursor } = yield this._dwn.records.query({
                    from: request === null || request === void 0 ? void 0 : request.from,
                    encryption: request === null || request === void 0 ? void 0 : request.encryption,
                    filter: Object.assign(Object.assign(Object.assign({}, request === null || request === void 0 ? void 0 : request.filter), { protocol: this._definition.protocol, protocolPath: normalizedPath }), ((typeEntry === null || typeEntry === void 0 ? void 0 : typeEntry.schema) !== undefined ? { schema: typeEntry.schema } : {})),
                    dateSort: request === null || request === void 0 ? void 0 : request.dateSort,
                    pagination: request === null || request === void 0 ? void 0 : request.pagination,
                    protocolRole: request === null || request === void 0 ? void 0 : request.protocolRole,
                });
                return {
                    status,
                    records: records.map((r) => new TypedRecord(r)),
                    cursor,
                };
            }),
            /**
             * Read a single record at the given protocol path.
             *
             * @param path - The protocol path to read from.
             * @param request - Read options including a filter to identify the record.
             */
            read: (path, request) => __awaiter(this, void 0, void 0, function* () {
                const normalizedPath = normalizePath(path);
                this._assertReady(normalizedPath);
                const typeName = lastSegment(normalizedPath);
                const typeEntry = this._definition.types[typeName];
                const { status, record } = yield this._dwn.records.read({
                    from: request.from,
                    encryption: request.encryption,
                    protocol: this._definition.protocol,
                    filter: Object.assign(Object.assign(Object.assign({}, request.filter), { protocol: this._definition.protocol, protocolPath: normalizedPath }), ((typeEntry === null || typeEntry === void 0 ? void 0 : typeEntry.schema) !== undefined ? { schema: typeEntry.schema } : {})),
                });
                return {
                    status,
                    record: new TypedRecord(record),
                };
            }),
            /**
             * Delete a record at the given protocol path.
             *
             * @param path - The protocol path (used for permission scoping).
             * @param request - Delete options including the `recordId`.
             */
            delete: (_path, request) => __awaiter(this, void 0, void 0, function* () {
                this._assertReady(normalizePath(_path));
                return this._dwn.records.delete({
                    from: request.from,
                    protocol: this._definition.protocol,
                    recordId: request.recordId,
                });
            }),
            /**
             * Subscribe to records at the given protocol path.
             *
             * Returns a {@link TypedLiveQuery} that atomically provides an initial
             * snapshot and a real-time stream of deduplicated change events, with
             * all records typed as `TypedRecord<T>`.
             *
             * @param path - The protocol path to subscribe to.
             * @param request - Optional filter and role.
             */
            subscribe: (path, request) => __awaiter(this, void 0, void 0, function* () {
                const normalizedPath = normalizePath(path);
                this._assertReady(normalizedPath);
                const typeName = lastSegment(normalizedPath);
                const typeEntry = this._definition.types[typeName];
                const { status, liveQuery } = yield this._dwn.records.subscribe({
                    from: request === null || request === void 0 ? void 0 : request.from,
                    filter: Object.assign(Object.assign(Object.assign({}, request === null || request === void 0 ? void 0 : request.filter), { protocol: this._definition.protocol, protocolPath: normalizedPath }), ((typeEntry === null || typeEntry === void 0 ? void 0 : typeEntry.schema) !== undefined ? { schema: typeEntry.schema } : {})),
                    protocolRole: request === null || request === void 0 ? void 0 : request.protocolRole,
                });
                return {
                    status,
                    liveQuery: liveQuery ? new TypedLiveQuery(liveQuery) : undefined,
                };
            }),
        };
        this._records = cached;
        return cached;
    }
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
 * Compares two protocol definitions for deep equality using deterministic
 * JSON serialization.
 *
 * Keys are sorted recursively so that semantically identical definitions
 * with different key ordering are treated as equal.
 */
function definitionsEqual(a, b) {
    return stableStringify(a) === stableStringify(b);
}
/**
 * Strips leading and trailing slashes from a path.
 *
 * `'friend/'` → `'friend'`, `'/group/member/'` → `'group/member'`.
 */
function normalizePath(path) {
    return path.replace(/^\/+|\/+$/g, '');
}
/**
 * Returns the last segment of a slash-delimited path.
 */
function lastSegment(path) {
    const parts = path.split('/');
    return parts[parts.length - 1];
}
/**
 * Recursively collects all valid protocol path strings from a structure object.
 *
 * Given `{ foo: { bar: { $actions: [...] } } }`, returns `Set(['foo', 'foo/bar'])`.
 * Keys starting with `$` are skipped.
 */
function collectPaths(structure, prefix = '') {
    const paths = new Set();
    for (const key of Object.keys(structure)) {
        if (key.startsWith('$')) {
            continue;
        }
        const fullPath = prefix ? `${prefix}/${key}` : key;
        paths.add(fullPath);
        const child = structure[key];
        if (child !== null && typeof child === 'object') {
            for (const nested of collectPaths(child, fullPath)) {
                paths.add(nested);
            }
        }
    }
    return paths;
}
/**
 * Deterministic JSON serialization with sorted keys.
 */
function stableStringify(value) {
    if (value === null || value === undefined || typeof value !== 'object') {
        return JSON.stringify(value);
    }
    if (Array.isArray(value)) {
        return '[' + value.map((item) => stableStringify(item)).join(',') + ']';
    }
    const keys = Object.keys(value).sort();
    const pairs = keys.map((key) => JSON.stringify(key) + ':' + stableStringify(value[key]));
    return '{' + pairs.join(',') + '}';
}
//# sourceMappingURL=typed-web5.js.map