@lunora/replica 1.0.0-alpha.7 → 1.0.0-alpha.71

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.
Files changed (49) hide show
  1. package/LICENSE.md +214 -0
  2. package/dist/adapters/better-sqlite3.mjs +1 -29
  3. package/dist/adapters/sqlite-wasm.d.mts +2 -2
  4. package/dist/adapters/sqlite-wasm.d.ts +2 -2
  5. package/dist/adapters/sqlite-wasm.mjs +1 -44
  6. package/dist/adapters/sqljs.mjs +1 -55
  7. package/dist/index.d.mts +43 -16
  8. package/dist/index.d.ts +43 -16
  9. package/dist/index.mjs +1 -20
  10. package/dist/packem_shared/EventEmitter-uo75adUL.mjs +1 -0
  11. package/dist/packem_shared/EventLog-B1-yhArT.mjs +1 -0
  12. package/dist/packem_shared/EventLogDO-CaZvpgJN.mjs +1 -0
  13. package/dist/packem_shared/EventLogDOClient-DGDMj5L5.mjs +1 -0
  14. package/dist/packem_shared/EventSource-BC0hKJSA.mjs +1 -0
  15. package/dist/packem_shared/EventsSync-DWyGGZQZ.mjs +1 -0
  16. package/dist/packem_shared/InMemorySnapshotStore-C4taIG5K.mjs +1 -0
  17. package/dist/packem_shared/LocalMirror-BIURA-He.mjs +4 -0
  18. package/dist/packem_shared/MaterializerRuntime-D0HOAe24.mjs +1 -0
  19. package/dist/packem_shared/SubscriptionManager-AhPw3lFc.mjs +1 -0
  20. package/dist/packem_shared/applyDiff-Da160K7_.mjs +1 -0
  21. package/dist/packem_shared/applyDiffToDb-C6ek5Elp.mjs +1 -0
  22. package/dist/packem_shared/classifyChanges-ioMfjjbU.mjs +1 -0
  23. package/dist/packem_shared/defineEvents-DHo-VK7G.mjs +1 -0
  24. package/dist/packem_shared/eventsContext-Dxow9Y7S.mjs +1 -0
  25. package/dist/packem_shared/isClientSeq-D2Xm0_lj.mjs +1 -0
  26. package/dist/packem_shared/{local-mirror.d-CyGOpUES.d.ts → local-mirror.d-ByrYTq4z.d.ts} +2 -2
  27. package/dist/packem_shared/{local-mirror.d-DTavX_y0.d.mts → local-mirror.d-Cip3QuMf.d.mts} +2 -2
  28. package/dist/packem_shared/subscribeToMirror-CX10AaP3.mjs +1 -0
  29. package/dist/react.d.mts +41 -14
  30. package/dist/react.d.ts +41 -14
  31. package/dist/react.mjs +1 -15
  32. package/package.json +1 -1
  33. package/dist/packem_shared/EventEmitter-CMZfct03.mjs +0 -92
  34. package/dist/packem_shared/EventLog-CnK-3Wge.mjs +0 -264
  35. package/dist/packem_shared/EventLogDO-DqlsVx0H.mjs +0 -381
  36. package/dist/packem_shared/EventLogDOClient-F4FO8Si4.mjs +0 -92
  37. package/dist/packem_shared/EventSource-D5yO9_aI.mjs +0 -221
  38. package/dist/packem_shared/EventsSync-BP36tC9O.mjs +0 -123
  39. package/dist/packem_shared/InMemorySnapshotStore-BHVAD-Bp.mjs +0 -24
  40. package/dist/packem_shared/LocalMirror-a-5jEqFN.mjs +0 -219
  41. package/dist/packem_shared/MaterializerRuntime-BoIrsMYB.mjs +0 -224
  42. package/dist/packem_shared/SubscriptionManager-C5xbw0pg.mjs +0 -75
  43. package/dist/packem_shared/applyDiff-98tKzmiW.mjs +0 -67
  44. package/dist/packem_shared/applyDiffToDb-DQ1xZp5J.mjs +0 -58
  45. package/dist/packem_shared/classifyChanges-RcqLBpLs.mjs +0 -41
  46. package/dist/packem_shared/defineEvents-DiBkPTh_.mjs +0 -28
  47. package/dist/packem_shared/eventsContext-Bk_p48hj.mjs +0 -6
  48. package/dist/packem_shared/isClientSeq-C46BkzqJ.mjs +0 -5
  49. package/dist/packem_shared/subscribeToMirror-CiaM-nQ7.mjs +0 -45
@@ -0,0 +1 @@
1
+ class h{#t=new Map;#e=new Set;on(t,s){let e=this.#t.get(t);return e||(e=new Set,this.#t.set(t,e)),e.add(s),()=>{this.off(t,s)}}off(t,s){this.#t.get(t)?.delete(s)}onAny(t){return this.#e.add(t),()=>{this.#e.delete(t)}}offAny(t){this.#e.delete(t)}emit(t,s){let e=!1;const r=this.#t.get(t);if(r)for(const i of r)try{i(s),e=!0}catch{}for(const i of this.#e)try{i(t,s),e=!0}catch{}return e}hasListeners(t){return(this.#t.get(t)?.size??0)>0||this.#e.size>0}listenerCount(t){return this.#t.get(t)?.size??0}clear(){this.#t.clear(),this.#e.clear()}}export{h as EventEmitter};
@@ -0,0 +1 @@
1
+ class l{#e=[];#t=0;#i=0;#s=null;#n;constructor(t){this.#n=l.#o(t?.maxEntries)}static#o(t){if(t!==void 0){if(!Number.isSafeInteger(t)||t<0)throw new RangeError("maxEntries must be a non-negative safe integer");return t}}#r(){return this.#e.slice(this.#t)}#h(){if(this.#n===void 0)return;const e=this.#e.length-this.#t-this.#n;e>0&&(this.#t+=e),this.#t>0&&this.#t>=this.#e.length-this.#t&&(this.#e=this.#e.slice(this.#t),this.#t=0)}append(t,e,s,n){const r=typeof t=="string"?t:t.type,o=typeof t=="string"?e:t.payload;let h,i;typeof t=="string"?(h=s,i=n):(h=void 0,i=e);const a=i?.parentSeqNum??this.#s??void 0,f=this.#i;this.#i+=1;const c={seq:f,type:r,payload:o,timestamp:i?.timestamp??Date.now(),tableDiffs:h,clientId:i?.clientId,sessionId:i?.sessionId,parentSeqNum:a};return this.#e.push(c),this.#s=c.seq,this.#h(),c}commitAll(t){if(t.length===0)return[];const e=[];for(const s of t){const{type:n}=s,r="payload"in s?s.payload:void 0,o="timestamp"in s?s.timestamp:Date.now(),h=e.at(-1)?.seq??this.#s??void 0,i=this.#i;this.#i+=1;const a={seq:i,type:n,payload:r,timestamp:o,parentSeqNum:h};this.#e.push(a),e.push(a)}return this.#s=e.at(-1)?.seq??this.#s,this.#h(),e}load(t){this.#e=[...t.entries],this.#t=0,this.#i=t.nextSeq,this.#s=t.headSeq,this.#h()}getSince(t){const e=this.#r();if(t<=0)return e;const s=e.findIndex(n=>n.seq>=t);return s===-1?[]:e.slice(s)}getFrom(t,e=50){const s=this.#e,n=this.#t,r=s.findIndex((h,i)=>i>=n&&h.seq>=t);if(r===-1)return{entries:[],hasMore:!1};const o=Math.min(r+e,s.length);return{entries:s.slice(r,o),hasMore:o<s.length}}snapshot(){return{entries:this.#r(),nextSeq:this.#i,headSeq:this.#s}}get size(){return this.#e.length-this.#t}get nextSeq(){return this.#i}get isEmpty(){return this.size===0}get headSeq(){return this.#s}clear(){this.#e=[],this.#t=0,this.#i=0,this.#s=null}truncateBelow(t){if(!Number.isSafeInteger(t)||t<0)throw new RangeError("floorSeq must be a non-negative safe integer");const e=this.#r(),s=e.findIndex(n=>n.seq>=t);this.#e=s===-1?[]:e.slice(s),this.#t=0}async*events(t=0){const e=this.getSince(t);for(const s of e)yield s}}export{l as EventLog};
@@ -0,0 +1 @@
1
+ const T=Symbol("lunora.replica.event-log-do.idempotency-conflict"),q=n=>{const t=new Error(n);return Object.defineProperty(t,T,{value:!0}),t},N=n=>n instanceof Error&&T in n,h=n=>{if(Array.isArray(n))return n.map(t=>h(t));if(n!==null&&typeof n=="object"){const t=n,e=Object.keys(t);e.sort();const s={};for(const r of e)s[r]=h(t[r]);return s}return n},S=async n=>{const t=JSON.stringify(h(n)),e=new TextEncoder().encode(t),s=await crypto.subtle.digest("SHA-256",e);return[...new Uint8Array(s)].map(r=>r.toString(16).padStart(2,"0")).join("")},l=(n,t=200)=>Response.json(n,{status:t,headers:{"content-type":"application/json"}}),u=(n,t,e)=>l({error:{code:t,message:e}},n),E=n=>typeof n.toArray=="function"?n.toArray():typeof n[Symbol.iterator]=="function"?[...n]:[],f=n=>E(n).map(e=>({seq:e.seq,type:e.type,payload:JSON.parse(e.payload),timestamp:e.timestamp,clientId:e.client_id??void 0,sessionId:e.session_id??void 0,parentSeqNum:e.parent_seq??void 0}));class p{state;env;#t=!1;constructor(t,e){this.state=t,this.env=e}async fetch(t){this.#p();const e=new URL(t.url);try{if(t.method==="POST"&&e.pathname==="/append")return await this.#e(t);if(t.method==="GET"&&e.pathname==="/since")return this.#i(e);if(t.method==="GET"&&e.pathname==="/range")return this.#a(e);if(t.method==="GET"&&e.pathname==="/size")return this.#o();if(t.method==="GET"&&e.pathname==="/state")return this.#c()}catch(s){return console.error("[event-log-do] request failed:",s),u(500,"INTERNAL_ERROR","internal error")}return u(404,"NOT_FOUND","unknown route")}async#e(t){let e;try{e=await t.json()}catch{return u(400,"BAD_REQUEST","invalid JSON body")}const s=p.#n(e);if(s)return u(400,"BAD_REQUEST",s);const{sql:r}=this.state.storage,{batchId:i}=e,o=()=>p.#s(r,e,i),{transaction:a}=this.state.storage;let c;try{c=typeof a=="function"?await a(o):await o()}catch(m){if(N(m))return u(409,"CONFLICT",m.message);throw m}return l({entries:c})}static async#s(t,e,s){let r;if(typeof s=="string"){r=await S(e.events);const a=p.#u(t,s);if(a){if(a.fingerprint!==r)throw q(`batchId "${s}" was already used for a different event batch`);return a.entries}}const i=Date.now(),o=[];for(const a of e.events){const d={seq:p.#l(t),type:a.type,payload:a.payload,timestamp:a.timestamp??i,clientId:a.clientId,sessionId:a.sessionId,parentSeqNum:a.parentSeqNum};p.#m(t,d),o.push(d)}if(typeof s=="string"&&r!==void 0){const a=o[0]?.seq,c=o.at(-1)?.seq;a!==void 0&&c!==void 0&&p.#d(t,s,a,c,r)}return o}static#n(t){if(!Array.isArray(t.events)||t.events.length===0)return"events[] with a non-empty string `type` required";if(t.batchId!==void 0&&(typeof t.batchId!="string"||t.batchId.length===0))return"batchId must be a non-empty string";for(const e of t.events){const s=p.#r(e);if(s!==void 0)return s}}static#r(t){if(typeof t.type!="string"||t.type.length===0)return"events[] with a non-empty string `type` required";if(t.timestamp!==void 0&&!Number.isFinite(t.timestamp))return"events[].timestamp must be a finite number";if(t.clientId!==void 0&&typeof t.clientId!="string")return"events[].clientId must be a string";if(t.sessionId!==void 0&&typeof t.sessionId!="string")return"events[].sessionId must be a string";if(t.parentSeqNum!==void 0&&(typeof t.parentSeqNum!="number"||!Number.isInteger(t.parentSeqNum)||t.parentSeqNum<0))return"events[].parentSeqNum must be a non-negative integer"}#i(t){const e=t.searchParams.get("seq"),s=e===null?0:Number(e);if(!Number.isFinite(s)||s<0)return u(400,"BAD_REQUEST","invalid seq");const{sql:r}=this.state.storage,i=r.exec("SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events WHERE seq >= ? ORDER BY seq ASC",s);return l({entries:f(i)})}#a(t){const e=t.searchParams.get("from"),s=e===null?0:Number(e),r=t.searchParams.get("limit"),i=r===null?50:Number(r);if(!Number.isFinite(s)||s<0||!Number.isFinite(i)||i<1||i>1e3)return u(400,"BAD_REQUEST","invalid from/limit");const{sql:o}=this.state.storage,a=o.exec("SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events WHERE seq >= ? ORDER BY seq ASC LIMIT ?",s,i+1),c=f(a),d=c.length>i,y={entries:d?c.slice(0,i):c,hasMore:d};return l(y)}#o(){const{sql:t}=this.state.storage,e=t.exec("SELECT COUNT(*) AS count FROM events"),r=E(e)[0]?.count??0;return l({count:r})}#c(){const{sql:t}=this.state.storage,e=t.exec("SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events ORDER BY seq ASC"),s=f(e),r=(s.at(-1)?.seq??-1)+1;return l({entries:s,nextSeq:r})}#p(){if(this.#t)return;const{sql:t}=this.state.storage;t.exec("CREATE TABLE IF NOT EXISTS events (seq INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, payload TEXT NOT NULL, timestamp INTEGER NOT NULL, client_id TEXT, session_id TEXT, parent_seq INTEGER)"),t.exec("CREATE TABLE IF NOT EXISTS event_batches (batch_id TEXT PRIMARY KEY, first_seq INTEGER NOT NULL, last_seq INTEGER NOT NULL, fingerprint TEXT NOT NULL)"),this.#t=!0}static#u(t,e){const s=t.exec("SELECT first_seq, last_seq, fingerprint FROM event_batches WHERE batch_id = ?",e),i=E(s)[0];if(!i)return;const o=t.exec("SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events WHERE seq >= ? AND seq <= ? ORDER BY seq ASC",i.first_seq,i.last_seq);return{entries:f(o),fingerprint:i.fingerprint}}static#d(t,e,s,r,i){t.exec("INSERT INTO event_batches (batch_id, first_seq, last_seq, fingerprint) VALUES (?, ?, ?, ?)",e,s,r,i)}static#l(t){const e=t.exec("SELECT COALESCE(MAX(seq), -1) + 1 AS next_seq FROM events");return E(e)[0]?.next_seq??0}static#m(t,e){const s=typeof e.parentSeqNum=="number"?e.parentSeqNum:null;t.exec("INSERT INTO events (seq, type, payload, timestamp, client_id, session_id, parent_seq) VALUES (?, ?, ?, ?, ?, ?, ?)",e.seq,e.type,JSON.stringify(e.payload),e.timestamp,e.clientId??null,e.sessionId??null,s)}}export{p as EventLogDO};
@@ -0,0 +1 @@
1
+ class s{#a;constructor(t){this.#a=t.fetch}async append(t,a){const e=JSON.stringify({events:t,batchId:a?.batchId}),n=await this.#a(new Request("https://do/append",{method:"POST",headers:{"content-type":"application/json"},body:e}));if(!n.ok)throw await s.#e(n,"append");return(await n.json()).entries}async getSince(t){return(await this.#t(`/since?seq=${String(t)}`,"getSince")).entries}async getRange(t,a=50){return this.#t(`/range?from=${String(t)}&limit=${String(a)}`,"getRange")}async getSize(){return(await this.#t("/size","getSize")).count}async getState(){return this.#t("/state","getState")}async#t(t,a){const e=await this.#a(new Request(`https://do${t}`));if(!e.ok)throw await s.#e(e,a);return await e.json()}static async#e(t,a){try{const n=(await t.json()).error?.message??t.statusText;return new Error(`EventLogDO.${a} failed (${String(t.status)}): ${n}`)}catch{return new Error(`EventLogDO.${a} failed (${String(t.status)}): ${t.statusText}`)}}}export{s as EventLogDOClient};
@@ -0,0 +1 @@
1
+ import{EventEmitter as p}from"./EventEmitter-uo75adUL.mjs";import{EventLog as f}from"./EventLog-B1-yhArT.mjs";const d=Symbol("lunora.replica.event-source.unhandled");class w{emitter=new p;log;#e;#t;#n=!1;#i;#r=-1;constructor(e,t,n){this.#e={...e},this.#t=t,this.#i=n?.unknownEventHandling??"warn",this.log=new f({maxEntries:n?.maxLogEntries})}get state(){return this.#e}get replayed(){return this.#n}applyEvent(e,t,n){let r,o,i;typeof e=="string"?(r=e,o=t,i=n):(r=e.type,o=e.payload,i=t);const s={seq:this.log.nextSeq,type:r,payload:o,timestamp:Date.now(),clientId:i?.clientId,sessionId:i?.sessionId,parentSeqNum:i?.parentSeqNum??this.log.headSeq??void 0};let a;try{a=this.#t(this.#e,s)}catch(h){const u=h instanceof Error?h:new Error(String(h));throw this.emitter.emit("replay-error",{entry:s,error:u}),u}let c=!0;a===d&&(c=this.#s(s));const l=this.log.append(r,o,void 0,{...i,timestamp:s.timestamp});return a===d?(c&&this.emitter.emit("state-changed",{state:this.#e,entry:l}),l):(this.#e=a,this.emitter.emit("state-changed",{state:this.#e,entry:l}),l)}replayFromLog(e){const t=e.getSince(this.#r+1);for(const n of t)try{const r=this.#t(this.#e,n);r!==d&&(this.#e=r),this.log.append(n.type,n.payload,n.tableDiffs,{clientId:n.clientId,sessionId:n.sessionId,parentSeqNum:n.parentSeqNum})}catch(r){this.emitter.emit("replay-error",{entry:n,error:r instanceof Error?r:new Error(String(r))})}finally{this.#r=n.seq}this.#n=!0,this.emitter.emit("ready",{entryCount:this.log.size})}reset(e,t=-1){this.#e={...e},this.#r=t,this.#n=t>=0}async*events(e){const t=[];let n;const r=this.emitter.on("state-changed",({entry:i})=>{t.push(i),n?.()}),o=()=>{n?.()};e?.addEventListener("abort",o,{once:!0});try{let i=0;for(;i<this.log.size;){if(e?.aborted)return;for(const s of this.log.getSince(i))yield s;i=this.log.nextSeq}for(;!e?.aborted;){for(;t.length>0;){const s=t.shift();s&&(yield s)}t.length===0&&await new Promise(s=>{n=s})}}finally{r(),e?.removeEventListener("abort",o)}}#s(e){const t=this.#i;if(typeof t=="function")return t(e);switch(t){case"ignore":return!1;case"fail":throw new Error(`EventSource: unhandled event type "${e.type}" (seq ${String(e.seq)}). Configure \`unknownEventHandling\` to handle this event or change the strategy.`);default:return console.warn(`[EventSource] unhandled event type "${e.type}" (seq ${String(e.seq)}). The event was skipped. Configure \`unknownEventHandling\` if this is expected.`),!1}}}export{w as EventSource,d as UNHANDLED};
@@ -0,0 +1 @@
1
+ class e{#t;#i=0;#s;#r;constructor(t){this.#t=t}get watermark(){return this.#i}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.#r)return this.#r;const t=this.#e().finally(()=>{this.#r=void 0});return this.#r=t,t}async#e(){try{const t=await this.#t.fetchEventsSince(this.#i);if(t.length===0)return 0;this.#t.applyEvents(t);const s=this.#t.getTableDiffs();for(const i of s)this.#t.mirror.applyDiff(i);const r=t[t.length-1];return this.#i=r.seq+1,t.length}catch(t){return(this.#t.onError??console.error)(t),0}}}export{e 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 h="__lunora_mirror_meta",g=o=>{o.exec(`CREATE TABLE IF NOT EXISTS ${h} (
2
+ key TEXT PRIMARY KEY NOT NULL,
3
+ value TEXT NOT NULL
4
+ )`)},T="schema_version",f=3,l=o=>typeof o=="bigint"||typeof o=="boolean"?"INTEGER":typeof o=="number"?Number.isInteger(o)?"INTEGER":"REAL":"TEXT";class E{#e;#t;#n;#s=new Set;#i=0;static create(e,t){const n=y(e);return new E({db:n,tables:t?.tables})}constructor(e){this.#e=e.db,this.#t={...e.tables},this.#n=new b({maxEntries:e.maxEventLogEntries}),g(this.#e),this.#o()}onChange(e){return this.#s.add(e),()=>{this.#s.delete(e)}}get eventLog(){return this.#n}get db(){return this.#e}get version(){return this.#i}applyDiff(e){if(e.changes.length===0)return;const t=this.#t[e.table]?.primaryKey??"id";this.#h(e),m(this.#e,e,t),this.#n.append("table-diff",e,[e]),this.#r()}query(e,t){return this.#e.query(e,t)}clearData(){const e=this.#a();this.#e.transaction(()=>{for(const{name:t}of e)this.#e.exec(`DELETE FROM ${a(t)}`)}),this.#r()}#r(){this.#i+=1;for(const e of this.#s)try{e()}catch{}}close(){this.#e.close(),this.#n.clear(),this.#s.clear()}registerTable(e,t){this.#t[e]=t}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 ${h}`).find(s=>s.key===T)?.value===String(f))return;const n=this.#a();this.#e.transaction(()=>{for(const{name:s}of n)this.#e.exec(`DROP TABLE IF EXISTS ${a(s)}`);this.#e.exec(`INSERT OR REPLACE INTO ${h} (key, value) VALUES (?, ?)`,[T,String(f)])})}static#c(e,t){const n=new Set;for(const s of e.changes)if(s.type!=="delete")for(const c of Object.keys(s.data))c!==t&&n.add(c);return n}static#E(e,t,n){const s=new Map;for(const c of e.changes)if(!(c.type==="delete"||s.size===n.size))for(const r of n){if(r===t||s.has(r))continue;const i=c.data[r];i!=null&&s.set(r,l(i))}return s}static#l(e,t){for(const n of e.changes){const s=n.type==="delete"?n.id:n.data[t];if(s!=null)return l(s);if(n.type==="update")return l(n.id)}return"TEXT"}#h(e){const t=this.#t[e.table]?.primaryKey??"id",n=E.#c(e,t),s=E.#E(e,t,n);if(this.#e.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?",[e.table]).length===0){const r=E.#l(e,t);let i=`${a(t)} ${r==="INTEGER"?"INT":r} PRIMARY KEY NOT NULL`;for(const u of n)i+=`, ${a(u)} ${s.get(u)??"TEXT"}`;this.#e.exec(`CREATE TABLE IF NOT EXISTS ${a(e.table)} (${i})`)}else if(n.size>0){const r=new Set(this.#e.query(`PRAGMA table_info(${a(e.table)})`).map(i=>i.name));for(const i of n)r.has(i)||this.#e.exec(`ALTER TABLE ${a(e.table)} ADD COLUMN ${a(i)} ${s.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{#t;#n;#i;#s;#e;constructor(e,t={}){this.#t=[...e],this.#e=this.#t.map(()=>0),this.#n=t.snapshotStore,this.#i=t.doClient,this.#s=t.unknownEventHandling??"warn"}get appliedSeq(){return this.#e.length>0?Math.min(...this.#e):0}applyEntries(e){let t=0;for(const n of e){let s=!1,i=!1;const r=[];for(const[o,h]of this.#t.entries()){const l=this.#e[o]??0;if(n.seq<l)continue;const p=h.state;h.apply(n),s=!0,h.state!==p&&(i=!0),r.push(o)}if(s){i||this.#r(n);for(const o of r)this.#e[o]=n.seq+1;t+=1}}return t}#r(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.#n)return 0;let e=0;for(const[t,n]of this.#t.entries()){const s=await this.#n.load(n.def.name);if(s!==null&&typeof s=="object"){const i=s;if(Number.isSafeInteger(i.appliedSeq)&&i.appliedSeq>=0&&i.state!==void 0){const r=i.appliedSeq;n.setState(i.state),this.#e[t]=r,r>e&&(e=r)}}}return e}async persistSnapshots(){if(this.#n)for(const[e,t]of this.#t.entries())await this.#n.save(t.def.name,{appliedSeq:this.#e[e]??0,state:t.state})}async initialize(){if(!this.#i)return 0;await this.recoverFromSnapshots();const e=this.#e.length>0?Math.min(...this.#e):0,t=await this.#i.getSince(e);return t.length===0?0:this.applyEntries(t)}async appendEvent(e){if(!this.#i)throw new Error("MaterializerRuntime.appendEvent requires a doClient — pass one in the constructor options.");const n=(await this.#i.append([e]))[0];if(!n)throw new Error("MaterializerRuntime.appendEvent: DO returned empty result");return this.applyEntries([n]),n}reset(){for(const[e,t]of this.#t.entries())this.#e[e]=0,t.reset()}get materializers(){return this.#t}}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
+ const a=t=>{if(Array.isArray(t))return t.map(n=>a(n));if(t!==null&&typeof t=="object"){const n=t,e=Object.keys(n);e.sort();const o={};for(const s of e)o[s]=a(n[s]);return o}return t},r=t=>t.toString(16).padStart(4,"0"),u=t=>{let n=8997,e=33826,o=40164,s=52210;for(let c=0;c<t.length;c+=1){const i=t.codePointAt(c)??0;n^=i&65535,e^=i>>>16&65535;const p=n*435,y=e*435,h=o*435+n*256,b=s*435+e*256,d=y+(p>>>16),f=h+(d>>>16),g=b+(f>>>16);n=p&65535,e=d&65535,o=f&65535,s=g&65535}return r(s)+r(o)+r(e)+r(n)},x=(t,n,e)=>{const o=t.id??String(t.timestamp),s=`${t.table}::${o}::${String(n)}::${JSON.stringify(a(e))}`;return`row-${u(s)}`},l=(t,n)=>{for(const[e,o]of n.changes.entries())switch(o.type){case"delete":{t.delete(o.id);break}case"insert":{const s=o.data.id,c=typeof s=="string"||typeof s=="number"?String(s):x(n,e,o.data);t.set(c,{...o.data,id:c});break}case"update":{const s=t.get(o.id);s&&t.set(o.id,{...s,...o.data});break}}},w=(t,n)=>{const e=new Map(t);return l(e,n),e},S=(t,n)=>{const e=new Map(t);for(const o of n)l(e,o);return e},I=(t,n)=>{const e=new Map(t),o=e.get(n.table)??new Map;return e.set(n.table,w(o,n)),e};export{w as applyDiff,I as applyDiffToSnapshot,S as applyDiffs,a as canonicalizeForHash,x as deriveInsertId,u 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{fnv1a64Hex as a}from"./applyDiff-Da160K7_.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,u=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}},f=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{u as classifyChanges,i as createTableDiff,m as diffSize,g as isDiffEmpty,f 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 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};
@@ -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&lt;"chat.messageSent", { channelId: string; text: string }>
143
+ * // event: InputEvent<"chat.messageSent", { channelId: string; text: string }>
144
144
  * ```
145
145
  * @experimental
146
146
  */
@@ -420,7 +420,7 @@ interface LocalMirrorOptions {
420
420
  * mirror.applyDiff(someDiff);
421
421
  *
422
422
  * // Query locally:
423
- * const rows = mirror.query&lt;{ id: string; name: string }>(
423
+ * const rows = mirror.query<{ id: string; name: string }>(
424
424
  * "SELECT id, name FROM users WHERE name LIKE ?",
425
425
  * ["alice%"],
426
426
  * );
@@ -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&lt;"chat.messageSent", { channelId: string; text: string }>
143
+ * // event: InputEvent<"chat.messageSent", { channelId: string; text: string }>
144
144
  * ```
145
145
  * @experimental
146
146
  */
@@ -420,7 +420,7 @@ interface LocalMirrorOptions {
420
420
  * mirror.applyDiff(someDiff);
421
421
  *
422
422
  * // Query locally:
423
- * const rows = mirror.query&lt;{ id: string; name: string }>(
423
+ * const rows = mirror.query<{ id: string; name: string }>(
424
424
  * "SELECT id, name FROM users WHERE name LIKE ?",
425
425
  * ["alice%"],
426
426
  * );
@@ -0,0 +1 @@
1
+ const y=e=>`fn_${e.replaceAll(/[/:.]/g,"_")}`,w=e=>Array.isArray(e)?e:e!==null&&typeof e=="object"?[e]:[],d=(e,o,c,a,p)=>{const i=y(c.__lunoraRef);o.registerTable(i,{});let f=new Set;return e.subscribe(c,a,u=>{const b=w(u),n=new Set,r=[];for(const t of b){const l=t,s=l.id;(typeof s=="string"||typeof s=="number")&&n.add(String(s)),r.push({type:"insert",data:l})}for(const t of f)n.has(t)||r.push({type:"delete",id:t});f=n,r.length!==0&&o.applyDiff({table:i,changes:r,timestamp:Date.now()})},{shardKey:p})};export{d as subscribeToMirror};
package/dist/react.d.mts CHANGED
@@ -1,5 +1,17 @@
1
- import { L as LocalMirror } from "./packem_shared/local-mirror.d-DTavX_y0.mjs";
1
+ import { L as LocalMirror } from "./packem_shared/local-mirror.d-Cip3QuMf.mjs";
2
2
  import "./packem_shared/types.d-CkMkSwLJ.mjs";
3
+ /**
4
+ * Result of {@link useLocalQuery} — a discriminated union so callers get a
5
+ * typed error instead of a swallowed `undefined`.
6
+ * @experimental
7
+ */
8
+ type LocalQueryResult<T> = {
9
+ readonly data: T[];
10
+ readonly error?: undefined;
11
+ } | {
12
+ readonly data?: undefined;
13
+ readonly error: Error;
14
+ };
3
15
  /**
4
16
  * Options for the {@link useLocalQuery} hook.
5
17
  * @experimental
@@ -19,8 +31,21 @@ interface UseLocalQueryOptions {
19
31
  * React hook that subscribes to a local SQLite query and returns
20
32
  * live-updating results whenever the mirror applies a diff.
21
33
  *
22
- * Uses `useSyncExternalStore` to subscribe to the mirror's `onChange`
23
- * callback every diff triggers a re-query against the local SQLite.
34
+ * Uses `useSyncExternalStore` with `getSnapshot` reading {@link LocalMirror.version}
35
+ * — a plain number, unconditionally `Object.is`-stable across calls with no
36
+ * mutation in between. The actual query runs in a `useMemo` keyed on
37
+ * `[mirror, version, sql, stableParamsKey(params)]`, so it only re-executes
38
+ * when the mirror actually advances (or `sql`/`params` change) — not on
39
+ * every unrelated re-render.
40
+ *
41
+ * This intentionally does NOT cache query results inside {@link LocalMirror}
42
+ * itself: an LRU-capped cache in the mirror core evicts still-mounted hooks'
43
+ * entries once more than the cap's worth of distinct live queries are open
44
+ * on one mirror, which makes `getSnapshot` return a fresh (non-identical)
45
+ * object on the next read — `useSyncExternalStore` then force-re-renders,
46
+ * re-inserts, evicts another entry, and so on without bound. Keying the
47
+ * external-store snapshot on the version primitive instead of a cached
48
+ * query result has no such cap, so it can't loop.
24
49
  *
25
50
  * The hook works with React 18+ concurrent features, Suspense, and
26
51
  * server-side rendering. During SSR the same value is returned as on
@@ -34,26 +59,28 @@ interface UseLocalQueryOptions {
34
59
  * placeholders in `sql`.
35
60
  * @param _options Optional configuration (currently unused; reserved for
36
61
  * future features like shard key routing).
37
- * @returns An array of result rows typed via the generic parameter `T`, or
38
- * `undefined` if the query fails (e.g. the target table doesn't exist yet
39
- * because no matching diff has been applied). Treat `undefined` as a
40
- * "loading" or "no data yet" signal in your component.
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.
62
+ * @returns `{ data }` with the result rows typed via the generic parameter
63
+ * `T`, or `{ error }` when the query fails (e.g. malformed SQL, or the
64
+ * target table doesn't exist yet because no matching diff has been applied
65
+ * that specific case surfaces as a "no such table" `Error`). Never
66
+ * collapses a failure to `undefined` — check `error` explicitly rather than
67
+ * treating a missing `data` as "still loading".
45
68
  * @example
46
69
  * ```tsx
47
70
  * import { useLocalQuery } from "@lunora/replica/react";
48
71
  * import { mirror } from "./mirror";
49
72
  *
50
73
  * function UserList() {
51
- * const users = useLocalQuery<{ id: string; name: string }>(
74
+ * const { data: users, error } = useLocalQuery<{ id: string; name: string }>(
52
75
  * mirror,
53
76
  * "SELECT id, name FROM fn_todos_list WHERE name LIKE ?",
54
77
  * ["%alice%"],
55
78
  * );
56
79
  *
80
+ * if (error) {
81
+ * return <p>Query failed: {error.message}</p>;
82
+ * }
83
+ *
57
84
  * if (users === undefined) {
58
85
  * return <p>Waiting for data…</p>;
59
86
  * }
@@ -63,5 +90,5 @@ interface UseLocalQueryOptions {
63
90
  * ```
64
91
  * @experimental
65
92
  */
66
- declare const useLocalQuery: <T = Record<string, unknown>>(mirror: LocalMirror, sql: string, params?: ReadonlyArray<unknown>, _options?: UseLocalQueryOptions) => T[] | undefined;
67
- export { UseLocalQueryOptions, useLocalQuery };
93
+ declare const useLocalQuery: <T = Record<string, unknown>>(mirror: LocalMirror, sql: string, params?: ReadonlyArray<unknown>, _options?: UseLocalQueryOptions) => LocalQueryResult<T>;
94
+ export { type LocalQueryResult, type UseLocalQueryOptions, useLocalQuery };
package/dist/react.d.ts CHANGED
@@ -1,5 +1,17 @@
1
- import { L as LocalMirror } from "./packem_shared/local-mirror.d-CyGOpUES.js";
1
+ import { L as LocalMirror } from "./packem_shared/local-mirror.d-ByrYTq4z.js";
2
2
  import "./packem_shared/types.d-CkMkSwLJ.js";
3
+ /**
4
+ * Result of {@link useLocalQuery} — a discriminated union so callers get a
5
+ * typed error instead of a swallowed `undefined`.
6
+ * @experimental
7
+ */
8
+ type LocalQueryResult<T> = {
9
+ readonly data: T[];
10
+ readonly error?: undefined;
11
+ } | {
12
+ readonly data?: undefined;
13
+ readonly error: Error;
14
+ };
3
15
  /**
4
16
  * Options for the {@link useLocalQuery} hook.
5
17
  * @experimental
@@ -19,8 +31,21 @@ interface UseLocalQueryOptions {
19
31
  * React hook that subscribes to a local SQLite query and returns
20
32
  * live-updating results whenever the mirror applies a diff.
21
33
  *
22
- * Uses `useSyncExternalStore` to subscribe to the mirror's `onChange`
23
- * callback every diff triggers a re-query against the local SQLite.
34
+ * Uses `useSyncExternalStore` with `getSnapshot` reading {@link LocalMirror.version}
35
+ * — a plain number, unconditionally `Object.is`-stable across calls with no
36
+ * mutation in between. The actual query runs in a `useMemo` keyed on
37
+ * `[mirror, version, sql, stableParamsKey(params)]`, so it only re-executes
38
+ * when the mirror actually advances (or `sql`/`params` change) — not on
39
+ * every unrelated re-render.
40
+ *
41
+ * This intentionally does NOT cache query results inside {@link LocalMirror}
42
+ * itself: an LRU-capped cache in the mirror core evicts still-mounted hooks'
43
+ * entries once more than the cap's worth of distinct live queries are open
44
+ * on one mirror, which makes `getSnapshot` return a fresh (non-identical)
45
+ * object on the next read — `useSyncExternalStore` then force-re-renders,
46
+ * re-inserts, evicts another entry, and so on without bound. Keying the
47
+ * external-store snapshot on the version primitive instead of a cached
48
+ * query result has no such cap, so it can't loop.
24
49
  *
25
50
  * The hook works with React 18+ concurrent features, Suspense, and
26
51
  * server-side rendering. During SSR the same value is returned as on
@@ -34,26 +59,28 @@ interface UseLocalQueryOptions {
34
59
  * placeholders in `sql`.
35
60
  * @param _options Optional configuration (currently unused; reserved for
36
61
  * future features like shard key routing).
37
- * @returns An array of result rows typed via the generic parameter `T`, or
38
- * `undefined` if the query fails (e.g. the target table doesn't exist yet
39
- * because no matching diff has been applied). Treat `undefined` as a
40
- * "loading" or "no data yet" signal in your component.
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.
62
+ * @returns `{ data }` with the result rows typed via the generic parameter
63
+ * `T`, or `{ error }` when the query fails (e.g. malformed SQL, or the
64
+ * target table doesn't exist yet because no matching diff has been applied
65
+ * that specific case surfaces as a "no such table" `Error`). Never
66
+ * collapses a failure to `undefined` — check `error` explicitly rather than
67
+ * treating a missing `data` as "still loading".
45
68
  * @example
46
69
  * ```tsx
47
70
  * import { useLocalQuery } from "@lunora/replica/react";
48
71
  * import { mirror } from "./mirror";
49
72
  *
50
73
  * function UserList() {
51
- * const users = useLocalQuery<{ id: string; name: string }>(
74
+ * const { data: users, error } = useLocalQuery<{ id: string; name: string }>(
52
75
  * mirror,
53
76
  * "SELECT id, name FROM fn_todos_list WHERE name LIKE ?",
54
77
  * ["%alice%"],
55
78
  * );
56
79
  *
80
+ * if (error) {
81
+ * return <p>Query failed: {error.message}</p>;
82
+ * }
83
+ *
57
84
  * if (users === undefined) {
58
85
  * return <p>Waiting for data…</p>;
59
86
  * }
@@ -63,5 +90,5 @@ interface UseLocalQueryOptions {
63
90
  * ```
64
91
  * @experimental
65
92
  */
66
- declare const useLocalQuery: <T = Record<string, unknown>>(mirror: LocalMirror, sql: string, params?: ReadonlyArray<unknown>, _options?: UseLocalQueryOptions) => T[] | undefined;
67
- export { UseLocalQueryOptions, useLocalQuery };
93
+ declare const useLocalQuery: <T = Record<string, unknown>>(mirror: LocalMirror, sql: string, params?: ReadonlyArray<unknown>, _options?: UseLocalQueryOptions) => LocalQueryResult<T>;
94
+ export { type LocalQueryResult, type UseLocalQueryOptions, useLocalQuery };
package/dist/react.mjs CHANGED
@@ -1,15 +1 @@
1
- import { useSyncExternalStore } from 'react';
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 i,useMemo as y}from"react";const u=t=>JSON.stringify(t??[],(r,n)=>typeof n=="bigint"?`${n.toString()}n`:n),p=(t,r,n,g)=>{const s=e=>t.onChange(e),o=()=>t.version,c=i(s,o,o),a=u(n);return y(()=>{try{return{data:t.query(r,n)}}catch(e){return{error:e instanceof Error?e:new Error(String(e))}}},[t,c,r,a])};export{p as useLocalQuery};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/replica",
3
- "version": "1.0.0-alpha.7",
3
+ "version": "1.0.0-alpha.71",
4
4
  "description": "Local-first replica runtime + local SQLite mirror for Lunora",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -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 };