@lunora/replica 1.0.0-alpha.8 → 1.0.0-alpha.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +214 -0
- package/README.md +55 -10
- package/dist/adapters/better-sqlite3.d.mts +1 -1
- package/dist/adapters/better-sqlite3.d.ts +1 -1
- package/dist/adapters/better-sqlite3.mjs +1 -29
- package/dist/adapters/sqlite-wasm.d.mts +3 -9
- package/dist/adapters/sqlite-wasm.d.ts +3 -9
- package/dist/adapters/sqlite-wasm.mjs +1 -44
- package/dist/adapters/sqljs.d.mts +1 -1
- package/dist/adapters/sqljs.d.ts +1 -1
- package/dist/adapters/sqljs.mjs +1 -55
- package/dist/index.d.mts +113 -52
- package/dist/index.d.ts +113 -52
- package/dist/index.mjs +1 -20
- package/dist/packem_shared/EventEmitter-uo75adUL.mjs +1 -0
- package/dist/packem_shared/EventLog-B1-yhArT.mjs +1 -0
- package/dist/packem_shared/EventLogDO-BF9ZWc6C.mjs +1 -0
- package/dist/packem_shared/EventLogDOClient-DWerZ3_n.mjs +1 -0
- package/dist/packem_shared/EventSource-BC0hKJSA.mjs +1 -0
- package/dist/packem_shared/EventsSync-B3wzXm-b.mjs +1 -0
- package/dist/packem_shared/InMemorySnapshotStore-C4taIG5K.mjs +1 -0
- package/dist/packem_shared/LocalMirror-ZmB8SJFe.mjs +4 -0
- package/dist/packem_shared/MaterializerRuntime-S-Knx6BM.mjs +1 -0
- package/dist/packem_shared/SubscriptionManager-AhPw3lFc.mjs +1 -0
- package/dist/packem_shared/applyDiff-DRJ1gap3.mjs +1 -0
- package/dist/packem_shared/applyDiffToDb-C6ek5Elp.mjs +1 -0
- package/dist/packem_shared/classifyChanges-BBc0-770.mjs +1 -0
- package/dist/packem_shared/defineEvents-DHo-VK7G.mjs +1 -0
- package/dist/packem_shared/eventsContext-Dxow9Y7S.mjs +1 -0
- package/dist/packem_shared/fnv1a-BNN96GYb.mjs +1 -0
- package/dist/packem_shared/isClientSeq-D2Xm0_lj.mjs +1 -0
- package/dist/packem_shared/{local-mirror.d-CyGOpUES.d.ts → local-mirror.d-CErKffFW.d.ts} +20 -12
- package/dist/packem_shared/{local-mirror.d-DTavX_y0.d.mts → local-mirror.d-CtQovAv_.d.mts} +20 -12
- package/dist/packem_shared/subscribeToMirror-BT4oOBng.mjs +1 -0
- package/dist/packem_shared/{types.d-CkMkSwLJ.d.mts → types.d-BuLTPLaQ.d.mts} +0 -2
- package/dist/packem_shared/{types.d-CkMkSwLJ.d.ts → types.d-BuLTPLaQ.d.ts} +0 -2
- package/dist/react.d.mts +39 -29
- package/dist/react.d.ts +39 -29
- package/dist/react.mjs +1 -15
- package/package.json +1 -1
- package/dist/packem_shared/EventEmitter-CMZfct03.mjs +0 -92
- package/dist/packem_shared/EventLog-CnK-3Wge.mjs +0 -264
- package/dist/packem_shared/EventLogDO-DqlsVx0H.mjs +0 -381
- package/dist/packem_shared/EventLogDOClient-F4FO8Si4.mjs +0 -92
- package/dist/packem_shared/EventSource-D5yO9_aI.mjs +0 -221
- package/dist/packem_shared/EventsSync-BP36tC9O.mjs +0 -123
- package/dist/packem_shared/InMemorySnapshotStore-BHVAD-Bp.mjs +0 -24
- package/dist/packem_shared/LocalMirror-a-5jEqFN.mjs +0 -219
- package/dist/packem_shared/MaterializerRuntime-BoIrsMYB.mjs +0 -224
- package/dist/packem_shared/SubscriptionManager-C5xbw0pg.mjs +0 -75
- package/dist/packem_shared/applyDiff-98tKzmiW.mjs +0 -67
- package/dist/packem_shared/applyDiffToDb-DQ1xZp5J.mjs +0 -58
- package/dist/packem_shared/classifyChanges-RcqLBpLs.mjs +0 -41
- package/dist/packem_shared/defineEvents-DiBkPTh_.mjs +0 -28
- package/dist/packem_shared/eventsContext-Bk_p48hj.mjs +0 -6
- package/dist/packem_shared/isClientSeq-C46BkzqJ.mjs +0 -5
- package/dist/packem_shared/subscribeToMirror-CiaM-nQ7.mjs +0 -45
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class c{#t;#r=0;#i=0;#s;#e;constructor(t){this.#t=t}get watermark(){return this.#r}start(){if(this.#s!==void 0)return;const t=this.#t.pollInterval??5e3;this.#s=setInterval(()=>{this.#n().catch(()=>{})},t)}stop(){this.#s!==void 0&&(clearInterval(this.#s),this.#s=void 0)}async sync(){return this.#n()}async#n(){if(this.#e)return this.#e;const t=this.#o().finally(()=>{this.#e=void 0});return this.#e=t,t}async#o(){let t=0;try{for(let r=0;r<1e3;r+=1){const s=await this.#t.fetchEventsSince(this.#r);if(s.length===0)return t;const e=s.filter(i=>i.seq>=this.#i);e.length>0&&(this.#t.applyEvents(e),this.#i=e[e.length-1].seq+1);const o=this.#t.getTableDiffs();for(const i of o)this.#t.mirror.applyDiff(i);t+=s.length;const n=s[s.length-1].seq+1;if(n<=this.#r)return t;this.#r=n}return t}catch(r){return(this.#t.onError??console.error)(r),t}}}export{c as EventsSync};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class t{#e=new Map;save(e,r){return this.#e.set(e,structuredClone(r)),Promise.resolve()}load(e){const r=this.#e.get(e);return Promise.resolve(r===void 0?null:structuredClone(r))}list(){return Promise.resolve([...this.#e.keys()])}delete(e){return this.#e.delete(e),Promise.resolve()}clear(){return this.#e.clear(),Promise.resolve()}}export{t as InMemorySnapshotStore};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{createSqlJsAdapter as y}from"../adapters/sqljs.mjs";import{applyDiffToDb as m,escapeIdentifier as a}from"./applyDiffToDb-C6ek5Elp.mjs";import{EventLog as b}from"./EventLog-B1-yhArT.mjs";const A=1e3,l="__lunora_mirror_meta",R=o=>{o.exec(`CREATE TABLE IF NOT EXISTS ${l} (
|
|
2
|
+
key TEXT PRIMARY KEY NOT NULL,
|
|
3
|
+
value TEXT NOT NULL
|
|
4
|
+
)`)},u="schema_version",f=3,h=o=>typeof o=="bigint"||typeof o=="boolean"?"INTEGER":typeof o=="number"?Number.isInteger(o)?"INTEGER":"REAL":"TEXT";class E{#e;#t;#s;#n=new Set;#i=0;static create(e,s){const t=y(e);return new E({db:t,tables:s?.tables})}constructor(e){this.#e=e.db,this.#t={...e.tables},this.#s=new b({maxEntries:e.maxEventLogEntries??A}),R(this.#e),this.#o()}onChange(e){return this.#n.add(e),()=>{this.#n.delete(e)}}get eventLog(){return this.#s}get db(){return this.#e}get version(){return this.#i}applyDiff(e){e.changes.length!==0&&(this.#l(e),m(this.#e,e,this.primaryKeyOf(e.table)),this.#s.append("table-diff",e,[e]),this.#r())}query(e,s){return this.#e.query(e,s)}clearData(){const e=this.#a();this.#e.transaction(()=>{for(const{name:s}of e)this.#e.exec(`DELETE FROM ${a(s)}`)}),this.#r()}#r(){this.#i+=1;for(const e of this.#n)try{e()}catch{}}close(){this.#e.close(),this.#s.clear(),this.#n.clear()}registerTable(e,s){this.#t[e]={...this.#t[e],...s}}primaryKeyOf(e){return this.#t[e]?.primaryKey??"id"}get mirroredTables(){return Object.keys(this.#t)}#a(){return this.#e.query(String.raw`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '\_\_lunora\_%' ESCAPE '\' AND name NOT LIKE 'sqlite\_%' ESCAPE '\'`)}#o(){if(this.#e.query(`SELECT key, value FROM ${l}`).find(n=>n.key===u)?.value===String(f))return;const t=this.#a();this.#e.transaction(()=>{for(const{name:n}of t)this.#e.exec(`DROP TABLE IF EXISTS ${a(n)}`);this.#e.exec(`INSERT OR REPLACE INTO ${l} (key, value) VALUES (?, ?)`,[u,String(f)])})}static#c(e,s){const t=new Set;for(const n of e.changes)if(n.type!=="delete")for(const c of Object.keys(n.data))c!==s&&t.add(c);return t}static#E(e,s,t){const n=new Map;for(const c of e.changes)if(!(c.type==="delete"||n.size===t.size))for(const r of t){if(r===s||n.has(r))continue;const i=c.data[r];i!=null&&n.set(r,h(i))}return n}static#h(e,s){for(const t of e.changes){const n=t.type==="delete"?t.id:t.data[s];if(n!=null)return h(n);if(t.type==="update")return h(t.id)}return"TEXT"}#l(e){const s=this.primaryKeyOf(e.table),t=E.#c(e,s),n=E.#E(e,s,t);if(this.#e.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?",[e.table]).length===0){const r=E.#h(e,s);let i=`${a(s)} ${r==="INTEGER"?"INT":r} PRIMARY KEY NOT NULL`;for(const T of t)i+=`, ${a(T)} ${n.get(T)??"TEXT"}`;this.#e.exec(`CREATE TABLE IF NOT EXISTS ${a(e.table)} (${i})`)}else if(t.size>0){const r=new Set(this.#e.query(`PRAGMA table_info(${a(e.table)})`).map(i=>i.name));for(const i of t)r.has(i)||this.#e.exec(`ALTER TABLE ${a(e.table)} ADD COLUMN ${a(i)} ${n.get(i)??"TEXT"}`)}}}export{E as LocalMirror};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const c=a=>{let e=a.initial();return{def:a,get state(){return Object.freeze(e)},setState(t){e=t},apply(t){e=a.handle(e,t)},reset(){e=a.initial()}}};class u{#e;#i;#n;#s;#t;constructor(e,t={}){this.#e=[...e],this.#t=this.#e.map(()=>0),this.#i=t.snapshotStore,this.#n=t.doClient,this.#s=t.unknownEventHandling??"warn"}get appliedSeq(){return this.#t.length>0?Math.min(...this.#t):0}applyEntries(e){let t=0;for(const i of e){let s=!1,n=!1;const r=[];for(const[o,p]of this.#e.entries()){const h=this.#t[o]??0;if(i.seq<h)continue;const l=p.state;p.apply(i),s=!0,p.state!==l&&(n=!0),r.push(o)}if(s){n||this.#a(i);for(const o of r)this.#t[o]=i.seq+1;t+=1}}return t}#a(e){const t=this.#s;if(typeof t=="function"){t(e);return}switch(t){case"ignore":return;case"fail":throw new Error(`MaterializerRuntime: unhandled event type "${e.type}" (seq ${String(e.seq)}). Configure \`unknownEventHandling\` to handle this event or change the strategy.`);default:console.warn(`[MaterializerRuntime] unhandled event type "${e.type}" (seq ${String(e.seq)}). The event was skipped. Configure \`unknownEventHandling\` if this is expected.`)}}async recoverFromSnapshots(){if(!this.#i)return 0;let e=0;for(const[t,i]of this.#e.entries()){const s=await this.#i.load(i.def.name);if(s!==null&&typeof s=="object"){const n=s;if(Number.isSafeInteger(n.appliedSeq)&&n.appliedSeq>=0&&n.state!==void 0){const r=n.appliedSeq;i.setState(n.state),this.#t[t]=r,r>e&&(e=r)}}}return e}async persistSnapshots(){if(this.#i)for(const[e,t]of this.#e.entries())await this.#i.save(t.def.name,{appliedSeq:this.#t[e]??0,state:t.state})}async initialize(){return this.#n?(await this.recoverFromSnapshots(),this.#r()):0}async#r(){const e=this.#n;if(!e)return 0;let t=this.appliedSeq,i=0;for(let s=0;s<1e3;s+=1){const n=await e.getSince(t);if(i+=this.applyEntries(n.entries),!n.truncated||n.cursor===void 0||n.cursor<=t)return i;t=n.cursor}return i}async appendEvent(e){if(!this.#n)throw new Error("MaterializerRuntime.appendEvent requires a doClient — pass one in the constructor options.");const i=(await this.#n.append([e]))[0];if(!i)throw new Error("MaterializerRuntime.appendEvent: DO returned empty result");return this.#e.length>0&&this.appliedSeq<i.seq&&(await this.#r(),this.appliedSeq<i.seq)||this.applyEntries([i]),i}reset(){for(const[e,t]of this.#e.entries())this.#t[e]=0,t.reset()}get materializers(){return this.#e}}export{u as MaterializerRuntime,c as defineMaterializer};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class i{#t=new Map;#e=0;#s(e){const t=String(this.#e);return this.#e+=1,this.#t.set(t,{...e,id:t}),()=>{this.#t.delete(t)}}onStateChange(e){return this.#s({kind:"state",callback:e})}onEvent(e,t){return this.#s({kind:"event",eventType:e,callback:t})}notifyState(e){for(const t of this.#t.values())if(t.kind==="state")try{t.callback(e)}catch{}}notifyEvent(e){for(const t of this.#t.values())if(t.kind==="event"&&t.eventType===e.type)try{t.callback(e)}catch{}}get size(){return this.#t.size}clear(){this.#t.clear()}}export{i as SubscriptionManager};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{f as i}from"./fnv1a-BNN96GYb.mjs";const r=t=>{if(Array.isArray(t))return t.map(e=>r(e));if(t!==null&&typeof t=="object"){const e=t,s=Object.keys(e);s.sort();const n={};for(const o of s)n[o]=r(e[o]);return n}return t},p=(t,e,s)=>{const n=t.id??String(t.timestamp),o=`${t.table}::${n}::${String(e)}::${JSON.stringify(r(s))}`;return`row-${i(o)}`},c=(t,e)=>{for(const[s,n]of e.changes.entries())switch(n.type){case"delete":{t.delete(n.id);break}case"insert":{const o=n.data.id,a=typeof o=="string"||typeof o=="number"?String(o):p(e,s,n.data);t.set(a,{...n.data,id:a});break}case"update":{const o=t.get(n.id);o&&t.set(n.id,{...o,...n.data});break}}},f=(t,e)=>{const s=new Map(t);return c(s,e),s},y=(t,e)=>{const s=new Map(t);for(const n of e)c(s,n);return s},b=(t,e)=>{const s=new Map(t),n=s.get(e.table)??new Map;return s.set(e.table,f(n,e)),s};export{f as applyDiff,b as applyDiffToSnapshot,y as applyDiffs,r as canonicalizeForHash,p as deriveInsertId,i as fnv1a64Hex};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const r=t=>`\`${t.replaceAll("`","``")}\``,E=t=>t.map(n=>`${r(n)} = ?`).join(", "),$=t=>`(${t.map(n=>r(n)).join(", ")})`,b=t=>`(${Array.from({length:t}).fill("?").join(", ")})`,c=t=>t==null?null:typeof t=="number"||typeof t=="bigint"||typeof t=="string"?t:typeof t=="boolean"?t?1:0:t instanceof Uint8Array?t:typeof t=="object"?JSON.stringify(t):String(t),g=(t,n,o)=>{if(n.changes.length===0)return;const f=r(n.table),y=r(o);for(const s of n.changes)switch(s.type){case"delete":{t.exec(`DELETE FROM ${f} WHERE ${y} = ?`,[c(s.id)]);break}case"insert":{const{data:i}=s,e=Object.keys(i);if(e.length===0)continue;const a=`INSERT OR REPLACE INTO ${f} ${$(e)} VALUES ${b(e.length)}`,p=e.map(l=>c(i[l]));t.exec(a,p);break}case"update":{const{data:i}=s,e=Object.keys(i);if(e.length===0)continue;const a=`UPDATE ${f} SET ${E(e)} WHERE ${y} = ?`,p=[...e.map(l=>c(i[l])),c(s.id)];t.exec(a,p);break}}},D=(t,n,o)=>{t.transaction(()=>{g(t,n,o??"id")})},h=(t,n)=>{n.length!==0&&t.transaction(()=>{for(const o of n)g(t,o,"id")})};export{D as applyDiffToDb,h as applyDiffsToDb,r as escapeIdentifier,c as normalizeBindValue};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{f as a}from"./fnv1a-BNN96GYb.mjs";let s=0;const c=()=>{if(typeof crypto<"u"&&typeof crypto.randomUUID=="function")return crypto.randomUUID();s+=1;const t=typeof crypto<"u"&&typeof crypto.getRandomValues=="function"?Array.from(crypto.getRandomValues(new Uint8Array(8)),n=>n.toString(16).padStart(2,"0")).join(""):Math.random().toString(16).slice(2,12);return`id_${Date.now().toString(36)}_${s.toString(36)}_${t}`},i=(t,n,o,r)=>({table:t,changes:n,timestamp:o??Date.now(),id:r??c()}),g=t=>t.changes.length===0,m=t=>t.changes.length,f=t=>{const n=[],o=[],r=[];for(const e of t.changes)e.type==="insert"?n.push(e):e.type==="update"?o.push(e):r.push(e);return{inserts:n,updates:o,deletes:r}},u=t=>{if(t.length===0)return null;const n=t[0],o=t[t.length-1],r=`merge:${a(t.map(e=>e.id??String(e.timestamp)).join("|"))}`;return i(n.table,t.flatMap(e=>e.changes),o.timestamp,r)};export{f as classifyChanges,i as createTableDiff,m as diffSize,g as isDiffEmpty,u as mergeDiffs};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const v=(a,n)=>{const e={},s={},i=n?.version?`${n.version}.`:"";for(const[o,p]of Object.entries(a)){const c={};for(const r of Object.keys(p)){const t=`${i}${o}.${r}`;s[t]=void 0;const f=Object.assign(y=>({type:t,payload:y,timestamp:Date.now()}),{type:t});c[r]=f}e[o]=c}return e._types=s,e};export{v as defineEvents};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const o=t=>{const e=t;return async({ctx:c,next:n})=>n({ctx:{events:e}})};export{o as eventsContext};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const o=c=>c.toString(16).padStart(4,"0"),g=c=>{let t=8997,n=33826,e=40164,s=52210;for(let h=0;h<c.length;h+=1){t^=c.charCodeAt(h);const r=t*435,p=n*435,x=e*435+t*256,d=s*435+n*256,l=p+(r>>>16),a=x+(l>>>16),f=d+(a>>>16);t=r&65535,n=l&65535,e=a&65535,s=f&65535}return o(s)+o(e)+o(n)+o(t)};export{g as f};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const n=t=>typeof t=="number",e=t=>typeof t!="number"&&"rebaseGeneration"in t,o=t=>typeof t=="object"&&t!==null&&"type"in t&&typeof t.type=="string"&&"payload"in t&&"timestamp"in t;export{e as isClientSeq,n as isGlobalSeq,o as isInputEvent};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as SqliteAdapter } from "./types.d-
|
|
1
|
+
import { S as SqliteAdapter } from "./types.d-BuLTPLaQ.js";
|
|
2
2
|
/**
|
|
3
3
|
* Row-level change kind within a TableDiff.
|
|
4
4
|
*
|
|
@@ -140,7 +140,7 @@ declare const isClientSeq: (seq: Seq) => seq is ClientSeq;
|
|
|
140
140
|
*
|
|
141
141
|
* ```ts
|
|
142
142
|
* const event = events.chat.messageSent({ channelId: "c1", text: "hello" });
|
|
143
|
-
* // event: InputEvent
|
|
143
|
+
* // event: InputEvent<"chat.messageSent", { channelId: string; text: string }>
|
|
144
144
|
* ```
|
|
145
145
|
* @experimental
|
|
146
146
|
*/
|
|
@@ -381,15 +381,18 @@ interface LocalMirrorOptions {
|
|
|
381
381
|
/** Platform-specific SQLite adapter. */
|
|
382
382
|
readonly db: SqliteAdapter;
|
|
383
383
|
/**
|
|
384
|
-
* Cap the mirror's internal {@link EventLog} to this many entries
|
|
385
|
-
*
|
|
386
|
-
*
|
|
384
|
+
* Cap the mirror's internal {@link EventLog} to this many entries.
|
|
385
|
+
* Every applied diff is recorded in the log, so an uncapped log grows by
|
|
386
|
+
* one entry (holding every changed row) per diff for the life of the
|
|
387
|
+
* mirror — a leak by construction on a long-lived client.
|
|
387
388
|
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
* a
|
|
389
|
+
* Defaults to {@link DEFAULT_MAX_EVENT_LOG_ENTRIES}. On overflow the
|
|
390
|
+
* OLDEST entries are dropped; nothing in the mirror replays its own log,
|
|
391
|
+
* so a drop loses nothing the mirror needs. A consumer that does replay
|
|
392
|
+
* it (`eventLog.getSince(watermark)` from another tab / service worker)
|
|
393
|
+
* detects a gap when the first returned entry's `seq` is above its
|
|
394
|
+
* watermark, and should re-seed from the mirror's rows (`query`) instead
|
|
395
|
+
* of applying the partial window.
|
|
393
396
|
*/
|
|
394
397
|
readonly maxEventLogEntries?: number;
|
|
395
398
|
/**
|
|
@@ -420,7 +423,7 @@ interface LocalMirrorOptions {
|
|
|
420
423
|
* mirror.applyDiff(someDiff);
|
|
421
424
|
*
|
|
422
425
|
* // Query locally:
|
|
423
|
-
* const rows = mirror.query
|
|
426
|
+
* const rows = mirror.query<{ id: string; name: string }>(
|
|
424
427
|
* "SELECT id, name FROM users WHERE name LIKE ?",
|
|
425
428
|
* ["alice%"],
|
|
426
429
|
* );
|
|
@@ -519,9 +522,14 @@ declare class LocalMirror {
|
|
|
519
522
|
close(): void;
|
|
520
523
|
/**
|
|
521
524
|
* Register a table schema so the mirror can create the table on
|
|
522
|
-
* first use.
|
|
525
|
+
* first use. Merges into any definition already registered for `name`
|
|
526
|
+
* (from the constructor's `tables` or an earlier call), so a helper that
|
|
527
|
+
* registers `{}` just to make the table known does not erase a
|
|
528
|
+
* user-supplied `primaryKey`.
|
|
523
529
|
*/
|
|
524
530
|
registerTable(name: string, definition: MirrorTableDef): void;
|
|
531
|
+
/** The primary-key column of a mirrored table (`"id"` unless registered otherwise). */
|
|
532
|
+
primaryKeyOf(table: string): string;
|
|
525
533
|
/**
|
|
526
534
|
* Return the list of mirrored table names.
|
|
527
535
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as SqliteAdapter } from "./types.d-
|
|
1
|
+
import { S as SqliteAdapter } from "./types.d-BuLTPLaQ.mjs";
|
|
2
2
|
/**
|
|
3
3
|
* Row-level change kind within a TableDiff.
|
|
4
4
|
*
|
|
@@ -140,7 +140,7 @@ declare const isClientSeq: (seq: Seq) => seq is ClientSeq;
|
|
|
140
140
|
*
|
|
141
141
|
* ```ts
|
|
142
142
|
* const event = events.chat.messageSent({ channelId: "c1", text: "hello" });
|
|
143
|
-
* // event: InputEvent
|
|
143
|
+
* // event: InputEvent<"chat.messageSent", { channelId: string; text: string }>
|
|
144
144
|
* ```
|
|
145
145
|
* @experimental
|
|
146
146
|
*/
|
|
@@ -381,15 +381,18 @@ interface LocalMirrorOptions {
|
|
|
381
381
|
/** Platform-specific SQLite adapter. */
|
|
382
382
|
readonly db: SqliteAdapter;
|
|
383
383
|
/**
|
|
384
|
-
* Cap the mirror's internal {@link EventLog} to this many entries
|
|
385
|
-
*
|
|
386
|
-
*
|
|
384
|
+
* Cap the mirror's internal {@link EventLog} to this many entries.
|
|
385
|
+
* Every applied diff is recorded in the log, so an uncapped log grows by
|
|
386
|
+
* one entry (holding every changed row) per diff for the life of the
|
|
387
|
+
* mirror — a leak by construction on a long-lived client.
|
|
387
388
|
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
* a
|
|
389
|
+
* Defaults to {@link DEFAULT_MAX_EVENT_LOG_ENTRIES}. On overflow the
|
|
390
|
+
* OLDEST entries are dropped; nothing in the mirror replays its own log,
|
|
391
|
+
* so a drop loses nothing the mirror needs. A consumer that does replay
|
|
392
|
+
* it (`eventLog.getSince(watermark)` from another tab / service worker)
|
|
393
|
+
* detects a gap when the first returned entry's `seq` is above its
|
|
394
|
+
* watermark, and should re-seed from the mirror's rows (`query`) instead
|
|
395
|
+
* of applying the partial window.
|
|
393
396
|
*/
|
|
394
397
|
readonly maxEventLogEntries?: number;
|
|
395
398
|
/**
|
|
@@ -420,7 +423,7 @@ interface LocalMirrorOptions {
|
|
|
420
423
|
* mirror.applyDiff(someDiff);
|
|
421
424
|
*
|
|
422
425
|
* // Query locally:
|
|
423
|
-
* const rows = mirror.query
|
|
426
|
+
* const rows = mirror.query<{ id: string; name: string }>(
|
|
424
427
|
* "SELECT id, name FROM users WHERE name LIKE ?",
|
|
425
428
|
* ["alice%"],
|
|
426
429
|
* );
|
|
@@ -519,9 +522,14 @@ declare class LocalMirror {
|
|
|
519
522
|
close(): void;
|
|
520
523
|
/**
|
|
521
524
|
* Register a table schema so the mirror can create the table on
|
|
522
|
-
* first use.
|
|
525
|
+
* first use. Merges into any definition already registered for `name`
|
|
526
|
+
* (from the constructor's `tables` or an earlier call), so a helper that
|
|
527
|
+
* registers `{}` just to make the table known does not erase a
|
|
528
|
+
* user-supplied `primaryKey`.
|
|
523
529
|
*/
|
|
524
530
|
registerTable(name: string, definition: MirrorTableDef): void;
|
|
531
|
+
/** The primary-key column of a mirrored table (`"id"` unless registered otherwise). */
|
|
532
|
+
primaryKeyOf(table: string): string;
|
|
525
533
|
/**
|
|
526
534
|
* Return the list of mirrored table names.
|
|
527
535
|
*/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const k=/["\\\u0000-\u001F\uD800-\uDFFF]/,O=r=>k.test(r)?JSON.stringify(r):`"${r}"`,w=r=>{if(r===void 0)return"null";if(typeof r=="bigint")throw new TypeError("stableStringify: cannot use a bigint in a stable JSON cache key — pass it as a string, or use stableWireKey");if(typeof r=="number"){if(Number.isNaN(r))return"nan";if(r===1/0)return"inf";if(r===-1/0)return"-inf";if(Object.is(r,-0))return"-0"}if(typeof r=="string")return O(r);if(r===null||typeof r!="object")return JSON.stringify(r);if(Array.isArray(r)){let e="[";for(let o=0;o<r.length;o++)o>0&&(e+=","),e+=w(r[o]);return e+"]"}const n=Object.getPrototypeOf(r);if(n!==null&&n!==Object.prototype){const e=r.constructor?.name??"value";throw new TypeError(`stableStringify: cannot use a ${e} in a stable JSON cache key — only plain objects, arrays, and JSON primitives are supported (wire-typed values key via stableWireKey)`)}const f=r,s=Object.keys(f).sort();let c="{",t=!0;for(const e of s){const o=f[e];o!==void 0&&(t?t=!1:c+=",",c+=O(e),c+=":",c+=w(o))}return c+"}"},l=r=>{let n="";for(let s=0;s<r.length;s+=32768)n+=String.fromCharCode(...r.subarray(s,s+32768));return btoa(n)},i="$lunora.wire$";const j="__proto__",E=r=>{if(r===null||typeof r!="object")return!1;const n=Object.getPrototypeOf(r);return n===null||n===Object.prototype},y=(r,n=0)=>{if(n>64)throw new RangeError("wire-codec: value nesting exceeds the 64-level limit");if(r===void 0)return[i,"undefined"];if(r===null)return null;const f=typeof r;if(f==="bigint")return[i,"bigint",r.toString()];if(f==="number"){const t=r;return Number.isNaN(t)?[i,"nan"]:t===1/0?[i,"inf"]:t===-1/0?[i,"-inf"]:t}if(f!=="object")return r;if(r instanceof Date)return[i,"date",y(r.getTime(),n+1)];if(r instanceof Error){const t=r,e={};for(const b of Object.keys(t))t[b]!==void 0&&(e[b]=y(t[b],n+1));const o=[i,"error",t.name,t.message,e];return t.cause!==void 0&&o.push(y(t.cause,n+1)),o}if(r instanceof URL)return[i,"url",r.href];if(r instanceof Map)return[i,"map",[...r.entries()].map(([t,e])=>[y(t,n+1),y(e,n+1)])];if(r instanceof Set)return[i,"set",[...r].map(t=>y(t,n+1))];if(r instanceof ArrayBuffer)return[i,"bytes",l(new Uint8Array(r)),"ArrayBuffer"];if(ArrayBuffer.isView(r)){const t=r,e=t.constructor.name,o=new Uint8Array(t.buffer,t.byteOffset,t.byteLength);return e==="Uint8Array"?[i,"bytes",l(o)]:[i,"bytes",l(o),e]}if(Array.isArray(r)){const t=r.map(e=>y(e,n+1));return t.length>0&&t[0]===i?[i,"arr",t]:t}if(!E(r)){const t=r.constructor?.name??"value";throw new TypeError(`wire-codec: cannot encode a ${t} over the Lunora wire — only plain objects, arrays, and the supported built-ins (Date, Error, URL, Map, Set, ArrayBuffer/typed arrays, bigint) round-trip`)}const s=r,c={};for(const t of Object.keys(s)){const e=s[t];if(e===void 0)continue;const o=y(e,n+1);t===j?Object.defineProperty(c,t,{configurable:!0,enumerable:!0,value:o,writable:!0}):c[t]=o}return c},N=r=>w(y(r)),S=r=>`fn_${r.replaceAll(/[/:.]/g,"_")}`,_=r=>Array.isArray(r)?r:r!==null&&typeof r=="object"?[r]:[],T=(r,n,f,s,c)=>{const t=S(f.__lunoraRef);n.registerTable(t,{});const e=n.primaryKeyOf(t);let o=new Map;return r.subscribe(f,s,b=>{const d=new Map,m=new Map,p=[];for(const a of _(b)){const u=a,g=u[e];if(typeof g!="string"&&typeof g!="number"&&typeof g!="bigint"){p.push({type:"insert",data:u});continue}const A=String(g),h=N(u);d.set(A,h),m.set(A,u)}for(const[a,u]of d)o.get(a)!==u&&p.push({data:m.get(a),type:"insert"});for(const a of o.keys())d.has(a)||p.push({type:"delete",id:a});p.length>0&&n.applyDiff({table:t,changes:p,timestamp:Date.now()}),o=d},{shardKey:c})};export{T as subscribeToMirror};
|
|
@@ -11,8 +11,6 @@ interface SqliteAdapter {
|
|
|
11
11
|
close: () => void;
|
|
12
12
|
/** Execute a SQL statement (with optional bound params). */
|
|
13
13
|
exec: (sql: string, params?: ReadonlyArray<unknown>) => void;
|
|
14
|
-
/** Return the id of the last inserted row. */
|
|
15
|
-
lastInsertRowId: () => number;
|
|
16
14
|
/**
|
|
17
15
|
* Execute a SQL statement and return the result rows.
|
|
18
16
|
* Columns can be accessed by index or by name.
|
|
@@ -11,8 +11,6 @@ interface SqliteAdapter {
|
|
|
11
11
|
close: () => void;
|
|
12
12
|
/** Execute a SQL statement (with optional bound params). */
|
|
13
13
|
exec: (sql: string, params?: ReadonlyArray<unknown>) => void;
|
|
14
|
-
/** Return the id of the last inserted row. */
|
|
15
|
-
lastInsertRowId: () => number;
|
|
16
14
|
/**
|
|
17
15
|
* Execute a SQL statement and return the result rows.
|
|
18
16
|
* Columns can be accessed by index or by name.
|
package/dist/react.d.mts
CHANGED
|
@@ -1,26 +1,36 @@
|
|
|
1
|
-
import { L as LocalMirror } from "./packem_shared/local-mirror.d-
|
|
2
|
-
import "./packem_shared/types.d-
|
|
1
|
+
import { L as LocalMirror } from "./packem_shared/local-mirror.d-CtQovAv_.mjs";
|
|
2
|
+
import "./packem_shared/types.d-BuLTPLaQ.mjs";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Result of {@link useLocalQuery} — a discriminated union so callers get a
|
|
5
|
+
* typed error instead of a swallowed `undefined`.
|
|
5
6
|
* @experimental
|
|
6
7
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* argument.
|
|
15
|
-
*/
|
|
16
|
-
shardKey?: string;
|
|
17
|
-
}
|
|
8
|
+
type LocalQueryResult<T> = {
|
|
9
|
+
readonly data: T[];
|
|
10
|
+
readonly error?: undefined;
|
|
11
|
+
} | {
|
|
12
|
+
readonly data?: undefined;
|
|
13
|
+
readonly error: Error;
|
|
14
|
+
};
|
|
18
15
|
/**
|
|
19
16
|
* React hook that subscribes to a local SQLite query and returns
|
|
20
17
|
* live-updating results whenever the mirror applies a diff.
|
|
21
18
|
*
|
|
22
|
-
* Uses `useSyncExternalStore`
|
|
23
|
-
*
|
|
19
|
+
* Uses `useSyncExternalStore` with `getSnapshot` reading {@link LocalMirror.version}
|
|
20
|
+
* — a plain number, unconditionally `Object.is`-stable across calls with no
|
|
21
|
+
* mutation in between. The actual query runs in a `useMemo` keyed on
|
|
22
|
+
* `[mirror, version, sql, stableParamsKey(params)]`, so it only re-executes
|
|
23
|
+
* when the mirror actually advances (or `sql`/`params` change) — not on
|
|
24
|
+
* every unrelated re-render.
|
|
25
|
+
*
|
|
26
|
+
* This intentionally does NOT cache query results inside {@link LocalMirror}
|
|
27
|
+
* itself: an LRU-capped cache in the mirror core evicts still-mounted hooks'
|
|
28
|
+
* entries once more than the cap's worth of distinct live queries are open
|
|
29
|
+
* on one mirror, which makes `getSnapshot` return a fresh (non-identical)
|
|
30
|
+
* object on the next read — `useSyncExternalStore` then force-re-renders,
|
|
31
|
+
* re-inserts, evicts another entry, and so on without bound. Keying the
|
|
32
|
+
* external-store snapshot on the version primitive instead of a cached
|
|
33
|
+
* query result has no such cap, so it can't loop.
|
|
24
34
|
*
|
|
25
35
|
* The hook works with React 18+ concurrent features, Suspense, and
|
|
26
36
|
* server-side rendering. During SSR the same value is returned as on
|
|
@@ -32,28 +42,28 @@ interface UseLocalQueryOptions {
|
|
|
32
42
|
* engine without rewriting).
|
|
33
43
|
* @param params Optional positional bound parameters matching `?`
|
|
34
44
|
* placeholders in `sql`.
|
|
35
|
-
* @
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* **Error handling**: The hook catches SQL errors internally and returns
|
|
43
|
-
* `undefined`. Use a try-catch around `mirror.query(...)` directly if you
|
|
44
|
-
* need finer-grained error diagnostics.
|
|
45
|
+
* @returns `{ data }` with the result rows typed via the generic parameter
|
|
46
|
+
* `T`, or `{ error }` when the query fails (e.g. malformed SQL, or the
|
|
47
|
+
* target table doesn't exist yet because no matching diff has been applied
|
|
48
|
+
* — that specific case surfaces as a "no such table" `Error`). Never
|
|
49
|
+
* collapses a failure to `undefined` — check `error` explicitly rather than
|
|
50
|
+
* treating a missing `data` as "still loading".
|
|
45
51
|
* @example
|
|
46
52
|
* ```tsx
|
|
47
53
|
* import { useLocalQuery } from "@lunora/replica/react";
|
|
48
54
|
* import { mirror } from "./mirror";
|
|
49
55
|
*
|
|
50
56
|
* function UserList() {
|
|
51
|
-
* const users = useLocalQuery<{ id: string; name: string }>(
|
|
57
|
+
* const { data: users, error } = useLocalQuery<{ id: string; name: string }>(
|
|
52
58
|
* mirror,
|
|
53
59
|
* "SELECT id, name FROM fn_todos_list WHERE name LIKE ?",
|
|
54
60
|
* ["%alice%"],
|
|
55
61
|
* );
|
|
56
62
|
*
|
|
63
|
+
* if (error) {
|
|
64
|
+
* return <p>Query failed: {error.message}</p>;
|
|
65
|
+
* }
|
|
66
|
+
*
|
|
57
67
|
* if (users === undefined) {
|
|
58
68
|
* return <p>Waiting for data…</p>;
|
|
59
69
|
* }
|
|
@@ -63,5 +73,5 @@ interface UseLocalQueryOptions {
|
|
|
63
73
|
* ```
|
|
64
74
|
* @experimental
|
|
65
75
|
*/
|
|
66
|
-
declare const useLocalQuery: <T = Record<string, unknown>>(mirror: LocalMirror, sql: string, params?: ReadonlyArray<unknown
|
|
67
|
-
export {
|
|
76
|
+
declare const useLocalQuery: <T = Record<string, unknown>>(mirror: LocalMirror, sql: string, params?: ReadonlyArray<unknown>) => LocalQueryResult<T>;
|
|
77
|
+
export { type LocalQueryResult, useLocalQuery };
|
package/dist/react.d.ts
CHANGED
|
@@ -1,26 +1,36 @@
|
|
|
1
|
-
import { L as LocalMirror } from "./packem_shared/local-mirror.d-
|
|
2
|
-
import "./packem_shared/types.d-
|
|
1
|
+
import { L as LocalMirror } from "./packem_shared/local-mirror.d-CErKffFW.js";
|
|
2
|
+
import "./packem_shared/types.d-BuLTPLaQ.js";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Result of {@link useLocalQuery} — a discriminated union so callers get a
|
|
5
|
+
* typed error instead of a swallowed `undefined`.
|
|
5
6
|
* @experimental
|
|
6
7
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* argument.
|
|
15
|
-
*/
|
|
16
|
-
shardKey?: string;
|
|
17
|
-
}
|
|
8
|
+
type LocalQueryResult<T> = {
|
|
9
|
+
readonly data: T[];
|
|
10
|
+
readonly error?: undefined;
|
|
11
|
+
} | {
|
|
12
|
+
readonly data?: undefined;
|
|
13
|
+
readonly error: Error;
|
|
14
|
+
};
|
|
18
15
|
/**
|
|
19
16
|
* React hook that subscribes to a local SQLite query and returns
|
|
20
17
|
* live-updating results whenever the mirror applies a diff.
|
|
21
18
|
*
|
|
22
|
-
* Uses `useSyncExternalStore`
|
|
23
|
-
*
|
|
19
|
+
* Uses `useSyncExternalStore` with `getSnapshot` reading {@link LocalMirror.version}
|
|
20
|
+
* — a plain number, unconditionally `Object.is`-stable across calls with no
|
|
21
|
+
* mutation in between. The actual query runs in a `useMemo` keyed on
|
|
22
|
+
* `[mirror, version, sql, stableParamsKey(params)]`, so it only re-executes
|
|
23
|
+
* when the mirror actually advances (or `sql`/`params` change) — not on
|
|
24
|
+
* every unrelated re-render.
|
|
25
|
+
*
|
|
26
|
+
* This intentionally does NOT cache query results inside {@link LocalMirror}
|
|
27
|
+
* itself: an LRU-capped cache in the mirror core evicts still-mounted hooks'
|
|
28
|
+
* entries once more than the cap's worth of distinct live queries are open
|
|
29
|
+
* on one mirror, which makes `getSnapshot` return a fresh (non-identical)
|
|
30
|
+
* object on the next read — `useSyncExternalStore` then force-re-renders,
|
|
31
|
+
* re-inserts, evicts another entry, and so on without bound. Keying the
|
|
32
|
+
* external-store snapshot on the version primitive instead of a cached
|
|
33
|
+
* query result has no such cap, so it can't loop.
|
|
24
34
|
*
|
|
25
35
|
* The hook works with React 18+ concurrent features, Suspense, and
|
|
26
36
|
* server-side rendering. During SSR the same value is returned as on
|
|
@@ -32,28 +42,28 @@ interface UseLocalQueryOptions {
|
|
|
32
42
|
* engine without rewriting).
|
|
33
43
|
* @param params Optional positional bound parameters matching `?`
|
|
34
44
|
* placeholders in `sql`.
|
|
35
|
-
* @
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* **Error handling**: The hook catches SQL errors internally and returns
|
|
43
|
-
* `undefined`. Use a try-catch around `mirror.query(...)` directly if you
|
|
44
|
-
* need finer-grained error diagnostics.
|
|
45
|
+
* @returns `{ data }` with the result rows typed via the generic parameter
|
|
46
|
+
* `T`, or `{ error }` when the query fails (e.g. malformed SQL, or the
|
|
47
|
+
* target table doesn't exist yet because no matching diff has been applied
|
|
48
|
+
* — that specific case surfaces as a "no such table" `Error`). Never
|
|
49
|
+
* collapses a failure to `undefined` — check `error` explicitly rather than
|
|
50
|
+
* treating a missing `data` as "still loading".
|
|
45
51
|
* @example
|
|
46
52
|
* ```tsx
|
|
47
53
|
* import { useLocalQuery } from "@lunora/replica/react";
|
|
48
54
|
* import { mirror } from "./mirror";
|
|
49
55
|
*
|
|
50
56
|
* function UserList() {
|
|
51
|
-
* const users = useLocalQuery<{ id: string; name: string }>(
|
|
57
|
+
* const { data: users, error } = useLocalQuery<{ id: string; name: string }>(
|
|
52
58
|
* mirror,
|
|
53
59
|
* "SELECT id, name FROM fn_todos_list WHERE name LIKE ?",
|
|
54
60
|
* ["%alice%"],
|
|
55
61
|
* );
|
|
56
62
|
*
|
|
63
|
+
* if (error) {
|
|
64
|
+
* return <p>Query failed: {error.message}</p>;
|
|
65
|
+
* }
|
|
66
|
+
*
|
|
57
67
|
* if (users === undefined) {
|
|
58
68
|
* return <p>Waiting for data…</p>;
|
|
59
69
|
* }
|
|
@@ -63,5 +73,5 @@ interface UseLocalQueryOptions {
|
|
|
63
73
|
* ```
|
|
64
74
|
* @experimental
|
|
65
75
|
*/
|
|
66
|
-
declare const useLocalQuery: <T = Record<string, unknown>>(mirror: LocalMirror, sql: string, params?: ReadonlyArray<unknown
|
|
67
|
-
export {
|
|
76
|
+
declare const useLocalQuery: <T = Record<string, unknown>>(mirror: LocalMirror, sql: string, params?: ReadonlyArray<unknown>) => LocalQueryResult<T>;
|
|
77
|
+
export { type LocalQueryResult, useLocalQuery };
|
package/dist/react.mjs
CHANGED
|
@@ -1,15 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
const useLocalQuery = (mirror, sql, params, _options) => {
|
|
4
|
-
const subscribe = (onStoreChange) => mirror.onChange(onStoreChange);
|
|
5
|
-
const getSnapshot = () => mirror.version;
|
|
6
|
-
const getServerSnapshot = () => mirror.version;
|
|
7
|
-
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
8
|
-
try {
|
|
9
|
-
return mirror.query(sql, params);
|
|
10
|
-
} catch {
|
|
11
|
-
return void 0;
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export { useLocalQuery };
|
|
1
|
+
import{useSyncExternalStore as y,useMemo as i}from"react";const u=t=>JSON.stringify(t??[],(r,e)=>typeof e=="bigint"?`${e.toString()}n`:e),S=(t,r,e)=>{const s=n=>t.onChange(n),o=()=>t.version,c=y(s,o,o),a=u(e);return i(()=>{try{return{data:t.query(r,e)}}catch(n){return{error:n instanceof Error?n:new Error(String(n))}}},[t,c,r,a])};export{S as useLocalQuery};
|
package/package.json
CHANGED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
class EventEmitter {
|
|
2
|
-
#listeners = /* @__PURE__ */ new Map();
|
|
3
|
-
#wildcardListeners = /* @__PURE__ */ new Set();
|
|
4
|
-
// ── Registration ──────────────────────────────────────────────────
|
|
5
|
-
/**
|
|
6
|
-
* Register a handler for a specific event type.
|
|
7
|
-
* @returns An unsubscribe function (equivalent to calling {@link off}).
|
|
8
|
-
*/
|
|
9
|
-
on(event, handler) {
|
|
10
|
-
let set = this.#listeners.get(event);
|
|
11
|
-
if (!set) {
|
|
12
|
-
set = /* @__PURE__ */ new Set();
|
|
13
|
-
this.#listeners.set(event, set);
|
|
14
|
-
}
|
|
15
|
-
set.add(handler);
|
|
16
|
-
return () => {
|
|
17
|
-
this.off(event, handler);
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Remove a previously registered handler.
|
|
22
|
-
*/
|
|
23
|
-
off(event, handler) {
|
|
24
|
-
this.#listeners.get(event)?.delete(handler);
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Register a wildcard handler that fires for **every** event type.
|
|
28
|
-
* @returns An unsubscribe function.
|
|
29
|
-
*/
|
|
30
|
-
onAny(handler) {
|
|
31
|
-
this.#wildcardListeners.add(handler);
|
|
32
|
-
return () => {
|
|
33
|
-
this.#wildcardListeners.delete(handler);
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Remove a wildcard handler.
|
|
38
|
-
*/
|
|
39
|
-
offAny(handler) {
|
|
40
|
-
this.#wildcardListeners.delete(handler);
|
|
41
|
-
}
|
|
42
|
-
// ── Emission ──────────────────────────────────────────────────────
|
|
43
|
-
/**
|
|
44
|
-
* Emit an event. All registered handlers (typed + wildcard) are invoked
|
|
45
|
-
* synchronously. Exceptions from handlers are caught and silently
|
|
46
|
-
* swallowed — they **must not** break the emitter loop.
|
|
47
|
-
* @returns `true` if at least one handler was called.
|
|
48
|
-
*/
|
|
49
|
-
emit(event, payload) {
|
|
50
|
-
let called = false;
|
|
51
|
-
const typed = this.#listeners.get(event);
|
|
52
|
-
if (typed) {
|
|
53
|
-
for (const handler of typed) {
|
|
54
|
-
try {
|
|
55
|
-
handler(payload);
|
|
56
|
-
called = true;
|
|
57
|
-
} catch {
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
for (const handler of this.#wildcardListeners) {
|
|
62
|
-
try {
|
|
63
|
-
handler(event, payload);
|
|
64
|
-
called = true;
|
|
65
|
-
} catch {
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return called;
|
|
69
|
-
}
|
|
70
|
-
// ── Introspection ─────────────────────────────────────────────────
|
|
71
|
-
/**
|
|
72
|
-
* Return `true` when at least one listener is registered for `event`.
|
|
73
|
-
*/
|
|
74
|
-
hasListeners(event) {
|
|
75
|
-
return (this.#listeners.get(event)?.size ?? 0) > 0 || this.#wildcardListeners.size > 0;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Return the number of typed listeners for a specific event.
|
|
79
|
-
*/
|
|
80
|
-
listenerCount(event) {
|
|
81
|
-
return this.#listeners.get(event)?.size ?? 0;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Remove all listeners.
|
|
85
|
-
*/
|
|
86
|
-
clear() {
|
|
87
|
-
this.#listeners.clear();
|
|
88
|
-
this.#wildcardListeners.clear();
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export { EventEmitter };
|