@lunora/replica 1.0.0-alpha.2 → 1.0.0-alpha.21
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 +2 -0
- package/dist/adapters/better-sqlite3.d.mts +14 -13
- package/dist/adapters/better-sqlite3.d.ts +14 -13
- package/dist/adapters/better-sqlite3.mjs +1 -29
- package/dist/adapters/sqlite-wasm.d.mts +29 -18
- package/dist/adapters/sqlite-wasm.d.ts +29 -18
- package/dist/adapters/sqlite-wasm.mjs +1 -56
- package/dist/adapters/sqljs.d.mts +10 -9
- package/dist/adapters/sqljs.d.ts +10 -9
- package/dist/adapters/sqljs.mjs +1 -55
- package/dist/index.d.mts +646 -531
- package/dist/index.d.ts +646 -531
- package/dist/index.mjs +1 -20
- package/dist/packem_shared/EventEmitter-ovTsLeAj.mjs +1 -0
- package/dist/packem_shared/EventLog-DmlRY_4Z.mjs +1 -0
- package/dist/packem_shared/EventLogDO-Cb-7iN9w.mjs +1 -0
- package/dist/packem_shared/EventLogDOClient-C6gA3b3u.mjs +1 -0
- package/dist/packem_shared/EventSource-B5UTlkl9.mjs +1 -0
- package/dist/packem_shared/EventsSync-DWyGGZQZ.mjs +1 -0
- package/dist/packem_shared/InMemorySnapshotStore-C4taIG5K.mjs +1 -0
- package/dist/packem_shared/LocalMirror-Q7cPH_rX.mjs +4 -0
- package/dist/packem_shared/MaterializerRuntime-CqGGjSxl.mjs +1 -0
- package/dist/packem_shared/SubscriptionManager-CbSjG_GA.mjs +1 -0
- package/dist/packem_shared/applyDiff-BUzddc6r.mjs +1 -0
- package/dist/packem_shared/applyDiffToDb-1M2I3BHi.mjs +1 -0
- package/dist/packem_shared/classifyChanges-IoOGDVVx.mjs +1 -0
- package/dist/packem_shared/defineEvents-CJZV8Bgi.mjs +1 -0
- package/dist/packem_shared/eventsContext-Dxow9Y7S.mjs +1 -0
- package/dist/packem_shared/isClientSeq-DSXBJskD.mjs +1 -0
- package/dist/packem_shared/local-mirror.d-CyGOpUES.d.ts +530 -0
- package/dist/packem_shared/local-mirror.d-DTavX_y0.d.mts +530 -0
- package/dist/packem_shared/subscribeToMirror-Dru_AYBu.mjs +1 -0
- package/dist/packem_shared/{types.d-VfJ76cK4.d.mts → types.d-CkMkSwLJ.d.mts} +10 -9
- package/dist/packem_shared/{types.d-VfJ76cK4.d.ts → types.d-CkMkSwLJ.d.ts} +10 -9
- package/dist/react.d.mts +59 -57
- package/dist/react.d.ts +59 -57
- package/dist/react.mjs +1 -15
- package/package.json +2 -2
- package/dist/packem_shared/EventEmitter-CMZfct03.mjs +0 -92
- package/dist/packem_shared/EventLog-zMy7AYP4.mjs +0 -162
- package/dist/packem_shared/EventLogDO-CZYUvvSr.mjs +0 -235
- package/dist/packem_shared/EventLogDOClient-DGiEdi96.mjs +0 -86
- package/dist/packem_shared/EventSource-DfV4VoRD.mjs +0 -195
- package/dist/packem_shared/EventsSync-DkVbU0WV.mjs +0 -91
- package/dist/packem_shared/InMemorySnapshotStore-BHVAD-Bp.mjs +0 -24
- package/dist/packem_shared/LocalMirror-GeJ26eNe.mjs +0 -188
- package/dist/packem_shared/MaterializerRuntime-HqNXqJxp.mjs +0 -204
- package/dist/packem_shared/SubscriptionManager-C5xbw0pg.mjs +0 -75
- package/dist/packem_shared/applyDiff-BtbIl1D3.mjs +0 -40
- package/dist/packem_shared/applyDiffToDb-DQ1xZp5J.mjs +0 -58
- package/dist/packem_shared/classifyChanges-aZmkxgVI.mjs +0 -38
- 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/local-mirror.d-Bp19ueGy.d.mts +0 -412
- package/dist/packem_shared/local-mirror.d-GhuZAKgm.d.ts +0 -412
- package/dist/packem_shared/subscribeToMirror-CiaM-nQ7.mjs +0 -45
package/dist/index.mjs
CHANGED
|
@@ -1,20 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { createSqliteWasmAdapter } from './adapters/sqlite-wasm.mjs';
|
|
3
|
-
export { createSqlJsAdapter } from './adapters/sqljs.mjs';
|
|
4
|
-
export { applyDiff, applyDiffToSnapshot, applyDiffs } from './packem_shared/applyDiff-BtbIl1D3.mjs';
|
|
5
|
-
export { defineEvents } from './packem_shared/defineEvents-DiBkPTh_.mjs';
|
|
6
|
-
export { MaterializerRuntime, defineMaterializer } from './packem_shared/MaterializerRuntime-HqNXqJxp.mjs';
|
|
7
|
-
export { applyDiffToDb, applyDiffsToDb } from './packem_shared/applyDiffToDb-DQ1xZp5J.mjs';
|
|
8
|
-
export { EventEmitter } from './packem_shared/EventEmitter-CMZfct03.mjs';
|
|
9
|
-
export { EventLog } from './packem_shared/EventLog-zMy7AYP4.mjs';
|
|
10
|
-
export { EventLogDO } from './packem_shared/EventLogDO-CZYUvvSr.mjs';
|
|
11
|
-
export { EventLogDOClient } from './packem_shared/EventLogDOClient-DGiEdi96.mjs';
|
|
12
|
-
export { EventSource } from './packem_shared/EventSource-DfV4VoRD.mjs';
|
|
13
|
-
export { eventsContext } from './packem_shared/eventsContext-Bk_p48hj.mjs';
|
|
14
|
-
export { LocalMirror } from './packem_shared/LocalMirror-GeJ26eNe.mjs';
|
|
15
|
-
export { isClientSeq, isGlobalSeq, isInputEvent } from './packem_shared/isClientSeq-C46BkzqJ.mjs';
|
|
16
|
-
export { InMemorySnapshotStore } from './packem_shared/InMemorySnapshotStore-BHVAD-Bp.mjs';
|
|
17
|
-
export { subscribeToMirror } from './packem_shared/subscribeToMirror-CiaM-nQ7.mjs';
|
|
18
|
-
export { SubscriptionManager } from './packem_shared/SubscriptionManager-C5xbw0pg.mjs';
|
|
19
|
-
export { EventsSync } from './packem_shared/EventsSync-DkVbU0WV.mjs';
|
|
20
|
-
export { classifyChanges, createTableDiff, diffSize, isDiffEmpty, mergeDiffs } from './packem_shared/classifyChanges-aZmkxgVI.mjs';
|
|
1
|
+
import{createBetterSqlite3Adapter as o}from"./adapters/better-sqlite3.mjs";import{createSqliteWasmAdapter as f}from"./adapters/sqlite-wasm.mjs";import{createSqlJsAdapter as i}from"./adapters/sqljs.mjs";import{applyDiff as m,applyDiffToSnapshot as n,applyDiffs as x}from"./packem_shared/applyDiff-BUzddc6r.mjs";import{defineEvents as l}from"./packem_shared/defineEvents-CJZV8Bgi.mjs";import{MaterializerRuntime as S,defineMaterializer as E}from"./packem_shared/MaterializerRuntime-CqGGjSxl.mjs";import{applyDiffToDb as v,applyDiffsToDb as y}from"./packem_shared/applyDiffToDb-1M2I3BHi.mjs";import{EventEmitter as d}from"./packem_shared/EventEmitter-ovTsLeAj.mjs";import{EventLog as M}from"./packem_shared/EventLog-DmlRY_4Z.mjs";import{EventLogDO as u}from"./packem_shared/EventLogDO-Cb-7iN9w.mjs";import{EventLogDOClient as T}from"./packem_shared/EventLogDOClient-C6gA3b3u.mjs";import{EventSource as C,UNHANDLED as h}from"./packem_shared/EventSource-B5UTlkl9.mjs";import{eventsContext as I}from"./packem_shared/eventsContext-Dxow9Y7S.mjs";import{LocalMirror as O}from"./packem_shared/LocalMirror-Q7cPH_rX.mjs";import{isClientSeq as G,isGlobalSeq as H,isInputEvent as J}from"./packem_shared/isClientSeq-DSXBJskD.mjs";import{InMemorySnapshotStore as U}from"./packem_shared/InMemorySnapshotStore-C4taIG5K.mjs";import{subscribeToMirror as j}from"./packem_shared/subscribeToMirror-Dru_AYBu.mjs";import{SubscriptionManager as w}from"./packem_shared/SubscriptionManager-CbSjG_GA.mjs";import{EventsSync as K}from"./packem_shared/EventsSync-DWyGGZQZ.mjs";import{classifyChanges as Q,createTableDiff as V,diffSize as X,isDiffEmpty as Y,mergeDiffs as Z}from"./packem_shared/classifyChanges-IoOGDVVx.mjs";export{d as EventEmitter,M as EventLog,u as EventLogDO,T as EventLogDOClient,C as EventSource,K as EventsSync,U as InMemorySnapshotStore,O as LocalMirror,S as MaterializerRuntime,w as SubscriptionManager,h as UNHANDLED,m as applyDiff,v as applyDiffToDb,n as applyDiffToSnapshot,x as applyDiffs,y as applyDiffsToDb,Q as classifyChanges,o as createBetterSqlite3Adapter,i as createSqlJsAdapter,f as createSqliteWasmAdapter,V as createTableDiff,l as defineEvents,E as defineMaterializer,X as diffSize,I as eventsContext,G as isClientSeq,Y as isDiffEmpty,H as isGlobalSeq,J as isInputEvent,Z as mergeDiffs,j as subscribeToMirror};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class n{#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{n as EventEmitter};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class c{#s=[];#t=0;#i=0;#e=null;#n;constructor(t){this.#n=c.#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}}#h(){return this.#s.slice(this.#t)}#r(){if(this.#n===void 0)return;const t=this.#s.length-this.#t-this.#n;t>0&&(this.#t+=t),this.#t>0&&this.#t>=this.#s.length-this.#t&&(this.#s=this.#s.slice(this.#t),this.#t=0)}append(t,e,s,n){const h=typeof t=="string"?t:t.type,o=typeof t=="string"?e:t.payload;let r,i;typeof t=="string"?(r=s,i=n):(r=void 0,i=e);const a=i?.parentSeqNum??this.#e??void 0,p=this.#i;this.#i+=1;const l={seq:p,type:h,payload:o,timestamp:i?.timestamp??Date.now(),tableDiffs:r,clientId:i?.clientId,sessionId:i?.sessionId,parentSeqNum:a};return this.#s.push(l),this.#e=l.seq,this.#r(),l}commitAll(t){if(t.length===0)return[];const e=[];for(const s of t){const{type:n}=s,h="payload"in s?s.payload:void 0,o="timestamp"in s?s.timestamp:Date.now(),r=e.at(-1)?.seq??this.#e??void 0,i=this.#i;this.#i+=1;const a={seq:i,type:n,payload:h,timestamp:o,parentSeqNum:r};this.#s.push(a),e.push(a)}return this.#e=e.at(-1)?.seq??this.#e,this.#r(),e}load(t){this.#s=[...t.entries],this.#t=0,this.#i=t.nextSeq,this.#e=t.headSeq,this.#r()}getSince(t){const e=this.#h();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.#s,n=this.#t,h=s.findIndex((r,i)=>i>=n&&r.seq>=t);if(h===-1)return{entries:[],hasMore:!1};const o=Math.min(h+e,s.length);return{entries:s.slice(h,o),hasMore:o<s.length}}snapshot(){return{entries:this.#h(),nextSeq:this.#i,headSeq:this.#e}}get size(){return this.#s.length-this.#t}get nextSeq(){return this.#i}get isEmpty(){return this.size===0}get headSeq(){return this.#e}clear(){this.#s=[],this.#t=0,this.#i=0,this.#e=null}truncateBelow(t){if(!Number.isSafeInteger(t)||t<0)throw new RangeError("floorSeq must be a non-negative safe integer");const e=this.#h(),s=e.findIndex(n=>n.seq>=t);this.#s=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{c as EventLog};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const E=Symbol("lunora.replica.event-log-do.idempotency-conflict"),f=i=>{const e=new Error(i);return Object.defineProperty(e,E,{value:!0}),e},h=i=>i instanceof Error&&E in i,y=i=>{if(Array.isArray(i))return i.map(e=>y(e));if(i!==null&&typeof i=="object"){const e=i,t=Object.keys(e);t.sort();const s={};for(const n of t)s[n]=y(e[n]);return s}return i},q=async i=>{const e=JSON.stringify(y(i)),t=new TextEncoder().encode(e),s=await crypto.subtle.digest("SHA-256",t);return[...new Uint8Array(s)].map(n=>n.toString(16).padStart(2,"0")).join("")},m=i=>typeof i.toArray=="function"?i.toArray():typeof i[Symbol.iterator]=="function"?[...i]:[],l=i=>m(i).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 c{state;env;#e=!1;constructor(e,t){this.state=e,this.env=t}async fetch(e){this.#c();const t=new URL(e.url);try{if(e.method==="POST"&&t.pathname==="/append")return await this.#t(e);if(e.method==="GET"&&t.pathname==="/since")return this.#r(t);if(e.method==="GET"&&t.pathname==="/range")return this.#a(t);if(e.method==="GET"&&t.pathname==="/size")return this.#o();if(e.method==="GET"&&t.pathname==="/state")return this.#p()}catch(s){return console.error("[event-log-do] request failed:",s),Response.json({error:{code:"INTERNAL_ERROR",message:"internal error"}},{status:500,headers:{"content-type":"application/json"}})}return Response.json({error:{code:"NOT_FOUND",message:"unknown route"}},{status:404,headers:{"content-type":"application/json"}})}async#t(e){let t;try{t=await e.json()}catch{return Response.json({error:{code:"BAD_REQUEST",message:"invalid JSON body"}},{status:400,headers:{"content-type":"application/json"}})}const s=c.#n(t);if(s)return Response.json({error:{code:"BAD_REQUEST",message:s}},{status:400,headers:{"content-type":"application/json"}});const{sql:n}=this.state.storage,{batchId:a}=t,p=()=>c.#s(n,t,a),{transaction:r}=this.state.storage;let o;try{o=typeof r=="function"?await r(p):await p()}catch(d){if(h(d))return Response.json({error:{code:"CONFLICT",message:d.message}},{status:409,headers:{"content-type":"application/json"}});throw d}const u={entries:o};return Response.json(u,{status:200,headers:{"content-type":"application/json"}})}static async#s(e,t,s){let n;if(typeof s=="string"){n=await q(t.events);const r=c.#d(e,s);if(r){if(r.fingerprint!==n)throw f(`batchId "${s}" was already used for a different event batch`);return r.entries}}const a=Date.now(),p=[];for(const r of t.events){const o={seq:c.#l(e),type:r.type,payload:r.payload,timestamp:r.timestamp??a,clientId:r.clientId,sessionId:r.sessionId,parentSeqNum:r.parentSeqNum};c.#m(e,o),p.push(o)}if(typeof s=="string"&&n!==void 0){const r=p[0]?.seq,o=p.at(-1)?.seq;r!==void 0&&o!==void 0&&c.#u(e,s,r,o,n)}return p}static#n(e){if(!Array.isArray(e.events)||e.events.length===0)return"events[] with a non-empty string `type` required";if(e.batchId!==void 0&&(typeof e.batchId!="string"||e.batchId.length===0))return"batchId must be a non-empty string";for(const t of e.events){const s=c.#i(t);if(s!==void 0)return s}}static#i(e){if(typeof e.type!="string"||e.type.length===0)return"events[] with a non-empty string `type` required";if(e.timestamp!==void 0&&!Number.isFinite(e.timestamp))return"events[].timestamp must be a finite number";if(e.clientId!==void 0&&typeof e.clientId!="string")return"events[].clientId must be a string";if(e.sessionId!==void 0&&typeof e.sessionId!="string")return"events[].sessionId must be a string";if(e.parentSeqNum!==void 0&&(typeof e.parentSeqNum!="number"||!Number.isInteger(e.parentSeqNum)||e.parentSeqNum<0))return"events[].parentSeqNum must be a non-negative integer"}#r(e){const t=e.searchParams.get("seq"),s=t===null?0:Number(t);if(!Number.isFinite(s)||s<0)return Response.json({error:{code:"BAD_REQUEST",message:"invalid seq"}},{status:400,headers:{"content-type":"application/json"}});const{sql:n}=this.state.storage,a=n.exec("SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events WHERE seq >= ? ORDER BY seq ASC",s),p=l(a);return Response.json({entries:p},{status:200,headers:{"content-type":"application/json"}})}#a(e){const t=e.searchParams.get("from"),s=t===null?0:Number(t),n=e.searchParams.get("limit"),a=n===null?50:Number(n);if(!Number.isFinite(s)||s<0||!Number.isFinite(a)||a<1||a>1e3)return Response.json({error:{code:"BAD_REQUEST",message:"invalid from/limit"}},{status:400,headers:{"content-type":"application/json"}});const{sql:p}=this.state.storage,r=p.exec("SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events WHERE seq >= ? ORDER BY seq ASC LIMIT ?",s,a+1),o=l(r),u=o.length>a,d={entries:u?o.slice(0,a):o,hasMore:u};return Response.json(d,{status:200,headers:{"content-type":"application/json"}})}#o(){const{sql:e}=this.state.storage,t=e.exec("SELECT COUNT(*) AS count FROM events"),s=m(t)[0]?.count??0;return Response.json({count:s},{status:200,headers:{"content-type":"application/json"}})}#p(){const{sql:e}=this.state.storage,t=e.exec("SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events ORDER BY seq ASC"),s=l(t),n=(s.at(-1)?.seq??-1)+1;return Response.json({entries:s,nextSeq:n},{status:200,headers:{"content-type":"application/json"}})}#c(){if(this.#e)return;const{sql:e}=this.state.storage;e.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)"),e.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.#e=!0}static#d(e,t){const s=e.exec("SELECT first_seq, last_seq, fingerprint FROM event_batches WHERE batch_id = ?",t),n=m(s)[0];if(!n)return;const a=e.exec("SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events WHERE seq >= ? AND seq <= ? ORDER BY seq ASC",n.first_seq,n.last_seq);return{entries:l(a),fingerprint:n.fingerprint}}static#u(e,t,s,n,a){e.exec("INSERT INTO event_batches (batch_id, first_seq, last_seq, fingerprint) VALUES (?, ?, ?, ?)",t,s,n,a)}static#l(e){const t=e.exec("SELECT COALESCE(MAX(seq), -1) + 1 AS next_seq FROM events");return m(t)[0]?.next_seq??0}static#m(e,t){let s=null;typeof t.parentSeqNum=="number"&&(s=t.parentSeqNum),e.exec("INSERT INTO events (seq, type, payload, timestamp, client_id, session_id, parent_seq) VALUES (?, ?, ?, ?, ?, ?, ?)",t.seq,t.type,JSON.stringify(t.payload),t.timestamp,t.clientId??null,t.sessionId??null,s)}}export{c as EventLogDO};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class s{#t;constructor(t){this.#t=t.fetch}async append(t,e){const a=JSON.stringify({events:t,batchId:e?.batchId}),n=await this.#t(new Request("https://do/append",{method:"POST",headers:{"content-type":"application/json"},body:a}));if(!n.ok)throw await s.#e(n,"append");return(await n.json()).entries}async getSince(t){const e=await this.#t(new Request(`https://do/since?seq=${String(t)}`));if(!e.ok)throw await s.#e(e,"getSince");return(await e.json()).entries}async getRange(t,e=50){const a=await this.#t(new Request(`https://do/range?from=${String(t)}&limit=${String(e)}`));if(!a.ok)throw await s.#e(a,"getRange");return await a.json()}async getSize(){const t=await this.#t(new Request("https://do/size"));if(!t.ok)throw await s.#e(t,"getSize");return(await t.json()).count}async getState(){const t=await this.#t(new Request("https://do/state"));if(!t.ok)throw await s.#e(t,"getState");return await t.json()}static async#e(t,e){try{const a=(await t.json()).error?.message??t.statusText;return new Error(`EventLogDO.${e} failed (${String(t.status)}): ${a}`)}catch{return new Error(`EventLogDO.${e} failed (${String(t.status)}): ${t.statusText}`)}}}export{s as EventLogDOClient};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{EventEmitter as g}from"./EventEmitter-ovTsLeAj.mjs";import{EventLog as y}from"./EventLog-DmlRY_4Z.mjs";const c=Symbol("lunora.replica.event-source.unhandled");class E{emitter=new g;log;#t;#e;#n=!1;#s;#r=-1;constructor(t,e,n){this.#t={...t},this.#e=e,this.#s=n?.unknownEventHandling??"warn",this.log=new y({maxEntries:n?.maxLogEntries})}get state(){return this.#t}get replayed(){return this.#n}applyEvent(t,e,n){let r,i,s;typeof t=="string"?(r=t,i=e,s=n):(r=t.type,i=t.payload,s=e);const o={seq:this.log.nextSeq,type:r,payload:i,timestamp:Date.now(),clientId:s?.clientId,sessionId:s?.sessionId,parentSeqNum:s?.parentSeqNum??this.log.headSeq??void 0};let a;try{a=this.#e(this.#t,o)}catch(l){const p=l instanceof Error?l:new Error(String(l));throw this.emitter.emit("replay-error",{entry:o,error:p}),p}let d=!0;a===c&&(d=this.#i(o));const h=this.log.append(r,i,void 0,{...s,timestamp:o.timestamp});return a===c?(d&&this.emitter.emit("state-changed",{state:this.#t,entry:h}),h):(this.#t=a,this.emitter.emit("state-changed",{state:this.#t,entry:h}),h)}replayFromLog(t){const e=t.getSince(this.#r+1);for(const n of e)try{const r=this.#e(this.#t,n);r!==c&&(this.#t=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(t,e=-1){this.#t={...t},this.#r=e,this.#n=e>=0}async*events(t){const e=[];let n;const r=this.emitter.on("state-changed",({entry:i})=>{e.push(i),n?.()});try{let i=0;for(;i<this.log.size;){if(t?.aborted)return;for(const s of this.log.getSince(i))yield s;i=this.log.nextSeq}for(;!t?.aborted;){for(;e.length>0;){const s=e.shift();s&&(yield s)}e.length===0&&await new Promise(s=>{n=s})}}finally{r()}}#i(t){const e=this.#s;if(typeof e=="function")return e(t);switch(e){case"ignore":return!1;case"fail":throw new Error(`EventSource: unhandled event type "${t.type}" (seq ${String(t.seq)}). Configure \`unknownEventHandling\` to handle this event or change the strategy.`);default:return console.warn(`[EventSource] unhandled event type "${t.type}" (seq ${String(t.seq)}). The event was skipped. Configure \`unknownEventHandling\` if this is expected.`),!1}}}export{E as EventSource,c 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 c}from"../adapters/sqljs.mjs";import{applyDiffToDb as h,escapeIdentifier as a}from"./applyDiffToDb-1M2I3BHi.mjs";import{EventLog as E}from"./EventLog-DmlRY_4Z.mjs";const l="__lunora_mirror_meta",T=o=>{o.exec(`CREATE TABLE IF NOT EXISTS ${l} (
|
|
2
|
+
key TEXT PRIMARY KEY NOT NULL,
|
|
3
|
+
value TEXT NOT NULL
|
|
4
|
+
)`)};class n{#t;#e;#s;#r=new Set;#i=0;static create(t,e){const s=c(t);return new n({db:s,tables:e?.tables})}constructor(t){this.#t=t.db,this.#e={...t.tables},this.#s=new E({maxEntries:t.maxEventLogEntries}),T(this.#t)}onChange(t){return this.#r.add(t),()=>{this.#r.delete(t)}}get eventLog(){return this.#s}get db(){return this.#t}get version(){return this.#i}applyDiff(t){if(t.changes.length===0)return;const e=this.#e[t.table]?.primaryKey??"id";this.#n(t),h(this.#t,t,e),this.#s.append("table-diff",t,[t]),this.#i+=1;for(const s of this.#r)try{s()}catch{}}query(t,e){return this.#t.query(t,e)}clearData(){const t=this.#t.query(String.raw`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '\_\_lunora\_%' ESCAPE '\' AND name NOT LIKE 'sqlite\_%' ESCAPE '\'`);this.#t.transaction(()=>{for(const{name:e}of t)this.#t.exec(`DELETE FROM ${a(e)}`)}),this.#i+=1;for(const e of this.#r)try{e()}catch{}}close(){this.#t.close(),this.#s.clear()}registerTable(t,e){this.#e[t]=e}get mirroredTables(){return Object.keys(this.#e)}static#a(t,e){const s=new Set;for(const i of t.changes)if(i.type!=="delete")for(const r of Object.keys(i.data))r!==e&&s.add(r);return s}#n(t){const e=this.#e[t.table]?.primaryKey??"id",s=n.#a(t,e);if(this.#t.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?",[t.table]).length===0){let i=`${a(e)} TEXT PRIMARY KEY NOT NULL`;for(const r of s)i+=`, ${a(r)} TEXT`;this.#t.exec(`CREATE TABLE IF NOT EXISTS ${a(t.table)} (${i})`)}else if(s.size>0){const i=new Set(this.#t.query(`PRAGMA table_info(${a(t.table)})`).map(r=>r.name));for(const r of s)i.has(r)||this.#t.exec(`ALTER TABLE ${a(t.table)} ADD COLUMN ${a(r)} TEXT`)}}}export{n 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 f{#e;#n;#i;#s;#t;constructor(e,t={}){this.#e=[...e],this.#t=this.#e.map(()=>0),this.#n=t.snapshotStore,this.#i=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,h]of this.#e.entries()){const p=this.#t[o]??0;if(i.seq<p)continue;const l=h.state;h.apply(i),s=!0,h.state!==l&&(n=!0),r.push(o)}if(s){n||this.#r(i);for(const o of r)this.#t[o]=i.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,i]of this.#e.entries()){const s=await this.#n.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.#n)for(const[e,t]of this.#e.entries())await this.#n.save(t.def.name,{appliedSeq:this.#t[e]??0,state:t.state})}async initialize(){if(!this.#i)return 0;await this.recoverFromSnapshots();const e=this.#t.length>0?Math.min(...this.#t):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 t=(await this.#i.append([e]))[0];if(!t)throw new Error("MaterializerRuntime.appendEvent: DO returned empty result");return this.applyEntries([t]),t}reset(){for(const[e,t]of this.#e.entries())this.#t[e]=0,t.reset()}get materializers(){return this.#e}}export{f as MaterializerRuntime,c as defineMaterializer};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class a{#t=new Map;#e=0;onStateChange(e){const t=String(this.#e);this.#e+=1;const n={kind:"state",id:t,callback:e};return this.#t.set(t,n),()=>{this.#t.delete(t)}}onEvent(e,t){const n=String(this.#e);this.#e+=1;const s={kind:"event",id:n,eventType:e,callback:t};return this.#t.set(n,s),()=>{this.#t.delete(n)}}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{a as SubscriptionManager};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const i=t=>{if(Array.isArray(t))return t.map(e=>i(e));if(t!==null&&typeof t=="object"){const e=t,r=Object.keys(e);r.sort();const a={};for(const s of r)a[s]=i(e[s]);return a}return t},n=t=>t.toString(16).padStart(4,"0"),w=t=>{let e=8997,r=33826,a=40164,s=52210;for(let o=0;o<t.length;o+=1){const c=t.codePointAt(o)??0;e^=c&65535,r^=c>>>16&65535;const f=e*435,u=r*435,y=a*435+e*256,g=s*435+r*256,p=u+(f>>>16),d=y+(p>>>16),b=g+(d>>>16);e=f&65535,r=p&65535,a=d&65535,s=b&65535}return n(s)+n(a)+n(r)+n(e)},S=(t,e,r)=>{const a=t.id??String(t.timestamp),s=`${t.table}::${a}::${String(e)}::${JSON.stringify(i(r))}`;return`row-${w(s)}`},l=(t,e)=>{for(const[r,a]of e.changes.entries())switch(a.type){case"delete":{t.delete(a.id);break}case"insert":{const s=a.data.id,o=typeof s=="string"||typeof s=="number"?String(s):S(e,r,a.data);t.set(o,{...a.data,id:o});break}case"update":{const s=t.get(a.id);s&&t.set(a.id,{...s,...a.data});break}}},h=(t,e)=>{const r=new Map(t);return l(r,e),r},k=(t,e)=>{const r=new Map(t);for(const a of e)l(r,a);return r},M=(t,e)=>{const r=new Map(t),a=r.get(e.table)??new Map;return r.set(e.table,h(a,e)),r};export{h as applyDiff,M as applyDiffToSnapshot,k as applyDiffs,i as canonicalizeForHash,S as deriveInsertId,w as fnv1a64Hex};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const o=e=>`\`${e.replaceAll("`","``")}\``,$=e=>e.map(a=>`${o(a)} = ?`).join(", "),d=e=>`(${e.map(a=>o(a)).join(", ")})`,b=e=>`(${Array.from({length:e}).fill("?").join(", ")})`,E=(e,a,t)=>{if(a.changes.length===0)return;const i=o(a.table),p=o(t);for(const c of a.changes)switch(c.type){case"delete":{e.exec(`DELETE FROM ${i} WHERE ${p} = ?`,[c.id]);break}case"insert":{const{data:s}=c,n=Object.keys(s);if(n.length===0)continue;const l=`INSERT OR REPLACE INTO ${i} ${d(n)} VALUES ${b(n.length)}`,r=n.map(f=>s[f]);e.exec(l,r);break}case"update":{const{data:s}=c,n=Object.keys(s);if(n.length===0)continue;const l=`UPDATE ${i} SET ${$(n)} WHERE ${p} = ?`,r=[...n.map(f=>s[f]),c.id];e.exec(l,r);break}}},g=(e,a,t)=>{e.transaction(()=>{E(e,a,t??"id")})},h=(e,a)=>{a.length!==0&&e.transaction(()=>{for(const t of a)E(e,t,"id")})};export{g as applyDiffToDb,h as applyDiffsToDb,o as escapeIdentifier};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{fnv1a64Hex as s}from"./applyDiff-BUzddc6r.mjs";let o=0;const i=()=>{if(typeof crypto<"u"&&typeof crypto.randomUUID=="function")return crypto.randomUUID();o+=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)}_${o.toString(36)}_${t}`},p=(t,n,a,r)=>({table:t,changes:n,timestamp:a??Date.now(),id:r??i()}),c=t=>t.changes.length===0,g=t=>t.changes.length,m=t=>{const n=[],a=[],r=[];for(const e of t.changes)e.type==="insert"?n.push(e):e.type==="update"?a.push(e):r.push(e);return{inserts:n,updates:a,deletes:r}},l=t=>{if(t.length===0)return null;const n=t[0],a=t[t.length-1],r=`merge:${s(t.map(e=>e.id??String(e.timestamp)).join("|"))}`;return p(n.table,t.flatMap(e=>e.changes),a.timestamp,r)};export{m as classifyChanges,p as createTableDiff,g as diffSize,c as isDiffEmpty,l as mergeDiffs};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const d=(i,o)=>{const e={},n={},f=o?.version?`${o.version}.`:"";for(const[s,p]of Object.entries(i)){const c={};for(const[r]of Object.entries(p)){const t=`${f}${s}.${r}`;n[t]=void 0;const a=Object.assign(y=>({type:t,payload:y,timestamp:Date.now()}),{type:t});c[r]=a}e[s]=c}return e._types=n,e};export{d 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,i=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,i as isInputEvent};
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { S as SqliteAdapter } from "./types.d-CkMkSwLJ.js";
|
|
2
|
+
/**
|
|
3
|
+
* Row-level change kind within a TableDiff.
|
|
4
|
+
*
|
|
5
|
+
* Each change represents one row that was inserted, updated, or deleted on
|
|
6
|
+
* the server since the last sync tick.
|
|
7
|
+
* @experimental
|
|
8
|
+
*/
|
|
9
|
+
type RowChange = {
|
|
10
|
+
data: Record<string, unknown>;
|
|
11
|
+
type: "insert";
|
|
12
|
+
} | {
|
|
13
|
+
data: Record<string, unknown>;
|
|
14
|
+
id: string;
|
|
15
|
+
type: "update";
|
|
16
|
+
} | {
|
|
17
|
+
id: string;
|
|
18
|
+
type: "delete";
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* A scoped, ordered set of row changes for a single table.
|
|
22
|
+
*
|
|
23
|
+
* `TableDiff` is the unit of replication between the server and the local
|
|
24
|
+
* SQLite mirror. The server pushes diffs over the poke protocol; the
|
|
25
|
+
* client applies them via `applyDiff`.
|
|
26
|
+
* @experimental
|
|
27
|
+
*/
|
|
28
|
+
interface TableDiff {
|
|
29
|
+
/** Ordered row changes — insert/update/delete, earliest first. */
|
|
30
|
+
readonly changes: ReadonlyArray<RowChange>;
|
|
31
|
+
/**
|
|
32
|
+
* Optional stable identity for this diff, distinct from `timestamp`
|
|
33
|
+
* (multiple diffs can legitimately share a millisecond, so `timestamp`
|
|
34
|
+
* alone is not a unique diff identity). Used by `deriveInsertId` in
|
|
35
|
+
* `apply-diff.ts` to derive deterministic row ids for id-less inserts:
|
|
36
|
+
* replaying the SAME diff (same `id`) must always mint the SAME id,
|
|
37
|
+
* while two DIFFERENT diffs emitted in the same millisecond must not
|
|
38
|
+
* alias onto the same one. `createTableDiff` auto-generates one when
|
|
39
|
+
* omitted; diffs built as plain object literals (bypassing the helper)
|
|
40
|
+
* simply fall back to `timestamp` for that derivation.
|
|
41
|
+
*/
|
|
42
|
+
readonly id?: string;
|
|
43
|
+
/** Logical table name (matches the schema table name). */
|
|
44
|
+
readonly table: string;
|
|
45
|
+
/** Monotonic server timestamp (ms since epoch) when this diff was emitted. */
|
|
46
|
+
readonly timestamp: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Create a {@link TableDiff} with a snapshot of the current time and a
|
|
50
|
+
* fresh stable `id` (unless one is explicitly provided).
|
|
51
|
+
* @experimental
|
|
52
|
+
*/
|
|
53
|
+
declare const createTableDiff: (table: string, changes: ReadonlyArray<RowChange>, timestamp?: number, id?: string) => TableDiff;
|
|
54
|
+
/**
|
|
55
|
+
* Return `true` when the diff contains no row changes.
|
|
56
|
+
* @experimental
|
|
57
|
+
*/
|
|
58
|
+
declare const isDiffEmpty: (diff: TableDiff) => boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Return the number of rows touched by the diff (inserts + updates + deletes).
|
|
61
|
+
* @experimental
|
|
62
|
+
*/
|
|
63
|
+
declare const diffSize: (diff: TableDiff) => number;
|
|
64
|
+
/**
|
|
65
|
+
* Partition a {@link TableDiff} into three categories for batch processing.
|
|
66
|
+
* @experimental
|
|
67
|
+
*/
|
|
68
|
+
declare const classifyChanges: (diff: TableDiff) => {
|
|
69
|
+
deletes: RowChange[];
|
|
70
|
+
inserts: RowChange[];
|
|
71
|
+
updates: RowChange[];
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Merge several diffs for the same table into one (ordering preserved).
|
|
75
|
+
*
|
|
76
|
+
* Returns `null` when the input list is empty.
|
|
77
|
+
* @experimental
|
|
78
|
+
*/
|
|
79
|
+
declare const mergeDiffs: (diffs: ReadonlyArray<TableDiff>) => TableDiff | null;
|
|
80
|
+
/**
|
|
81
|
+
* Sequence-number types for the event log.
|
|
82
|
+
*
|
|
83
|
+
* Three namespaces match the vocabulary established by event-sourcing:
|
|
84
|
+
*
|
|
85
|
+
* | Namespace | Shape | Producer | Consumer |
|
|
86
|
+
* |-----------|--------------------|---------------------------|------------------------------|
|
|
87
|
+
* | `Global` | `number` | `EventLog` / `EventLogDO` | Materializers, subscribers |
|
|
88
|
+
* | `Input` | No seq (optimistic) | `defineEvents` factories | `EventLog.append` / DO client|
|
|
89
|
+
* | `Client` | `{generation,seq}` | _(future: rebase engine)_ | _(future: rebase-aware code)_|
|
|
90
|
+
* @module
|
|
91
|
+
*/
|
|
92
|
+
/**
|
|
93
|
+
* A server-authoritative (global) sequence number.
|
|
94
|
+
*
|
|
95
|
+
* Monotonically increasing, assigned by `EventLog` (in-memory) or
|
|
96
|
+
* `EventLogDO` (Durable Object). All confirmed log entries carry a
|
|
97
|
+
* `GlobalSeq`.
|
|
98
|
+
* @experimental
|
|
99
|
+
*/
|
|
100
|
+
type GlobalSeq = number;
|
|
101
|
+
/**
|
|
102
|
+
* A client-originated composite sequence number designed to survive rebase.
|
|
103
|
+
*
|
|
104
|
+
* Carries the last-confirmed `global` seq, a monotonically increasing
|
|
105
|
+
* `client` counter, and a `rebaseGeneration` that increments whenever the
|
|
106
|
+
* client's local events are rebased onto a new upstream baseline.
|
|
107
|
+
* @experimental
|
|
108
|
+
*/
|
|
109
|
+
interface ClientSeq {
|
|
110
|
+
/** Client-local monotonically increasing counter. */
|
|
111
|
+
readonly client: number;
|
|
112
|
+
/** The last-confirmed global sequence number. 0 for unconfirmed events. */
|
|
113
|
+
readonly global: number;
|
|
114
|
+
/** Incremented on every rebase. */
|
|
115
|
+
readonly rebaseGeneration: number;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Discriminated union of all sequence-number types.
|
|
119
|
+
* @experimental
|
|
120
|
+
*/
|
|
121
|
+
type Seq = GlobalSeq | ClientSeq;
|
|
122
|
+
/**
|
|
123
|
+
* Narrow `Seq` to `GlobalSeq`.
|
|
124
|
+
* @experimental
|
|
125
|
+
*/
|
|
126
|
+
declare const isGlobalSeq: (seq: Seq) => seq is GlobalSeq;
|
|
127
|
+
/**
|
|
128
|
+
* Narrow `Seq` to `ClientSeq`.
|
|
129
|
+
* @experimental
|
|
130
|
+
*/
|
|
131
|
+
declare const isClientSeq: (seq: Seq) => seq is ClientSeq;
|
|
132
|
+
/**
|
|
133
|
+
* An event that has **not** yet been assigned a sequence number.
|
|
134
|
+
*
|
|
135
|
+
* Input events represent optimistic / command payloads before the server
|
|
136
|
+
* confirms them. They carry `type`, `payload`, and `timestamp` but no
|
|
137
|
+
* `seq` — the log assigns one on append.
|
|
138
|
+
*
|
|
139
|
+
* Create input events via `defineEvents` factories:
|
|
140
|
+
*
|
|
141
|
+
* ```ts
|
|
142
|
+
* const event = events.chat.messageSent({ channelId: "c1", text: "hello" });
|
|
143
|
+
* // event: InputEvent<"chat.messageSent", { channelId: string; text: string }>
|
|
144
|
+
* ```
|
|
145
|
+
* @experimental
|
|
146
|
+
*/
|
|
147
|
+
interface InputEvent<Type extends string = string, Payload = unknown> {
|
|
148
|
+
/** Arbitrary JSON-serialisable payload. */
|
|
149
|
+
readonly payload: Payload;
|
|
150
|
+
/** Millisecond timestamp (epoch) when the event was created. */
|
|
151
|
+
readonly timestamp: number;
|
|
152
|
+
/** Event type discriminator (e.g. `"chat.messageSent"`). */
|
|
153
|
+
readonly type: Type;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Type guard: check whether `value` is an {@link InputEvent}.
|
|
157
|
+
* @experimental
|
|
158
|
+
*/
|
|
159
|
+
declare const isInputEvent: (value: unknown) => value is InputEvent;
|
|
160
|
+
/**
|
|
161
|
+
* A single entry in the append-only {@link EventLog}.
|
|
162
|
+
*
|
|
163
|
+
* Entries are immutable once appended; the `seq` field is assigned
|
|
164
|
+
* monotonically by the log and doubles as a watermark for catch-up
|
|
165
|
+
* replication between tabs or service-worker instances.
|
|
166
|
+
* @experimental
|
|
167
|
+
*/
|
|
168
|
+
interface EventLogEntry {
|
|
169
|
+
/**
|
|
170
|
+
* Globally-unique client identifier that originated this event.
|
|
171
|
+
* Set by clients that support offline / optimistic writes.
|
|
172
|
+
* `undefined` when the event was created server-side.
|
|
173
|
+
*/
|
|
174
|
+
readonly clientId?: string;
|
|
175
|
+
/**
|
|
176
|
+
* The sequence number of the causal parent event.
|
|
177
|
+
*
|
|
178
|
+
* A {@link GlobalSeq} for events confirmed by the server (pointing
|
|
179
|
+
* to the previous confirmed event), or a {@link ClientSeq} for
|
|
180
|
+
* optimistic / offline events pointing to the local predecessor.
|
|
181
|
+
* `undefined` for the first event in a log.
|
|
182
|
+
*/
|
|
183
|
+
readonly parentSeqNum?: Seq;
|
|
184
|
+
/** Arbitrary JSON-serialisable payload. */
|
|
185
|
+
readonly payload: unknown;
|
|
186
|
+
/** Monotonically increasing sequence number (0-based). A {@link GlobalSeq}. */
|
|
187
|
+
readonly seq: GlobalSeq;
|
|
188
|
+
/**
|
|
189
|
+
* Session identifier from the originating client.
|
|
190
|
+
* Paired with `clientId` to disambiguate concurrent sessions.
|
|
191
|
+
*/
|
|
192
|
+
readonly sessionId?: string;
|
|
193
|
+
/**
|
|
194
|
+
* Optional table diffs that this event produced.
|
|
195
|
+
* When present, a consumer can re-play the event by applying the diffs
|
|
196
|
+
* to its local mirror without re-executing the originating mutation.
|
|
197
|
+
*/
|
|
198
|
+
readonly tableDiffs?: ReadonlyArray<TableDiff>;
|
|
199
|
+
/** Millisecond timestamp (epoch) when the entry was appended. */
|
|
200
|
+
readonly timestamp: number;
|
|
201
|
+
/** Event type discriminator (e.g. "row-insert", "mutation-apply"). */
|
|
202
|
+
readonly type: string;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Serialised snapshot of the log — used for persistence and transfer.
|
|
206
|
+
* @experimental
|
|
207
|
+
*/
|
|
208
|
+
interface EventLogSnapshot {
|
|
209
|
+
readonly entries: ReadonlyArray<EventLogEntry>;
|
|
210
|
+
/** The sequence number of the last entry (head), or `null` for an empty log. */
|
|
211
|
+
readonly headSeq: GlobalSeq | null;
|
|
212
|
+
readonly nextSeq: number;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Optional metadata that can accompany an appended event.
|
|
216
|
+
* @experimental
|
|
217
|
+
*/
|
|
218
|
+
interface AppendOptions {
|
|
219
|
+
/** Globally-unique client identifier. */
|
|
220
|
+
readonly clientId?: string;
|
|
221
|
+
/**
|
|
222
|
+
* Causal parent sequence number.
|
|
223
|
+
* Automatically set to the previous entry's seq when omitted.
|
|
224
|
+
*/
|
|
225
|
+
readonly parentSeqNum?: Seq;
|
|
226
|
+
/** Session identifier within the client. */
|
|
227
|
+
readonly sessionId?: string;
|
|
228
|
+
/**
|
|
229
|
+
* Override the entry's `timestamp` instead of stamping `Date.now()` at
|
|
230
|
+
* append time.
|
|
231
|
+
*
|
|
232
|
+
* Used by callers (e.g. {@link import("./event-source").EventSource | EventSource})
|
|
233
|
+
* that must commit the EXACT entry a reducer already observed — a
|
|
234
|
+
* second, independently-drawn `Date.now()` at append time could produce
|
|
235
|
+
* a persisted entry a timestamp-dependent reducer cannot reproduce on
|
|
236
|
+
* replay.
|
|
237
|
+
*/
|
|
238
|
+
readonly timestamp?: number;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Options for constructing an {@link EventLog}.
|
|
242
|
+
* @experimental
|
|
243
|
+
*/
|
|
244
|
+
interface EventLogOptions {
|
|
245
|
+
/**
|
|
246
|
+
* Cap the number of entries retained in memory (REPLICA-06). When an
|
|
247
|
+
* append would exceed the cap, the OLDEST entries are evicted (ring
|
|
248
|
+
* buffer) — a `getSince`/`getFrom` call for a watermark below the oldest
|
|
249
|
+
* retained `seq` then returns only what's left, silently missing
|
|
250
|
+
* anything evicted.
|
|
251
|
+
*
|
|
252
|
+
* `undefined` (the default) preserves the original unbounded behavior.
|
|
253
|
+
* Set this only when you have another durable source of truth for
|
|
254
|
+
* anything older than the cap (a snapshot, a server-side `EventLogDO`) —
|
|
255
|
+
* see {@link EventLog#truncateBelow} for the caller-driven equivalent
|
|
256
|
+
* tied to snapshot persistence.
|
|
257
|
+
*/
|
|
258
|
+
readonly maxEntries?: number;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* An append-only, in-memory event log for local event sourcing.
|
|
262
|
+
*
|
|
263
|
+
* The log is the single source of truth for "what happened" and drives
|
|
264
|
+
* catch-up replication: a new tab or service worker asks for entries
|
|
265
|
+
* since its known `seq` watermark and re-applies them.
|
|
266
|
+
* @remarks This class is intentionally **not** a full SQLite-backed log.
|
|
267
|
+
* Persistence is the caller's responsibility (write the snapshot
|
|
268
|
+
* to IndexedDB / OPFS via {@link EventLog#snapshot}).
|
|
269
|
+
* @experimental
|
|
270
|
+
*/
|
|
271
|
+
declare class EventLog {
|
|
272
|
+
#private;
|
|
273
|
+
constructor(options?: EventLogOptions);
|
|
274
|
+
/**
|
|
275
|
+
* Append a new entry to the log.
|
|
276
|
+
*
|
|
277
|
+
* Accepts either an {@link InputEvent} (e.g. from a `defineEvents` factory)
|
|
278
|
+
* or the traditional `(type, payload, tableDiffs?)` triple.
|
|
279
|
+
* @returns The newly created entry (already written to the log).
|
|
280
|
+
*/
|
|
281
|
+
append(event: InputEvent, options?: AppendOptions): EventLogEntry;
|
|
282
|
+
append(type: string, payload: unknown, tableDiffs?: ReadonlyArray<TableDiff>, options?: AppendOptions): EventLogEntry;
|
|
283
|
+
/**
|
|
284
|
+
* Atomically append multiple events to the log.
|
|
285
|
+
*
|
|
286
|
+
* All events are assigned sequential global sequence numbers and
|
|
287
|
+
* automatically wired as a causal chain (each event's `parentSeqNum`
|
|
288
|
+
* points to the previous event in the batch, or to the log head for
|
|
289
|
+
* the first event).
|
|
290
|
+
* @param events An array of events to commit atomically.
|
|
291
|
+
* @returns The newly created entries in order.
|
|
292
|
+
*/
|
|
293
|
+
commitAll(events: ReadonlyArray<InputEvent | {
|
|
294
|
+
payload: unknown;
|
|
295
|
+
type: string;
|
|
296
|
+
}>): EventLogEntry[];
|
|
297
|
+
/**
|
|
298
|
+
* Replace the log contents with a previously captured snapshot.
|
|
299
|
+
* This is the restore counterpart of {@link EventLog#snapshot}.
|
|
300
|
+
* Restores `headSeq` from the snapshot so auto-parenting continues
|
|
301
|
+
* after restore.
|
|
302
|
+
*
|
|
303
|
+
* Runs `#enforceCap()` after restoring so a snapshot captured under a
|
|
304
|
+
* different (or no) `maxEntries` can never leave this log over its
|
|
305
|
+
* configured capacity.
|
|
306
|
+
*/
|
|
307
|
+
load(snapshot: EventLogSnapshot): void;
|
|
308
|
+
/**
|
|
309
|
+
* Return **all** entries whose `seq >= sinceSeq`.
|
|
310
|
+
* Useful for catch-up: "give me everything since my last watermark".
|
|
311
|
+
*/
|
|
312
|
+
getSince(sinceSeq: number): ReadonlyArray<EventLogEntry>;
|
|
313
|
+
/**
|
|
314
|
+
* Paginated read starting at `fromSeq`.
|
|
315
|
+
* @returns `{ entries, hasMore }` where `hasMore` is `true` when more
|
|
316
|
+
* entries exist beyond the requested page.
|
|
317
|
+
*/
|
|
318
|
+
getFrom(fromSeq: number, limit?: number): {
|
|
319
|
+
entries: ReadonlyArray<EventLogEntry>;
|
|
320
|
+
hasMore: boolean;
|
|
321
|
+
};
|
|
322
|
+
/**
|
|
323
|
+
* Return all entries as a snapshot suitable for serialisation.
|
|
324
|
+
*/
|
|
325
|
+
snapshot(): EventLogSnapshot;
|
|
326
|
+
/** Number of entries currently in the log. */
|
|
327
|
+
get size(): number;
|
|
328
|
+
/** The next sequence number that will be assigned. */
|
|
329
|
+
get nextSeq(): number;
|
|
330
|
+
/** Return `true` when there are no entries. */
|
|
331
|
+
get isEmpty(): boolean;
|
|
332
|
+
/**
|
|
333
|
+
* The sequence number of the last (most recent) entry, or `null`
|
|
334
|
+
* when the log is empty. Used internally for auto-parenting and
|
|
335
|
+
* exposed for consumers that need the causal head.
|
|
336
|
+
*/
|
|
337
|
+
get headSeq(): GlobalSeq | null;
|
|
338
|
+
/** Remove all entries (primarily for testing). */
|
|
339
|
+
clear(): void;
|
|
340
|
+
/**
|
|
341
|
+
* Discard all entries with `seq < floorSeq` (REPLICA-06).
|
|
342
|
+
*
|
|
343
|
+
* `headSeq`/`nextSeq` are untouched (they're independent counters), so
|
|
344
|
+
* appends after a truncation continue the same sequence uninterrupted.
|
|
345
|
+
*
|
|
346
|
+
* **Caller-driven, not automatic**: only call this after the truncated
|
|
347
|
+
* range has already been durably captured elsewhere (a snapshot, a
|
|
348
|
+
* server-side `EventLogDO`) — truncating without such a floor makes any
|
|
349
|
+
* future `getSince`/`getFrom`/`EventSource.replayFromLog` call for a
|
|
350
|
+
* watermark below `floorSeq` silently miss the discarded entries. This is
|
|
351
|
+
* the hook the caller ties to snapshot persistence; the log itself has no
|
|
352
|
+
* concept of "already durably persisted".
|
|
353
|
+
*
|
|
354
|
+
* `floorSeq` must be a non-negative safe integer — `NaN` would make
|
|
355
|
+
* every comparison false and silently clear the entire log.
|
|
356
|
+
*/
|
|
357
|
+
truncateBelow(floorSeq: number): void;
|
|
358
|
+
/**
|
|
359
|
+
* Return an async generator that yields every entry starting from
|
|
360
|
+
* `fromSeq` (default `0` = all entries).
|
|
361
|
+
*
|
|
362
|
+
* Because `EventLog` is purely in-memory, the generator yields all
|
|
363
|
+
* matching entries synchronously on first iteration and then completes.
|
|
364
|
+
* For a streaming / push-based variant see {@link EventSource.events}.
|
|
365
|
+
*/
|
|
366
|
+
events(fromSeq?: number): AsyncGenerator<EventLogEntry>;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* `MirrorTableDef` is part of the experimental `@lunora/replica` API and may change without a major version bump.
|
|
370
|
+
* @experimental
|
|
371
|
+
*/
|
|
372
|
+
interface MirrorTableDef {
|
|
373
|
+
/** Primary key column name (defaults to `"id"`). */
|
|
374
|
+
readonly primaryKey?: string;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Options for constructing a {@link LocalMirror}.
|
|
378
|
+
* @experimental
|
|
379
|
+
*/
|
|
380
|
+
interface LocalMirrorOptions {
|
|
381
|
+
/** Platform-specific SQLite adapter. */
|
|
382
|
+
readonly db: SqliteAdapter;
|
|
383
|
+
/**
|
|
384
|
+
* Cap the mirror's internal {@link EventLog} to this many entries
|
|
385
|
+
* (REPLICA-06). Every applied diff is recorded in the log — with no cap,
|
|
386
|
+
* a long-running client accumulates one entry per diff forever.
|
|
387
|
+
*
|
|
388
|
+
* `undefined` (the default) preserves unbounded retention. Set this when
|
|
389
|
+
* catch-up replication only ever needs a bounded recent window; older
|
|
390
|
+
* entries are silently evicted (oldest-first) once the cap is exceeded.
|
|
391
|
+
* See {@link EventLog#truncateBelow} for caller-driven truncation tied to
|
|
392
|
+
* a snapshot instead.
|
|
393
|
+
*/
|
|
394
|
+
readonly maxEventLogEntries?: number;
|
|
395
|
+
/**
|
|
396
|
+
* Table schemas the mirror should manage.
|
|
397
|
+
*
|
|
398
|
+
* On first use the mirror creates any missing tables automatically
|
|
399
|
+
* based on the columns observed in the first diff/row applied.
|
|
400
|
+
* If you want a fixed schema, pass it here with explicit column
|
|
401
|
+
* definitions in `columns`.
|
|
402
|
+
*/
|
|
403
|
+
readonly tables?: Record<string, MirrorTableDef>;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Local SQLite mirror that maintains a client-side replica of server
|
|
407
|
+
* tables by applying {@link TableDiff} deltas.
|
|
408
|
+
*
|
|
409
|
+
* Usage:
|
|
410
|
+
* ```ts
|
|
411
|
+
* import { createSqlJsAdapter } from "@lunora/replica/adapters/sqljs";
|
|
412
|
+
* import initSqlJs from "sql.js";
|
|
413
|
+
*
|
|
414
|
+
* const SQL = await initSqlJs();
|
|
415
|
+
* const db = createSqlJsAdapter(new SQL.Database());
|
|
416
|
+
*
|
|
417
|
+
* const mirror = new LocalMirror({ db });
|
|
418
|
+
*
|
|
419
|
+
* // Apply a server diff:
|
|
420
|
+
* mirror.applyDiff(someDiff);
|
|
421
|
+
*
|
|
422
|
+
* // Query locally:
|
|
423
|
+
* const rows = mirror.query<{ id: string; name: string }>(
|
|
424
|
+
* "SELECT id, name FROM users WHERE name LIKE ?",
|
|
425
|
+
* ["alice%"],
|
|
426
|
+
* );
|
|
427
|
+
* ```
|
|
428
|
+
*/
|
|
429
|
+
type ChangeSubscriber = () => void;
|
|
430
|
+
/**
|
|
431
|
+
* `LocalMirror` is part of the experimental `@lunora/replica` API and may change without a major version bump.
|
|
432
|
+
* @experimental
|
|
433
|
+
*/
|
|
434
|
+
declare class LocalMirror {
|
|
435
|
+
#private;
|
|
436
|
+
/**
|
|
437
|
+
* Convenience factory that creates a {@link LocalMirror} backed by a
|
|
438
|
+
* {@link createSqlJsAdapter sql.js adapter} without needing to import
|
|
439
|
+
* and wire sql.js manually.
|
|
440
|
+
*
|
|
441
|
+
* The caller provides an initialised sql.js database — this method wraps
|
|
442
|
+
* it in an adapter and constructs the mirror.
|
|
443
|
+
* @example
|
|
444
|
+
* ```ts
|
|
445
|
+
* import initSqlJs from "sql.js";
|
|
446
|
+
*
|
|
447
|
+
* const SQL = await initSqlJs();
|
|
448
|
+
* const mirror = LocalMirror.create(new SQL.Database(), {
|
|
449
|
+
* tables: { todos: { primaryKey: "id" } },
|
|
450
|
+
* });
|
|
451
|
+
* ```
|
|
452
|
+
*/
|
|
453
|
+
static create(sqlJsDatabase: {
|
|
454
|
+
close: () => void;
|
|
455
|
+
exec: (sql: string) => {
|
|
456
|
+
columns: string[];
|
|
457
|
+
values: unknown[][];
|
|
458
|
+
}[];
|
|
459
|
+
run: (sql: string, params?: unknown[]) => void;
|
|
460
|
+
}, options?: {
|
|
461
|
+
tables?: Record<string, MirrorTableDef>;
|
|
462
|
+
}): LocalMirror;
|
|
463
|
+
constructor(options: LocalMirrorOptions);
|
|
464
|
+
/**
|
|
465
|
+
* Subscribe to data-change notifications. Fires after every {@link applyDiff}.
|
|
466
|
+
* Returns an unsubscribe function.
|
|
467
|
+
*/
|
|
468
|
+
onChange(callback: ChangeSubscriber): () => void;
|
|
469
|
+
/**
|
|
470
|
+
* The in-memory event log tracking every diff applied to this mirror.
|
|
471
|
+
* Use {@link EventLog.getSince} for catch-up replication across tabs
|
|
472
|
+
* or service-worker instances.
|
|
473
|
+
*/
|
|
474
|
+
get eventLog(): EventLog;
|
|
475
|
+
/**
|
|
476
|
+
* The raw SQLite adapter. Advanced consumers (e.g. the React hook)
|
|
477
|
+
* can use it for ad-hoc queries or bulk operations.
|
|
478
|
+
*/
|
|
479
|
+
get db(): SqliteAdapter;
|
|
480
|
+
/**
|
|
481
|
+
* Monotonically increasing version counter, bumped on every operation
|
|
482
|
+
* that changes mirrored data (`applyDiff`, `clearData`). Use this — not
|
|
483
|
+
* `eventLog.size` — as a `useSyncExternalStore` snapshot so operations
|
|
484
|
+
* that don't append to the log still trigger a re-render.
|
|
485
|
+
*/
|
|
486
|
+
get version(): number;
|
|
487
|
+
/**
|
|
488
|
+
* Apply a server-side diff to the local SQLite mirror.
|
|
489
|
+
*
|
|
490
|
+
* The diff is applied in a transaction and recorded in the event log
|
|
491
|
+
* so other tabs or the SW can catch up.
|
|
492
|
+
*/
|
|
493
|
+
applyDiff(diff: TableDiff): void;
|
|
494
|
+
/**
|
|
495
|
+
* Run an arbitrary SQL query against the local mirror and return
|
|
496
|
+
* typed results.
|
|
497
|
+
* @example
|
|
498
|
+
* ```ts
|
|
499
|
+
* const users = mirror.query<{ id: string; name: string }>(
|
|
500
|
+
* "SELECT id, name FROM users WHERE active = ?",
|
|
501
|
+
* [true],
|
|
502
|
+
* );
|
|
503
|
+
* ```
|
|
504
|
+
*/
|
|
505
|
+
query<T = Record<string, unknown>>(sql: string, params?: ReadonlyArray<unknown>): T[];
|
|
506
|
+
/**
|
|
507
|
+
* Delete every row from all known tables (preserves the event log
|
|
508
|
+
* and schema). Useful when re-syncing from scratch.
|
|
509
|
+
*
|
|
510
|
+
* Notifies `onChange` subscribers and bumps {@link LocalMirror.version}
|
|
511
|
+
* (REPLICA-09) even though nothing is appended to the event log — a
|
|
512
|
+
* consumer keyed only on `eventLog.size` would otherwise never learn the
|
|
513
|
+
* mirror was cleared and keep rendering deleted rows.
|
|
514
|
+
*/
|
|
515
|
+
clearData(): void;
|
|
516
|
+
/**
|
|
517
|
+
* Dispose the mirror and close the database connection.
|
|
518
|
+
*/
|
|
519
|
+
close(): void;
|
|
520
|
+
/**
|
|
521
|
+
* Register a table schema so the mirror can create the table on
|
|
522
|
+
* first use.
|
|
523
|
+
*/
|
|
524
|
+
registerTable(name: string, definition: MirrorTableDef): void;
|
|
525
|
+
/**
|
|
526
|
+
* Return the list of mirrored table names.
|
|
527
|
+
*/
|
|
528
|
+
get mirroredTables(): ReadonlyArray<string>;
|
|
529
|
+
}
|
|
530
|
+
export { AppendOptions as A, ClientSeq as C, EventLogEntry as E, GlobalSeq as G, InputEvent as I, LocalMirror as L, MirrorTableDef as M, RowChange as R, Seq as S, TableDiff as T, EventLog as a, EventLogOptions as b, EventLogSnapshot as c, LocalMirrorOptions as d, classifyChanges as e, createTableDiff as f, diffSize as g, isDiffEmpty as h, isClientSeq as i, isGlobalSeq as j, isInputEvent as k, mergeDiffs as m };
|