@byearlybird/starling 0.8.1 → 0.9.0

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/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as StoreSnapshot, c as processDocument, i as Store, n as PluginHooks, o as createStore, r as PluginMethods, s as EncodedDocument, t as Plugin } from "./store-BGsRIZeM.js";
2
- export { type EncodedDocument, Plugin, PluginHooks, PluginMethods, Store, StoreSnapshot, createStore, processDocument };
1
+ import { a as StoreAddOptions, i as Store, l as EncodedDocument, n as Query, o as StoreConfig, r as QueryConfig, s as StoreSetTransaction, t as Plugin, u as processDocument } from "./store-Dc-hIF56.js";
2
+ export { type EncodedDocument, Plugin, Query, QueryConfig, Store, StoreAddOptions, StoreConfig, StoreSetTransaction, processDocument };
package/dist/index.js CHANGED
@@ -1,11 +1,21 @@
1
- //#region src/eventstamp.ts
2
- const generateNonce = () => {
1
+ //#region src/crdt/eventstamp.ts
2
+ function generateNonce() {
3
3
  return Math.random().toString(16).slice(2, 6).padStart(4, "0");
4
- };
5
- const encodeEventstamp = (timestampMs, counter, nonce) => {
4
+ }
5
+ function encodeEventstamp(timestampMs, counter, nonce) {
6
6
  return `${new Date(timestampMs).toISOString()}|${counter.toString(16).padStart(4, "0")}|${nonce}`;
7
- };
8
- const decodeEventstamp = (eventstamp) => {
7
+ }
8
+ /**
9
+ * Validates whether a string is a properly formatted eventstamp.
10
+ * Expected format: YYYY-MM-DDTHH:mm:ss.SSSZ|HHHH+|HHHH
11
+ * where HHHH+ represents 4 or more hex characters for the counter,
12
+ * and HHHH represents exactly 4 hex characters for the nonce.
13
+ */
14
+ function isValidEventstamp(stamp) {
15
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\|[0-9a-f]{4,}\|[0-9a-f]{4}$/.test(stamp);
16
+ }
17
+ function decodeEventstamp(eventstamp) {
18
+ if (!isValidEventstamp(eventstamp)) throw new Error(`Invalid eventstamp format: "${eventstamp}". Expected format: YYYY-MM-DDTHH:mm:ss.SSSZ|HHHH+|HHHH`);
9
19
  const parts = eventstamp.split("|");
10
20
  const isoString = parts[0];
11
21
  const hexCounter = parts[1];
@@ -15,26 +25,36 @@ const decodeEventstamp = (eventstamp) => {
15
25
  counter: parseInt(hexCounter, 16),
16
26
  nonce
17
27
  };
18
- };
28
+ }
19
29
  const MIN_EVENTSTAMP = encodeEventstamp(0, 0, "0000");
20
30
 
21
31
  //#endregion
22
- //#region src/utils.ts
23
- const isObject = (value) => !!(value != null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype);
24
- const isEncodedValue = (value) => !!(typeof value === "object" && value !== null && "~value" in value && "~eventstamp" in value);
32
+ //#region src/crdt/utils.ts
33
+ function isObject(value) {
34
+ return !!(value != null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype);
35
+ }
36
+ function isEncodedValue(value) {
37
+ return !!(typeof value === "object" && value !== null && "~value" in value && "~eventstamp" in value);
38
+ }
25
39
 
26
40
  //#endregion
27
- //#region src/value.ts
28
- const encodeValue = (value, eventstamp) => ({
29
- "~value": value,
30
- "~eventstamp": eventstamp
31
- });
32
- const decodeValue = (value) => value["~value"];
33
- const mergeValues = (into, from) => into["~eventstamp"] > from["~eventstamp"] ? [into, into["~eventstamp"]] : [from, from["~eventstamp"]];
41
+ //#region src/crdt/value.ts
42
+ function encodeValue(value, eventstamp) {
43
+ return {
44
+ "~value": value,
45
+ "~eventstamp": eventstamp
46
+ };
47
+ }
48
+ function decodeValue(value) {
49
+ return value["~value"];
50
+ }
51
+ function mergeValues(into, from) {
52
+ return into["~eventstamp"] > from["~eventstamp"] ? [into, into["~eventstamp"]] : [from, from["~eventstamp"]];
53
+ }
34
54
 
35
55
  //#endregion
36
- //#region src/record.ts
37
- const processRecord = (source, process) => {
56
+ //#region src/crdt/record.ts
57
+ function processRecord(source, process) {
38
58
  const result = {};
39
59
  const step = (input, output) => {
40
60
  for (const key in input) {
@@ -49,8 +69,8 @@ const processRecord = (source, process) => {
49
69
  };
50
70
  step(source, result);
51
71
  return result;
52
- };
53
- const encodeRecord = (obj, eventstamp) => {
72
+ }
73
+ function encodeRecord(obj, eventstamp) {
54
74
  const result = {};
55
75
  const step = (input, output) => {
56
76
  for (const key in input) {
@@ -64,8 +84,8 @@ const encodeRecord = (obj, eventstamp) => {
64
84
  };
65
85
  step(obj, result);
66
86
  return result;
67
- };
68
- const decodeRecord = (obj) => {
87
+ }
88
+ function decodeRecord(obj) {
69
89
  const result = {};
70
90
  const step = (input, output) => {
71
91
  for (const key in input) {
@@ -80,8 +100,8 @@ const decodeRecord = (obj) => {
80
100
  };
81
101
  step(obj, result);
82
102
  return result;
83
- };
84
- const mergeRecords = (into, from) => {
103
+ }
104
+ function mergeRecords(into, from) {
85
105
  const result = {};
86
106
  let greatestEventstamp = MIN_EVENTSTAMP;
87
107
  const step = (v1, v2, output) => {
@@ -116,21 +136,25 @@ const mergeRecords = (into, from) => {
116
136
  };
117
137
  step(into, from, result);
118
138
  return [result, greatestEventstamp];
119
- };
139
+ }
120
140
 
121
141
  //#endregion
122
- //#region src/document.ts
123
- const encodeDoc = (id, obj, eventstamp, deletedAt = null) => ({
124
- "~id": id,
125
- "~data": isObject(obj) ? encodeRecord(obj, eventstamp) : encodeValue(obj, eventstamp),
126
- "~deletedAt": deletedAt
127
- });
128
- const decodeDoc = (doc) => ({
129
- "~id": doc["~id"],
130
- "~data": isEncodedValue(doc["~data"]) ? decodeValue(doc["~data"]) : decodeRecord(doc["~data"]),
131
- "~deletedAt": doc["~deletedAt"]
132
- });
133
- const mergeDocs = (into, from) => {
142
+ //#region src/crdt/document.ts
143
+ function encodeDoc(id, obj, eventstamp, deletedAt = null) {
144
+ return {
145
+ "~id": id,
146
+ "~data": isObject(obj) ? encodeRecord(obj, eventstamp) : encodeValue(obj, eventstamp),
147
+ "~deletedAt": deletedAt
148
+ };
149
+ }
150
+ function decodeDoc(doc) {
151
+ return {
152
+ "~id": doc["~id"],
153
+ "~data": isEncodedValue(doc["~data"]) ? decodeValue(doc["~data"]) : decodeRecord(doc["~data"]),
154
+ "~deletedAt": doc["~deletedAt"]
155
+ };
156
+ }
157
+ function mergeDocs(into, from) {
134
158
  const intoIsValue = isEncodedValue(into["~data"]);
135
159
  const fromIsValue = isEncodedValue(from["~data"]);
136
160
  if (intoIsValue !== fromIsValue) throw new Error("Merge error: Incompatible types");
@@ -143,277 +167,354 @@ const mergeDocs = (into, from) => {
143
167
  "~data": mergedData,
144
168
  "~deletedAt": mergedDeletedAt
145
169
  }, greatestEventstamp];
146
- };
147
- const deleteDoc = (doc, eventstamp) => ({
148
- "~id": doc["~id"],
149
- "~data": doc["~data"],
150
- "~deletedAt": eventstamp
151
- });
152
- const processDocument = (doc, process) => {
170
+ }
171
+ function deleteDoc(doc, eventstamp) {
172
+ return {
173
+ "~id": doc["~id"],
174
+ "~data": doc["~data"],
175
+ "~deletedAt": eventstamp
176
+ };
177
+ }
178
+ function processDocument(doc, process) {
153
179
  const processedData = isEncodedValue(doc["~data"]) ? process(doc["~data"]) : processRecord(doc["~data"], process);
154
180
  return {
155
181
  "~id": doc["~id"],
156
182
  "~data": processedData,
157
183
  "~deletedAt": doc["~deletedAt"]
158
184
  };
159
- };
185
+ }
160
186
 
161
187
  //#endregion
162
- //#region src/clock.ts
163
- const createClock = () => {
164
- let counter = 0;
165
- let lastMs = Date.now();
166
- let lastNonce = generateNonce();
167
- return {
168
- now: () => {
169
- const wallMs = Date.now();
170
- if (wallMs > lastMs) {
171
- lastMs = wallMs;
172
- counter = 0;
173
- lastNonce = generateNonce();
174
- } else {
175
- counter++;
176
- lastNonce = generateNonce();
177
- }
178
- return encodeEventstamp(lastMs, counter, lastNonce);
179
- },
180
- latest() {
181
- return encodeEventstamp(lastMs, counter, lastNonce);
182
- },
183
- forward(eventstamp) {
184
- if (eventstamp > this.latest()) {
185
- const newer = decodeEventstamp(eventstamp);
186
- lastMs = newer.timestampMs;
187
- counter = newer.counter;
188
- lastNonce = newer.nonce;
189
- }
188
+ //#region src/crdt/collection.ts
189
+ /**
190
+ * Merges two collections using field-level Last-Write-Wins semantics.
191
+ *
192
+ * The merge operation:
193
+ * 1. Forwards the clock to the newest eventstamp from either collection
194
+ * 2. Merges each document pair using field-level LWW (via mergeDocs)
195
+ * 3. Tracks what changed for hook notifications (added/updated/deleted)
196
+ *
197
+ * Deletion is final: once a document is deleted, updates to it are merged into
198
+ * the document's data but don't restore visibility. Only new documents or
199
+ * transitions into the deleted state are tracked.
200
+ *
201
+ * @param into - The base collection to merge into
202
+ * @param from - The source collection to merge from
203
+ * @returns Merged collection and categorized changes
204
+ *
205
+ * @example
206
+ * ```typescript
207
+ * const into = {
208
+ * "~docs": [{ "~id": "doc1", "~data": {...}, "~deletedAt": null }],
209
+ * "~eventstamp": "2025-01-01T00:00:00.000Z|0001|a1b2"
210
+ * };
211
+ *
212
+ * const from = {
213
+ * "~docs": [
214
+ * { "~id": "doc1", "~data": {...}, "~deletedAt": null }, // updated
215
+ * { "~id": "doc2", "~data": {...}, "~deletedAt": null } // new
216
+ * ],
217
+ * "~eventstamp": "2025-01-01T00:05:00.000Z|0001|c3d4"
218
+ * };
219
+ *
220
+ * const result = mergeCollections(into, from);
221
+ * // result.collection.~eventstamp === "2025-01-01T00:05:00.000Z|0001|c3d4"
222
+ * // result.changes.added has "doc2"
223
+ * // result.changes.updated has "doc1"
224
+ * ```
225
+ */
226
+ function mergeCollections(into, from) {
227
+ const intoDocsById = /* @__PURE__ */ new Map();
228
+ for (const doc of into["~docs"]) intoDocsById.set(doc["~id"], doc);
229
+ const added = /* @__PURE__ */ new Map();
230
+ const updated = /* @__PURE__ */ new Map();
231
+ const deleted = /* @__PURE__ */ new Set();
232
+ const mergedDocsById = new Map(intoDocsById);
233
+ for (const fromDoc of from["~docs"]) {
234
+ const id = fromDoc["~id"];
235
+ const intoDoc = intoDocsById.get(id);
236
+ if (!intoDoc) {
237
+ mergedDocsById.set(id, fromDoc);
238
+ if (!fromDoc["~deletedAt"]) added.set(id, fromDoc);
239
+ } else {
240
+ if (intoDoc === fromDoc) continue;
241
+ const [mergedDoc] = mergeDocs(intoDoc, fromDoc);
242
+ mergedDocsById.set(id, mergedDoc);
243
+ const wasDeleted = intoDoc["~deletedAt"] !== null;
244
+ const isDeleted = mergedDoc["~deletedAt"] !== null;
245
+ if (!wasDeleted && isDeleted) deleted.add(id);
246
+ else if (!isDeleted) updated.set(id, mergedDoc);
190
247
  }
191
- };
192
- };
193
-
194
- //#endregion
195
- //#region src/kv.ts
196
- const createKV = (iterable) => {
197
- let readMap = new Map(iterable);
248
+ }
249
+ const newestEventstamp = into["~eventstamp"] >= from["~eventstamp"] ? into["~eventstamp"] : from["~eventstamp"];
198
250
  return {
199
- get(key) {
200
- return readMap.get(key) ?? null;
201
- },
202
- values() {
203
- return readMap.values();
251
+ collection: {
252
+ "~docs": Array.from(mergedDocsById.values()),
253
+ "~eventstamp": newestEventstamp
204
254
  },
205
- entries() {
206
- return readMap.entries();
207
- },
208
- get size() {
209
- return readMap.size;
210
- },
211
- begin(callback) {
212
- const staging = new Map(readMap);
213
- let rolledBack = false;
214
- callback({
215
- get(key) {
216
- return staging.get(key) ?? null;
217
- },
218
- set(key, value, opts) {
219
- if (opts?.replace) {
220
- staging.set(key, value);
221
- return null;
222
- } else {
223
- const prev = staging.get(key);
224
- if (prev) {
225
- const [merged, eventstamp] = mergeDocs(prev, value);
226
- staging.set(key, merged);
227
- return eventstamp;
228
- } else {
229
- staging.set(key, value);
230
- return null;
231
- }
232
- }
233
- },
234
- del(key, eventstamp) {
235
- const prev = staging.get(key);
236
- if (prev) staging.set(key, deleteDoc(prev, eventstamp));
237
- },
238
- rollback() {
239
- rolledBack = true;
240
- }
241
- });
242
- if (!rolledBack) readMap = staging;
255
+ changes: {
256
+ added,
257
+ updated,
258
+ deleted
243
259
  }
244
260
  };
245
- };
261
+ }
246
262
 
247
263
  //#endregion
248
- //#region src/transaction.ts
264
+ //#region src/clock.ts
249
265
  /**
250
- * Factory for creating a transaction object with all mutation methods.
251
- * Closes over arrays for tracking puts, patches, and deletes.
266
+ * A Hybrid Logical Clock that generates monotonically increasing eventstamps.
267
+ * Combines wall-clock time with a counter for handling clock stalls and a
268
+ * random nonce for tie-breaking.
269
+ *
270
+ * The clock automatically increments the counter when the wall clock doesn't
271
+ * advance, ensuring eventstamps are always unique and monotonic.
252
272
  */
253
- const createTransaction = (kvTx, clock, getId, encodeValue$1, decodeActive, putKeyValues, patchKeyValues, deleteKeys) => {
254
- const state = { rolledBack: false };
255
- const tx = {
256
- rolledBack: false,
257
- add(value, options) {
258
- const key = options?.withId ?? getId();
259
- kvTx.set(key, encodeValue$1(key, value), { replace: true });
260
- putKeyValues.push([key, value]);
261
- return key;
262
- },
263
- update(key, value) {
264
- kvTx.set(key, encodeDoc(key, value, clock.now()));
265
- const merged = decodeActive(kvTx.get(key));
266
- if (merged) patchKeyValues.push([key, merged]);
267
- },
268
- merge(doc) {
269
- if (kvTx.get(doc["~id"])) kvTx.set(doc["~id"], doc);
270
- else kvTx.set(doc["~id"], doc, { replace: true });
271
- const currentDoc = kvTx.get(doc["~id"]);
272
- if (currentDoc) if (currentDoc["~deletedAt"]) deleteKeys.push(doc["~id"]);
273
- else {
274
- const merged = decodeDoc(currentDoc)["~data"];
275
- patchKeyValues.push([doc["~id"], merged]);
276
- }
277
- },
278
- del(key) {
279
- if (!kvTx.get(key)) return;
280
- kvTx.del(key, clock.now());
281
- deleteKeys.push(key);
282
- },
283
- get(key) {
284
- return decodeActive(kvTx.get(key));
285
- },
286
- rollback() {
287
- state.rolledBack = true;
288
- tx.rolledBack = true;
289
- kvTx.rollback();
273
+ var Clock = class {
274
+ #counter = 0;
275
+ #lastMs = Date.now();
276
+ #lastNonce = generateNonce();
277
+ /** Generates a new eventstamp, advancing the clock */
278
+ now() {
279
+ const wallMs = Date.now();
280
+ if (wallMs > this.#lastMs) {
281
+ this.#lastMs = wallMs;
282
+ this.#counter = 0;
283
+ this.#lastNonce = generateNonce();
284
+ } else {
285
+ this.#counter++;
286
+ this.#lastNonce = generateNonce();
290
287
  }
291
- };
292
- return tx;
288
+ return encodeEventstamp(this.#lastMs, this.#counter, this.#lastNonce);
289
+ }
290
+ /** Returns the most recent eventstamp without advancing the clock */
291
+ latest() {
292
+ return encodeEventstamp(this.#lastMs, this.#counter, this.#lastNonce);
293
+ }
294
+ /** Fast-forwards the clock to match a newer remote eventstamp */
295
+ forward(eventstamp) {
296
+ if (eventstamp > this.latest()) {
297
+ const newer = decodeEventstamp(eventstamp);
298
+ this.#lastMs = newer.timestampMs;
299
+ this.#counter = newer.counter;
300
+ this.#lastNonce = newer.nonce;
301
+ }
302
+ }
293
303
  };
294
304
 
295
305
  //#endregion
296
306
  //#region src/store.ts
297
- const createStore = (config = {}) => {
298
- const kv = createKV();
299
- const clock = createClock();
300
- const getId = config.getId ?? (() => crypto.randomUUID());
301
- const encodeValue$1 = (key, value) => encodeDoc(key, value, clock.now());
302
- const decodeActive = (doc) => {
307
+ var Store = class {
308
+ #readMap = /* @__PURE__ */ new Map();
309
+ #clock = new Clock();
310
+ #getId;
311
+ #onInitHandlers = [];
312
+ #onDisposeHandlers = [];
313
+ #onAddHandlers = [];
314
+ #onUpdateHandlers = [];
315
+ #onDeleteHandlers = [];
316
+ #queries = /* @__PURE__ */ new Set();
317
+ constructor(config = {}) {
318
+ this.#getId = config.getId ?? (() => crypto.randomUUID());
319
+ }
320
+ get(key) {
321
+ return this.#decodeActive(this.#readMap.get(key) ?? null);
322
+ }
323
+ entries() {
324
+ const self = this;
325
+ function* iterator() {
326
+ for (const [key, doc] of self.#readMap.entries()) {
327
+ const data = self.#decodeActive(doc);
328
+ if (data !== null) yield [key, data];
329
+ }
330
+ }
331
+ return iterator();
332
+ }
333
+ collection() {
334
+ return {
335
+ "~docs": Array.from(this.#readMap.values()),
336
+ "~eventstamp": this.#clock.latest()
337
+ };
338
+ }
339
+ merge(collection) {
340
+ const result = mergeCollections(this.collection(), collection);
341
+ this.#clock.forward(result.collection["~eventstamp"]);
342
+ this.#readMap = new Map(result.collection["~docs"].map((doc) => [doc["~id"], doc]));
343
+ const addEntries = Array.from(result.changes.added.entries()).map(([key, doc]) => [key, decodeDoc(doc)["~data"]]);
344
+ const updateEntries = Array.from(result.changes.updated.entries()).map(([key, doc]) => [key, decodeDoc(doc)["~data"]]);
345
+ const deleteKeys = Array.from(result.changes.deleted);
346
+ if (addEntries.length > 0 || updateEntries.length > 0 || deleteKeys.length > 0) this.#emitMutations(addEntries, updateEntries, deleteKeys);
347
+ }
348
+ begin(callback, opts) {
349
+ const silent = opts?.silent ?? false;
350
+ const addEntries = [];
351
+ const updateEntries = [];
352
+ const deleteKeys = [];
353
+ const staging = new Map(this.#readMap);
354
+ let rolledBack = false;
355
+ const result = callback({
356
+ add: (value, options) => {
357
+ const key = options?.withId ?? this.#getId();
358
+ staging.set(key, this.#encodeValue(key, value));
359
+ addEntries.push([key, value]);
360
+ return key;
361
+ },
362
+ update: (key, value) => {
363
+ const doc = encodeDoc(key, value, this.#clock.now());
364
+ const prev = staging.get(key);
365
+ const mergedDoc = prev ? mergeDocs(prev, doc)[0] : doc;
366
+ staging.set(key, mergedDoc);
367
+ const merged = this.#decodeActive(mergedDoc);
368
+ if (merged !== null) updateEntries.push([key, merged]);
369
+ },
370
+ merge: (doc) => {
371
+ const existing = staging.get(doc["~id"]);
372
+ const mergedDoc = existing ? mergeDocs(existing, doc)[0] : doc;
373
+ staging.set(doc["~id"], mergedDoc);
374
+ const decoded = this.#decodeActive(mergedDoc);
375
+ const isNew = !this.#readMap.has(doc["~id"]);
376
+ if (mergedDoc["~deletedAt"]) deleteKeys.push(doc["~id"]);
377
+ else if (decoded !== null) if (isNew) addEntries.push([doc["~id"], decoded]);
378
+ else updateEntries.push([doc["~id"], decoded]);
379
+ },
380
+ del: (key) => {
381
+ const currentDoc = staging.get(key);
382
+ if (!currentDoc) return;
383
+ staging.set(key, deleteDoc(currentDoc, this.#clock.now()));
384
+ deleteKeys.push(key);
385
+ },
386
+ get: (key) => this.#decodeActive(staging.get(key) ?? null),
387
+ rollback: () => {
388
+ rolledBack = true;
389
+ }
390
+ });
391
+ if (!rolledBack) {
392
+ this.#readMap = staging;
393
+ if (!silent) this.#emitMutations(addEntries, updateEntries, deleteKeys);
394
+ }
395
+ return result;
396
+ }
397
+ add(value, options) {
398
+ return this.begin((tx) => tx.add(value, options));
399
+ }
400
+ update(key, value) {
401
+ this.begin((tx) => tx.update(key, value));
402
+ }
403
+ del(key) {
404
+ this.begin((tx) => tx.del(key));
405
+ }
406
+ use(plugin) {
407
+ this.#onInitHandlers.push(plugin.onInit);
408
+ this.#onDisposeHandlers.push(plugin.onDispose);
409
+ if (plugin.onAdd) this.#onAddHandlers.push(plugin.onAdd);
410
+ if (plugin.onUpdate) this.#onUpdateHandlers.push(plugin.onUpdate);
411
+ if (plugin.onDelete) this.#onDeleteHandlers.push(plugin.onDelete);
412
+ return this;
413
+ }
414
+ async init() {
415
+ for (const hook of this.#onInitHandlers) await hook(this);
416
+ for (const query of this.#queries) this.#hydrateQuery(query);
417
+ return this;
418
+ }
419
+ async dispose() {
420
+ for (let i = this.#onDisposeHandlers.length - 1; i >= 0; i--) await this.#onDisposeHandlers[i]?.();
421
+ for (const query of this.#queries) {
422
+ query.callbacks.clear();
423
+ query.results.clear();
424
+ }
425
+ this.#queries.clear();
426
+ this.#onInitHandlers = [];
427
+ this.#onDisposeHandlers = [];
428
+ this.#onAddHandlers = [];
429
+ this.#onUpdateHandlers = [];
430
+ this.#onDeleteHandlers = [];
431
+ }
432
+ query(config) {
433
+ const query = {
434
+ where: config.where,
435
+ select: config.select,
436
+ order: config.order,
437
+ results: /* @__PURE__ */ new Map(),
438
+ callbacks: /* @__PURE__ */ new Set()
439
+ };
440
+ this.#queries.add(query);
441
+ this.#hydrateQuery(query);
442
+ return {
443
+ results: () => {
444
+ if (query.order) return Array.from(query.results).sort(([, a], [, b]) => query.order(a, b));
445
+ return Array.from(query.results);
446
+ },
447
+ onChange: (callback) => {
448
+ query.callbacks.add(callback);
449
+ return () => {
450
+ query.callbacks.delete(callback);
451
+ };
452
+ },
453
+ dispose: () => {
454
+ this.#queries.delete(query);
455
+ query.callbacks.clear();
456
+ query.results.clear();
457
+ }
458
+ };
459
+ }
460
+ #encodeValue(key, value) {
461
+ return encodeDoc(key, value, this.#clock.now());
462
+ }
463
+ #decodeActive(doc) {
303
464
  if (!doc || doc["~deletedAt"]) return null;
304
465
  return decodeDoc(doc)["~data"];
305
- };
306
- const onInitHandlers = /* @__PURE__ */ new Set();
307
- const onDisposeHandlers = /* @__PURE__ */ new Set();
308
- const onAddHandlers = /* @__PURE__ */ new Set();
309
- const onUpdateHandlers = /* @__PURE__ */ new Set();
310
- const onDeleteHandlers = /* @__PURE__ */ new Set();
311
- return {
312
- get(key) {
313
- return decodeActive(kv.get(key));
314
- },
315
- entries() {
316
- function* iterator() {
317
- for (const [key, doc] of kv.entries()) {
318
- const data = decodeActive(doc);
319
- if (data !== null) yield [key, data];
320
- }
466
+ }
467
+ #emitMutations(addEntries, updateEntries, deleteKeys) {
468
+ this.#notifyQueries(addEntries, updateEntries, deleteKeys);
469
+ if (addEntries.length > 0) for (const handler of this.#onAddHandlers) handler(addEntries);
470
+ if (updateEntries.length > 0) for (const handler of this.#onUpdateHandlers) handler(updateEntries);
471
+ if (deleteKeys.length > 0) for (const handler of this.#onDeleteHandlers) handler(deleteKeys);
472
+ }
473
+ #notifyQueries(addEntries, updateEntries, deleteKeys) {
474
+ if (this.#queries.size === 0) return;
475
+ const dirtyQueries = /* @__PURE__ */ new Set();
476
+ if (addEntries.length > 0) {
477
+ for (const [key, value] of addEntries) for (const query of this.#queries) if (query.where(value)) {
478
+ const selected = this.#selectValue(query, value);
479
+ query.results.set(key, selected);
480
+ dirtyQueries.add(query);
321
481
  }
322
- return iterator();
323
- },
324
- snapshot() {
325
- return {
326
- docs: Array.from(kv.values()),
327
- latestEventstamp: clock.latest()
328
- };
329
- },
330
- merge(snapshot) {
331
- clock.forward(snapshot.latestEventstamp);
332
- this.begin((tx) => {
333
- for (const doc of snapshot.docs) tx.merge(doc);
334
- });
335
- },
336
- begin(callback, opts) {
337
- const silent = opts?.silent ?? false;
338
- const putKeyValues = [];
339
- const patchKeyValues = [];
340
- const deleteKeys = [];
341
- let result;
342
- let shouldNotify = false;
343
- kv.begin((kvTx) => {
344
- const tx = createTransaction(kvTx, clock, getId, encodeValue$1, decodeActive, putKeyValues, patchKeyValues, deleteKeys);
345
- result = callback(tx);
346
- if (!tx.rolledBack && !silent) shouldNotify = true;
347
- });
348
- if (shouldNotify) {
349
- if (putKeyValues.length > 0) onAddHandlers.forEach((fn) => {
350
- fn(putKeyValues);
351
- });
352
- if (patchKeyValues.length > 0) onUpdateHandlers.forEach((fn) => {
353
- fn(patchKeyValues);
354
- });
355
- if (deleteKeys.length > 0) onDeleteHandlers.forEach((fn) => {
356
- fn(deleteKeys);
357
- });
358
- }
359
- return result;
360
- },
361
- add(value, options) {
362
- return this.begin((tx) => tx.add(value, options));
363
- },
364
- update(key, value) {
365
- return this.begin((tx) => tx.update(key, value));
366
- },
367
- del(key) {
368
- return this.begin((tx) => tx.del(key));
369
- },
370
- use(plugin) {
371
- const { hooks: pluginHooks, methods } = plugin;
372
- if (pluginHooks.onAdd || pluginHooks.onUpdate || pluginHooks.onDelete) {
373
- if (pluginHooks.onAdd) {
374
- const onAdd = pluginHooks.onAdd;
375
- onAddHandlers.add(onAdd);
376
- onDisposeHandlers.add(() => {
377
- onAddHandlers.delete(onAdd);
378
- });
379
- }
380
- if (pluginHooks.onUpdate) {
381
- const onUpdate = pluginHooks.onUpdate;
382
- onUpdateHandlers.add(onUpdate);
383
- onDisposeHandlers.add(() => {
384
- onUpdateHandlers.delete(onUpdate);
385
- });
386
- }
387
- if (pluginHooks.onDelete) {
388
- const onDelete = pluginHooks.onDelete;
389
- onDeleteHandlers.add(onDelete);
390
- onDisposeHandlers.add(() => {
391
- onDeleteHandlers.delete(onDelete);
392
- });
393
- }
482
+ }
483
+ if (updateEntries.length > 0) for (const [key, value] of updateEntries) for (const query of this.#queries) {
484
+ const matches = query.where(value);
485
+ const inResults = query.results.has(key);
486
+ if (matches && !inResults) {
487
+ const selected = this.#selectValue(query, value);
488
+ query.results.set(key, selected);
489
+ dirtyQueries.add(query);
490
+ } else if (!matches && inResults) {
491
+ query.results.delete(key);
492
+ dirtyQueries.add(query);
493
+ } else if (matches && inResults) {
494
+ const selected = this.#selectValue(query, value);
495
+ query.results.set(key, selected);
496
+ dirtyQueries.add(query);
394
497
  }
395
- if (methods) Object.assign(this, methods);
396
- onInitHandlers.add(pluginHooks.onInit);
397
- onDisposeHandlers.add(pluginHooks.onDispose);
398
- return this;
399
- },
400
- async init() {
401
- for (const fn of onInitHandlers) await fn(this);
402
- return this;
403
- },
404
- async dispose() {
405
- const disposerArray = Array.from(onDisposeHandlers);
406
- disposerArray.reverse();
407
- for (const fn of disposerArray) await fn();
408
- },
409
- latestEventstamp() {
410
- return clock.latest();
411
- },
412
- forwardClock(eventstamp) {
413
- clock.forward(eventstamp);
414
498
  }
415
- };
499
+ if (deleteKeys.length > 0) {
500
+ for (const key of deleteKeys) for (const query of this.#queries) if (query.results.delete(key)) dirtyQueries.add(query);
501
+ }
502
+ if (dirtyQueries.size > 0) this.#runQueryCallbacks(dirtyQueries);
503
+ }
504
+ #runQueryCallbacks(dirtyQueries) {
505
+ for (const query of dirtyQueries) for (const callback of query.callbacks) callback();
506
+ }
507
+ #hydrateQuery(query) {
508
+ query.results.clear();
509
+ for (const [key, value] of this.entries()) if (query.where(value)) {
510
+ const selected = this.#selectValue(query, value);
511
+ query.results.set(key, selected);
512
+ }
513
+ }
514
+ #selectValue(query, value) {
515
+ return query.select ? query.select(value) : value;
516
+ }
416
517
  };
417
518
 
418
519
  //#endregion
419
- export { createStore, processDocument };
520
+ export { Store, processDocument };
@@ -0,0 +1,17 @@
1
+ import { c as Collection, t as Plugin } from "../../store-Dc-hIF56.js";
2
+ import { Storage } from "unstorage";
3
+
4
+ //#region src/plugins/unstorage/plugin.d.ts
5
+ type MaybePromise<T> = T | Promise<T>;
6
+ type UnstorageOnBeforeSet = (data: Collection) => MaybePromise<Collection>;
7
+ type UnstorageOnAfterGet = (data: Collection) => MaybePromise<Collection>;
8
+ type UnstorageConfig = {
9
+ debounceMs?: number;
10
+ pollIntervalMs?: number;
11
+ onBeforeSet?: UnstorageOnBeforeSet;
12
+ onAfterGet?: UnstorageOnAfterGet;
13
+ skip?: () => boolean;
14
+ };
15
+ declare function unstoragePlugin<T>(key: string, storage: Storage<Collection>, config?: UnstorageConfig): Plugin<T>;
16
+ //#endregion
17
+ export { type UnstorageConfig, unstoragePlugin };
@@ -1,12 +1,12 @@
1
1
  //#region src/plugins/unstorage/plugin.ts
2
- const unstoragePlugin = (key, storage, config = {}) => {
2
+ function unstoragePlugin(key, storage, config = {}) {
3
3
  const { debounceMs = 0, pollIntervalMs, onBeforeSet, onAfterGet, skip } = config;
4
4
  let debounceTimer = null;
5
5
  let pollInterval = null;
6
6
  let store = null;
7
7
  const persistSnapshot = async () => {
8
8
  if (!store) return;
9
- const data = store.snapshot();
9
+ const data = store.collection();
10
10
  const persisted = onBeforeSet !== void 0 ? await onBeforeSet(data) : data;
11
11
  await storage.set(key, persisted);
12
12
  };
@@ -31,7 +31,7 @@ const unstoragePlugin = (key, storage, config = {}) => {
31
31
  const data = onAfterGet !== void 0 ? await onAfterGet(persisted) : persisted;
32
32
  store.merge(data);
33
33
  };
34
- return { hooks: {
34
+ return {
35
35
  onInit: async (s) => {
36
36
  store = s;
37
37
  await pollStorage();
@@ -59,8 +59,8 @@ const unstoragePlugin = (key, storage, config = {}) => {
59
59
  onDelete: () => {
60
60
  schedulePersist();
61
61
  }
62
- } };
63
- };
62
+ };
63
+ }
64
64
 
65
65
  //#endregion
66
66
  export { unstoragePlugin };
@@ -0,0 +1,114 @@
1
+ //#region src/crdt/value.d.ts
2
+ /**
3
+ * A primitive value wrapped with its eventstamp for Last-Write-Wins conflict resolution.
4
+ * Used as the leaf nodes in the CRDT data structure.
5
+ *
6
+ * @template T - The type of the wrapped value (primitive or complex type)
7
+ */
8
+ type EncodedValue<T> = {
9
+ /** The actual value being stored */
10
+ "~value": T;
11
+ /** The eventstamp indicating when this value was last written (ISO|counter|nonce) */
12
+ "~eventstamp": string;
13
+ };
14
+ //#endregion
15
+ //#region src/crdt/record.d.ts
16
+ /**
17
+ * A nested object structure where each field is either an EncodedValue (leaf)
18
+ * or another EncodedRecord (nested object). This enables field-level
19
+ * Last-Write-Wins merging for complex data structures.
20
+ *
21
+ * Each field maintains its own eventstamp, allowing concurrent updates to
22
+ * different fields to be preserved during merge operations.
23
+ */
24
+ type EncodedRecord = {
25
+ [key: string]: EncodedValue<unknown> | EncodedRecord;
26
+ };
27
+ //#endregion
28
+ //#region src/crdt/document.d.ts
29
+ /**
30
+ * Top-level document structure with system metadata for tracking identity,
31
+ * data, and deletion state. Documents are the primary unit of storage and
32
+ * synchronization in Starling.
33
+ *
34
+ * The tilde prefix (~) distinguishes system metadata from user-defined data.
35
+ */
36
+ type EncodedDocument = {
37
+ /** Unique identifier for this document */
38
+ "~id": string;
39
+ /** The document's data, either a primitive value or nested object structure */
40
+ "~data": EncodedValue<unknown> | EncodedRecord;
41
+ /** Eventstamp when this document was soft-deleted, or null if not deleted */
42
+ "~deletedAt": string | null;
43
+ };
44
+ declare function processDocument(doc: EncodedDocument, process: (value: EncodedValue<unknown>) => EncodedValue<unknown>): EncodedDocument;
45
+ //#endregion
46
+ //#region src/crdt/collection.d.ts
47
+ /**
48
+ * A collection represents the complete state of a store:
49
+ * - A set of documents (including soft-deleted ones)
50
+ * - The highest eventstamp observed across all operations
51
+ *
52
+ * Collections are the unit of synchronization between store replicas.
53
+ */
54
+ type Collection = {
55
+ /** Array of encoded documents with eventstamps and metadata */
56
+ "~docs": EncodedDocument[];
57
+ /** Latest eventstamp observed by this collection for clock synchronization */
58
+ "~eventstamp": string;
59
+ };
60
+ //#endregion
61
+ //#region src/store.d.ts
62
+ type NotPromise<T> = T extends Promise<any> ? never : T;
63
+ type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;
64
+ type StoreAddOptions = {
65
+ withId?: string;
66
+ };
67
+ type StoreConfig = {
68
+ getId?: () => string;
69
+ };
70
+ type StoreSetTransaction<T> = {
71
+ add: (value: T, options?: StoreAddOptions) => string;
72
+ update: (key: string, value: DeepPartial<T>) => void;
73
+ merge: (doc: EncodedDocument) => void;
74
+ del: (key: string) => void;
75
+ get: (key: string) => T | null;
76
+ rollback: () => void;
77
+ };
78
+ type Plugin<T> = {
79
+ onInit: (store: Store<T>) => Promise<void> | void;
80
+ onDispose: () => Promise<void> | void;
81
+ onAdd?: (entries: ReadonlyArray<readonly [string, T]>) => void;
82
+ onUpdate?: (entries: ReadonlyArray<readonly [string, T]>) => void;
83
+ onDelete?: (keys: ReadonlyArray<string>) => void;
84
+ };
85
+ type QueryConfig<T, U = T> = {
86
+ where: (data: T) => boolean;
87
+ select?: (data: T) => U;
88
+ order?: (a: U, b: U) => number;
89
+ };
90
+ type Query<U> = {
91
+ results: () => Array<readonly [string, U]>;
92
+ onChange: (callback: () => void) => () => void;
93
+ dispose: () => void;
94
+ };
95
+ declare class Store<T> {
96
+ #private;
97
+ constructor(config?: StoreConfig);
98
+ get(key: string): T | null;
99
+ entries(): IterableIterator<readonly [string, T]>;
100
+ collection(): Collection;
101
+ merge(collection: Collection): void;
102
+ begin<R = void>(callback: (tx: StoreSetTransaction<T>) => NotPromise<R>, opts?: {
103
+ silent?: boolean;
104
+ }): NotPromise<R>;
105
+ add(value: T, options?: StoreAddOptions): string;
106
+ update(key: string, value: DeepPartial<T>): void;
107
+ del(key: string): void;
108
+ use(plugin: Plugin<T>): this;
109
+ init(): Promise<this>;
110
+ dispose(): Promise<void>;
111
+ query<U = T>(config: QueryConfig<T, U>): Query<U>;
112
+ }
113
+ //#endregion
114
+ export { StoreAddOptions as a, Collection as c, Store as i, EncodedDocument as l, Query as n, StoreConfig as o, QueryConfig as r, StoreSetTransaction as s, Plugin as t, processDocument as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byearlybird/starling",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -11,15 +11,10 @@
11
11
  "import": "./dist/index.js",
12
12
  "default": "./dist/index.js"
13
13
  },
14
- "./plugin-query": {
15
- "types": "./dist/plugin-query.d.ts",
16
- "import": "./dist/plugin-query.js",
17
- "default": "./dist/plugin-query.js"
18
- },
19
14
  "./plugin-unstorage": {
20
- "types": "./dist/plugin-unstorage.d.ts",
21
- "import": "./dist/plugin-unstorage.js",
22
- "default": "./dist/plugin-unstorage.js"
15
+ "types": "./dist/plugins/unstorage/plugin.d.ts",
16
+ "import": "./dist/plugins/unstorage/plugin.js",
17
+ "default": "./dist/plugins/unstorage/plugin.js"
23
18
  }
24
19
  },
25
20
  "files": [
@@ -1,19 +0,0 @@
1
- import { t as Plugin } from "./store-BGsRIZeM.js";
2
-
3
- //#region src/plugins/query/plugin.d.ts
4
- type QueryConfig<T, U = T> = {
5
- where: (data: T) => boolean;
6
- select?: (data: T) => U;
7
- order?: (a: U, b: U) => number;
8
- };
9
- type Query<U> = {
10
- results: () => Map<string, U>;
11
- onChange: (callback: () => void) => () => void;
12
- dispose: () => void;
13
- };
14
- type QueryMethods<T> = {
15
- query: <U = T>(config: QueryConfig<T, U>) => Query<U>;
16
- };
17
- declare const queryPlugin: <T>() => Plugin<T, QueryMethods<T>>;
18
- //#endregion
19
- export { type Query, type QueryConfig, type QueryMethods, queryPlugin };
@@ -1,102 +0,0 @@
1
- //#region src/plugins/query/plugin.ts
2
- const queryPlugin = () => {
3
- const queries = /* @__PURE__ */ new Set();
4
- let store = null;
5
- const hydrateQuery = (query) => {
6
- if (!store) return;
7
- query.results.clear();
8
- for (const [key, value] of store.entries()) if (query.where(value)) {
9
- const selected = query.select ? query.select(value) : value;
10
- query.results.set(key, selected);
11
- }
12
- };
13
- const runCallbacks = (dirtyQueries) => {
14
- for (const query of dirtyQueries) for (const callback of query.callbacks) callback();
15
- dirtyQueries.clear();
16
- };
17
- const onAdd = (entries) => {
18
- const dirtyQueries = /* @__PURE__ */ new Set();
19
- for (const [key, value] of entries) for (const q of queries) if (q.where(value)) {
20
- const selected = q.select ? q.select(value) : value;
21
- q.results.set(key, selected);
22
- dirtyQueries.add(q);
23
- }
24
- runCallbacks(dirtyQueries);
25
- };
26
- const onUpdate = (entries) => {
27
- const dirtyQueries = /* @__PURE__ */ new Set();
28
- for (const [key, value] of entries) for (const q of queries) {
29
- const matches = q.where(value);
30
- const inResults = q.results.has(key);
31
- if (matches && !inResults) {
32
- const selected = q.select ? q.select(value) : value;
33
- q.results.set(key, selected);
34
- dirtyQueries.add(q);
35
- } else if (!matches && inResults) {
36
- q.results.delete(key);
37
- dirtyQueries.add(q);
38
- } else if (matches && inResults) {
39
- const selected = q.select ? q.select(value) : value;
40
- q.results.set(key, selected);
41
- dirtyQueries.add(q);
42
- }
43
- }
44
- runCallbacks(dirtyQueries);
45
- };
46
- const onDelete = (keys) => {
47
- const dirtyQueries = /* @__PURE__ */ new Set();
48
- for (const key of keys) for (const q of queries) if (q.results.has(key)) {
49
- q.results.delete(key);
50
- dirtyQueries.add(q);
51
- }
52
- runCallbacks(dirtyQueries);
53
- };
54
- return {
55
- hooks: {
56
- onInit: (s) => {
57
- store = s;
58
- for (const q of queries) hydrateQuery(q);
59
- },
60
- onDispose: () => {
61
- queries.clear();
62
- store = null;
63
- },
64
- onAdd,
65
- onUpdate,
66
- onDelete
67
- },
68
- methods: { query: (config) => {
69
- const query = {
70
- where: config.where,
71
- ...config.select && { select: config.select },
72
- ...config.order && { order: config.order },
73
- results: /* @__PURE__ */ new Map(),
74
- callbacks: /* @__PURE__ */ new Set()
75
- };
76
- queries.add(query);
77
- hydrateQuery(query);
78
- return {
79
- results: () => {
80
- if (query.order) {
81
- const orderFn = query.order;
82
- const sorted = Array.from(query.results).sort(([, a], [, b]) => orderFn(a, b));
83
- return new Map(sorted);
84
- } else return new Map(query.results);
85
- },
86
- onChange: (callback) => {
87
- query.callbacks.add(callback);
88
- return () => {
89
- query.callbacks.delete(callback);
90
- };
91
- },
92
- dispose: () => {
93
- queries.delete(query);
94
- query.callbacks.clear();
95
- }
96
- };
97
- } }
98
- };
99
- };
100
-
101
- //#endregion
102
- export { queryPlugin };
@@ -1,17 +0,0 @@
1
- import { a as StoreSnapshot, t as Plugin } from "./store-BGsRIZeM.js";
2
- import { Storage } from "unstorage";
3
-
4
- //#region src/plugins/unstorage/plugin.d.ts
5
- type MaybePromise<T> = T | Promise<T>;
6
- type UnstorageOnBeforeSet = (data: StoreSnapshot) => MaybePromise<StoreSnapshot>;
7
- type UnstorageOnAfterGet = (data: StoreSnapshot) => MaybePromise<StoreSnapshot>;
8
- type UnstorageConfig = {
9
- debounceMs?: number;
10
- pollIntervalMs?: number;
11
- onBeforeSet?: UnstorageOnBeforeSet;
12
- onAfterGet?: UnstorageOnAfterGet;
13
- skip?: () => boolean;
14
- };
15
- declare const unstoragePlugin: <T>(key: string, storage: Storage<StoreSnapshot>, config?: UnstorageConfig) => Plugin<T>;
16
- //#endregion
17
- export { type UnstorageConfig, unstoragePlugin };
@@ -1,86 +0,0 @@
1
- //#region src/value.d.ts
2
- type EncodedValue<T> = {
3
- "~value": T;
4
- "~eventstamp": string;
5
- };
6
- //#endregion
7
- //#region src/record.d.ts
8
- type EncodedRecord = {
9
- [key: string]: EncodedValue<unknown> | EncodedRecord;
10
- };
11
- //#endregion
12
- //#region src/document.d.ts
13
- type EncodedDocument = {
14
- "~id": string;
15
- "~data": EncodedValue<unknown> | EncodedRecord;
16
- "~deletedAt": string | null;
17
- };
18
- declare const processDocument: (doc: EncodedDocument, process: (value: EncodedValue<unknown>) => EncodedValue<unknown>) => EncodedDocument;
19
- //#endregion
20
- //#region src/transaction.d.ts
21
- type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;
22
- type StorePutOptions = {
23
- withId?: string;
24
- };
25
- type StoreSetTransaction<T> = {
26
- add: (value: T, options?: StorePutOptions) => string;
27
- update: (key: string, value: DeepPartial<T>) => void;
28
- merge: (doc: EncodedDocument) => void;
29
- del: (key: string) => void;
30
- get: (key: string) => T | null;
31
- rollback: () => void;
32
- };
33
- //#endregion
34
- //#region src/store.d.ts
35
- /**
36
- * Type constraint to prevent Promise returns from set callbacks.
37
- * Transactions must be synchronous operations.
38
- */
39
- type NotPromise<T> = T extends Promise<any> ? never : T;
40
- /**
41
- * Plugin lifecycle and event hooks.
42
- * All hooks are optional except onInit and onDispose, which are required.
43
- */
44
- type PluginHooks<T> = {
45
- onInit: (store: Store<T>) => Promise<void> | void;
46
- onDispose: () => Promise<void> | void;
47
- onAdd?: (entries: ReadonlyArray<readonly [string, T]>) => void;
48
- onUpdate?: (entries: ReadonlyArray<readonly [string, T]>) => void;
49
- onDelete?: (keys: ReadonlyArray<string>) => void;
50
- };
51
- type PluginMethods = Record<string, (...args: any[]) => any>;
52
- type Plugin<T, M extends PluginMethods = {}> = {
53
- hooks: PluginHooks<T>;
54
- methods?: M;
55
- };
56
- /**
57
- * Complete persistent state of a store.
58
- * Contains all encoded documents (including deleted ones with ~deletedAt metadata)
59
- * and the latest eventstamp for clock synchronization during merges.
60
- */
61
- type StoreSnapshot = {
62
- docs: EncodedDocument[];
63
- latestEventstamp: string;
64
- };
65
- type Store<T, Extended = {}> = {
66
- get: (key: string) => T | null;
67
- begin: <R = void>(callback: (tx: StoreSetTransaction<T>) => NotPromise<R>, opts?: {
68
- silent?: boolean;
69
- }) => NotPromise<R>;
70
- add: (value: T, options?: StorePutOptions) => string;
71
- update: (key: string, value: DeepPartial<T>) => void;
72
- del: (key: string) => void;
73
- entries: () => IterableIterator<readonly [string, T]>;
74
- snapshot: () => StoreSnapshot;
75
- merge: (snapshot: StoreSnapshot) => void;
76
- use: <M extends PluginMethods>(plugin: Plugin<T, M>) => Store<T, Extended & M>;
77
- init: () => Promise<Store<T, Extended>>;
78
- dispose: () => Promise<void>;
79
- latestEventstamp: () => string;
80
- forwardClock: (eventstamp: string) => void;
81
- } & Extended;
82
- declare const createStore: <T>(config?: {
83
- getId?: () => string;
84
- }) => Store<T, {}>;
85
- //#endregion
86
- export { StoreSnapshot as a, processDocument as c, Store as i, PluginHooks as n, createStore as o, PluginMethods as r, EncodedDocument as s, Plugin as t };