{"version":3,"file":"confirmOn.mjs","sources":["../../src/predict/confirmOn.ts"],"sourcesContent":["/**\n * Declarative confirm binding for predicted event channels — the `confirmOn`\n * option of `predict.defineEvent`.\n *\n * Most channels confirm from the same authoritative signal shape: \"this\n * collection entry's field flipped\" (a crate's `alive`, a box's `active`), or\n * the collection membership change IS the signal (a banana appearing /\n * disappearing). `confirmOn` absorbs that wiring: the owning Predict\n * subscribes the schema listeners and calls `channel.confirm(...)` on the\n * channel's behalf, tearing them down with the channel.\n *\n * Three variants, all **data** (a collection name, a field name, an expected\n * value) — deliberately no predicate closures, so the binding serializes and\n * ports to non-JS clients as a plain struct:\n *\n *     confirmOn: { collection: \"crates\",  field: \"alive\", equals: false }\n *     confirmOn: { collection: \"bananas\", event: \"remove\" }\n *     confirmOn: { collection: \"bananas\", event: \"add\", mine: \"owner\" }\n *\n *   - **field-flip** — when a child's `field` becomes `equals`, confirm the\n *     entry keyed by that child's COLLECTION key. Requires channel entry keys\n *     (the `uniqueBy` output) to BE collection keys; mismatched schemes stay\n *     manual.\n *   - **`event: \"remove\"`** — the child's removal confirms the entry keyed by\n *     the removed key.\n *   - **`event: \"add\"`** — a child's arrival confirms KEYLESS (settle-all): a\n *     predicted spawn/drop can't know the key the server will assign, so these\n *     channels hold a single anonymous pending slot. `mine` names a child\n *     field compared `===` to the room's `sessionId`, so a remote player's\n *     add doesn't falsely confirm ours.\n *\n * Root-level collections only. Rejects are never auto-bound — the ack-anchored\n * auto-reject already covers the miss case, and a wrong binding producing\n * silent rejects would be hard to debug. Confirm-only.\n */\n\nimport type { CollectionKeys, ChildOf } from \"../core/schema-reflect.ts\";\n\n/** The loose callbacks surface the owning Predict already holds (its\n *  `Callbacks.get(room)` wrapper). All three return detach functions. */\nexport interface ConfirmOnCallbacks {\n    onAdd: (...args: any[]) => () => void;\n    onRemove: (...args: any[]) => () => void;\n    listen: (instance: any, field: string, cb: (v: any) => void, immediate?: boolean) => () => void;\n}\n\n// Two flat shapes discriminated on the PRESENCE of `field` vs `event` (the\n// absent members typed `?: undefined`) — presence checks narrow reliably\n// where boolean discriminants don't. Distributed over the state's collection\n// keys so `field`/`mine` complete to the child's own fields.\ntype ConfirmOnField<TState> = {\n    [K in CollectionKeys<TState> & keyof TState]: {\n        /** Root-level collection whose children carry the signal field. */\n        collection: K;\n        /** Confirm `channel.confirm(collectionKey)` when this child field\n         *  becomes {@link equals}. */\n        field: keyof ChildOf<TState[K]> & string;\n        equals: string | number | boolean;\n        event?: undefined;\n        mine?: undefined;\n    }\n}[CollectionKeys<TState> & keyof TState];\n\ntype ConfirmOnSignal<TState> = {\n    [K in CollectionKeys<TState> & keyof TState]: {\n        /** Root-level collection whose membership change is the signal. */\n        collection: K;\n        /** `remove` confirms keyed by the removed collection key; `add`\n         *  confirms KEYLESS (settle-all — a predicted spawn can't know its\n         *  server key), optionally gated by {@link mine}. */\n        event: \"add\" | \"remove\";\n        /** `add` only: child field compared `===` to the room's `sessionId`\n         *  (requires a Predict built from a Room). */\n        mine?: keyof ChildOf<TState[K]> & string;\n        field?: undefined;\n        equals?: undefined;\n    }\n}[CollectionKeys<TState> & keyof TState];\n\nexport type ConfirmOn<TState = any> = ConfirmOnField<TState> | ConfirmOnSignal<TState>;\n\n/** Runtime shape of {@link ConfirmOn} — what the wiring actually reads. */\nexport interface ConfirmOnResolved {\n    collection: string;\n    field?: string;\n    equals?: unknown;\n    event?: \"add\" | \"remove\";\n    mine?: string;\n}\n\n/**\n * Subscribe the schema listeners for a {@link ConfirmOn} binding and route\n * them into `channel.confirm(...)`. Returns a single idempotent detacher\n * (registered on the channel's teardown list by `defineEvent`).\n */\nexport function wireConfirmOn(\n    callbacks: ConfirmOnCallbacks,\n    channel: { confirm(key?: string | number): number },\n    on: ConfirmOnResolved,\n    sessionId: (() => string | undefined) | undefined,\n): () => void {\n    const offs: Array<(() => void) | undefined> = [];\n\n    if (on.field !== undefined) {\n        // field-flip: per-child listeners, wired as children arrive (onAdd\n        // replays existing children by default — wanted here).\n        const listeners = new Map<object, () => void>();\n        offs.push(callbacks.onAdd(on.collection, (child: object, key: string | number) => {\n            if (listeners.has(child)) return;   // decoder can re-fire onAdd for one ref\n            listeners.set(child, callbacks.listen(\n                child,\n                on.field!,\n                (v: unknown) => { if (v === on.equals) channel.confirm(key); },\n                // no immediate fire: a child already flipped at bind time is\n                // history, not a settle signal (would fire onUnpredicted)\n                false,\n            ));\n        }));\n        offs.push(callbacks.onRemove(on.collection, (child: object) => {\n            listeners.get(child)?.();\n            listeners.delete(child);\n        }));\n        offs.push(() => {\n            for (const off of listeners.values()) off();\n            listeners.clear();\n        });\n    } else if (on.event === \"remove\") {\n        offs.push(callbacks.onRemove(on.collection, (_child: object, key: string | number) => {\n            channel.confirm(key);\n        }));\n    } else {\n        // add: keyless settle-all, optionally only for our own entities.\n        // No replay of existing entries — they're history, not signals.\n        offs.push(callbacks.onAdd(on.collection, (child: Record<string, unknown>) => {\n            if (on.mine !== undefined && child[on.mine] !== sessionId?.()) return;\n            channel.confirm();\n        }, false));\n    }\n\n    return () => {\n        for (const off of offs.splice(0)) off?.();\n    };\n}\n"],"names":[],"mappings":";;;;;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCG;AAwDH;;;;AAIG;AACG,SAAU,aAAa,CACzB,SAA6B,EAC7B,OAAmD,EACnD,EAAqB,EACrB,SAAiD,EAAA;IAEjD,MAAM,IAAI,GAAoC,EAAE;AAEhD,IAAA,IAAI,EAAE,CAAC,KAAK,KAAK,SAAS,EAAE;;;AAGxB,QAAA,MAAM,SAAS,GAAG,IAAI,GAAG,EAAsB;AAC/C,QAAA,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,KAAa,EAAE,GAAoB,KAAI;AAC7E,YAAA,IAAI,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;AAAE,gBAAA,OAAO;YACjC,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,SAAS,CAAC,MAAM,CACjC,KAAK,EACL,EAAE,CAAC,KAAM,EACT,CAAC,CAAU,KAAI,EAAG,IAAI,CAAC,KAAK,EAAE,CAAC,MAAM;AAAE,gBAAA,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;;;YAG9D,KAAK,CACR,CAAC;QACN,CAAC,CAAC,CAAC;AACH,QAAA,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,KAAa,KAAI;AAC1D,YAAA,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI;AACxB,YAAA,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC;QAC3B,CAAC,CAAC,CAAC;AACH,QAAA,IAAI,CAAC,IAAI,CAAC,MAAK;AACX,YAAA,KAAK,MAAM,GAAG,IAAI,SAAS,CAAC,MAAM,EAAE;AAAE,gBAAA,GAAG,EAAE;YAC3C,SAAS,CAAC,KAAK,EAAE;AACrB,QAAA,CAAC,CAAC;IACN;AAAO,SAAA,IAAI,EAAE,CAAC,KAAK,KAAK,QAAQ,EAAE;AAC9B,QAAA,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,MAAc,EAAE,GAAoB,KAAI;AACjF,YAAA,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC;QACxB,CAAC,CAAC,CAAC;IACP;SAAO;;;AAGH,QAAA,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,KAA8B,KAAI;AACxE,YAAA,IAAI,EAAE,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,SAAS,IAAI;gBAAE;YAC/D,OAAO,CAAC,OAAO,EAAE;AACrB,QAAA,CAAC,EAAE,KAAK,CAAC,CAAC;IACd;AAEA,IAAA,OAAO,MAAK;QACR,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;YAAE,GAAG,IAAI;AAC7C,IAAA,CAAC;AACL;;;;"}