@byearlybird/starling 0.9.0 → 0.9.2

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 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 };
1
+ import { a as StoreAddOptions, c as Collection, d as EncodedRecord, f as EncodedValue, 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-F8qPSj_p.js";
2
+ export { type Collection, type EncodedDocument, type EncodedRecord, type EncodedValue, Plugin, Query, QueryConfig, Store, StoreAddOptions, StoreConfig, StoreSetTransaction, processDocument };
package/dist/index.js CHANGED
@@ -175,6 +175,24 @@ function deleteDoc(doc, eventstamp) {
175
175
  "~deletedAt": eventstamp
176
176
  };
177
177
  }
178
+ /**
179
+ * Transform all values in a document using a provided function.
180
+ *
181
+ * Useful for custom serialization in plugin hooks (encryption, compression, etc.)
182
+ *
183
+ * @param doc - Document to transform
184
+ * @param process - Function to apply to each leaf value
185
+ * @returns New document with transformed values
186
+ *
187
+ * @example
188
+ * ```ts
189
+ * // Encrypt all values before persisting
190
+ * const encrypted = processDocument(doc, (value) => ({
191
+ * ...value,
192
+ * "~value": encrypt(value["~value"])
193
+ * }));
194
+ * ```
195
+ */
178
196
  function processDocument(doc, process) {
179
197
  const processedData = isEncodedValue(doc["~data"]) ? process(doc["~data"]) : processRecord(doc["~data"], process);
180
198
  return {
@@ -304,6 +322,30 @@ var Clock = class {
304
322
 
305
323
  //#endregion
306
324
  //#region src/store.ts
325
+ /**
326
+ * Lightweight local-first data store with built-in sync and reactive queries.
327
+ *
328
+ * Stores plain JavaScript objects with automatic field-level conflict resolution
329
+ * using Last-Write-Wins semantics powered by hybrid logical clocks.
330
+ *
331
+ * @template T - The type of documents stored in this collection
332
+ *
333
+ * @example
334
+ * ```ts
335
+ * const store = await new Store<{ text: string; completed: boolean }>()
336
+ * .use(unstoragePlugin('todos', storage))
337
+ * .init();
338
+ *
339
+ * // Add, update, delete
340
+ * const id = store.add({ text: 'Buy milk', completed: false });
341
+ * store.update(id, { completed: true });
342
+ * store.del(id);
343
+ *
344
+ * // Reactive queries
345
+ * const activeTodos = store.query({ where: (todo) => !todo.completed });
346
+ * activeTodos.onChange(() => console.log('Todos changed!'));
347
+ * ```
348
+ */
307
349
  var Store = class {
308
350
  #readMap = /* @__PURE__ */ new Map();
309
351
  #clock = new Clock();
@@ -317,9 +359,16 @@ var Store = class {
317
359
  constructor(config = {}) {
318
360
  this.#getId = config.getId ?? (() => crypto.randomUUID());
319
361
  }
362
+ /**
363
+ * Get a document by ID.
364
+ * @returns The document, or null if not found or deleted
365
+ */
320
366
  get(key) {
321
367
  return this.#decodeActive(this.#readMap.get(key) ?? null);
322
368
  }
369
+ /**
370
+ * Iterate over all non-deleted documents as [id, document] tuples.
371
+ */
323
372
  entries() {
324
373
  const self = this;
325
374
  function* iterator() {
@@ -330,12 +379,20 @@ var Store = class {
330
379
  }
331
380
  return iterator();
332
381
  }
382
+ /**
383
+ * Get the complete store state as a Collection for persistence or sync.
384
+ * @returns Collection containing all documents and the latest eventstamp
385
+ */
333
386
  collection() {
334
387
  return {
335
388
  "~docs": Array.from(this.#readMap.values()),
336
389
  "~eventstamp": this.#clock.latest()
337
390
  };
338
391
  }
392
+ /**
393
+ * Merge a collection from storage or another replica using field-level LWW.
394
+ * @param collection - Collection from storage or another store instance
395
+ */
339
396
  merge(collection) {
340
397
  const result = mergeCollections(this.collection(), collection);
341
398
  this.#clock.forward(result.collection["~eventstamp"]);
@@ -345,6 +402,22 @@ var Store = class {
345
402
  const deleteKeys = Array.from(result.changes.deleted);
346
403
  if (addEntries.length > 0 || updateEntries.length > 0 || deleteKeys.length > 0) this.#emitMutations(addEntries, updateEntries, deleteKeys);
347
404
  }
405
+ /**
406
+ * Run multiple operations in a transaction with rollback support.
407
+ *
408
+ * @param callback - Function receiving a transaction context
409
+ * @param opts - Optional config. Use `silent: true` to skip plugin hooks.
410
+ * @returns The callback's return value
411
+ *
412
+ * @example
413
+ * ```ts
414
+ * const id = store.begin((tx) => {
415
+ * const newId = tx.add({ text: 'Buy milk' });
416
+ * tx.update(newId, { priority: 'high' });
417
+ * return newId; // Return value becomes begin()'s return value
418
+ * });
419
+ * ```
420
+ */
348
421
  begin(callback, opts) {
349
422
  const silent = opts?.silent ?? false;
350
423
  const addEntries = [];
@@ -394,15 +467,34 @@ var Store = class {
394
467
  }
395
468
  return result;
396
469
  }
470
+ /**
471
+ * Add a document to the store.
472
+ * @returns The document's ID (generated or provided via options)
473
+ */
397
474
  add(value, options) {
398
475
  return this.begin((tx) => tx.add(value, options));
399
476
  }
477
+ /**
478
+ * Update a document with a partial value.
479
+ *
480
+ * Uses field-level merge - only specified fields are updated.
481
+ */
400
482
  update(key, value) {
401
483
  this.begin((tx) => tx.update(key, value));
402
484
  }
485
+ /**
486
+ * Soft-delete a document.
487
+ *
488
+ * Deleted docs remain in snapshots for sync purposes but are
489
+ * excluded from queries and reads.
490
+ */
403
491
  del(key) {
404
492
  this.begin((tx) => tx.del(key));
405
493
  }
494
+ /**
495
+ * Register a plugin for persistence, analytics, etc.
496
+ * @returns This store instance for chaining
497
+ */
406
498
  use(plugin) {
407
499
  this.#onInitHandlers.push(plugin.onInit);
408
500
  this.#onDisposeHandlers.push(plugin.onDispose);
@@ -411,11 +503,25 @@ var Store = class {
411
503
  if (plugin.onDelete) this.#onDeleteHandlers.push(plugin.onDelete);
412
504
  return this;
413
505
  }
506
+ /**
507
+ * Initialize the store and run plugin onInit hooks.
508
+ *
509
+ * Must be called before using the store. Runs plugin setup (hydrate
510
+ * snapshots, start pollers, etc.) and hydrates existing queries.
511
+ *
512
+ * @returns This store instance for chaining
513
+ */
414
514
  async init() {
415
515
  for (const hook of this.#onInitHandlers) await hook(this);
416
516
  for (const query of this.#queries) this.#hydrateQuery(query);
417
517
  return this;
418
518
  }
519
+ /**
520
+ * Dispose the store and run plugin cleanup.
521
+ *
522
+ * Flushes pending operations, clears queries, and runs plugin teardown.
523
+ * Call when shutting down to avoid memory leaks.
524
+ */
419
525
  async dispose() {
420
526
  for (let i = this.#onDisposeHandlers.length - 1; i >= 0; i--) await this.#onDisposeHandlers[i]?.();
421
527
  for (const query of this.#queries) {
@@ -429,6 +535,17 @@ var Store = class {
429
535
  this.#onUpdateHandlers = [];
430
536
  this.#onDeleteHandlers = [];
431
537
  }
538
+ /**
539
+ * Create a reactive query that auto-updates when matching docs change.
540
+ *
541
+ * @example
542
+ * ```ts
543
+ * const active = store.query({ where: (todo) => !todo.completed });
544
+ * active.results(); // [[id, todo], ...]
545
+ * active.onChange(() => console.log('Updated!'));
546
+ * active.dispose(); // Clean up when done
547
+ * ```
548
+ */
432
549
  query(config) {
433
550
  const query = {
434
551
  where: config.where,
@@ -1,17 +1,54 @@
1
- import { c as Collection, t as Plugin } from "../../store-Dc-hIF56.js";
1
+ import { c as Collection, t as Plugin } from "../../store-F8qPSj_p.js";
2
2
  import { Storage } from "unstorage";
3
3
 
4
4
  //#region src/plugins/unstorage/plugin.d.ts
5
5
  type MaybePromise<T> = T | Promise<T>;
6
6
  type UnstorageOnBeforeSet = (data: Collection) => MaybePromise<Collection>;
7
7
  type UnstorageOnAfterGet = (data: Collection) => MaybePromise<Collection>;
8
+ /**
9
+ * Configuration options for the unstorage persistence plugin.
10
+ */
8
11
  type UnstorageConfig = {
12
+ /** Delay in ms to collapse rapid mutations into a single write. Default: 0 (immediate) */
9
13
  debounceMs?: number;
14
+ /** Interval in ms to poll storage for external changes. When set, enables automatic sync. */
10
15
  pollIntervalMs?: number;
16
+ /** Hook invoked before persisting to storage. Use for encryption, compression, etc. */
11
17
  onBeforeSet?: UnstorageOnBeforeSet;
18
+ /** Hook invoked after loading from storage. Use for decryption, validation, etc. */
12
19
  onAfterGet?: UnstorageOnAfterGet;
20
+ /** Function that returns true to skip persistence operations. Use for conditional sync. */
13
21
  skip?: () => boolean;
14
22
  };
23
+ /**
24
+ * Persistence plugin for Starling using unstorage backends.
25
+ *
26
+ * Automatically persists store snapshots and optionally polls for external changes.
27
+ *
28
+ * @param key - Storage key for this dataset
29
+ * @param storage - Unstorage instance (localStorage, HTTP, filesystem, etc.)
30
+ * @param config - Optional configuration for debouncing, polling, hooks, and conditional sync
31
+ * @returns Plugin instance for store.use()
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * import { unstoragePlugin } from "@byearlybird/starling/plugin-unstorage";
36
+ * import { createStorage } from "unstorage";
37
+ * import localStorageDriver from "unstorage/drivers/localstorage";
38
+ *
39
+ * const store = await new Store<Todo>()
40
+ * .use(unstoragePlugin('todos', createStorage({
41
+ * driver: localStorageDriver({ base: 'app:' })
42
+ * }), {
43
+ * debounceMs: 300,
44
+ * pollIntervalMs: 5000,
45
+ * skip: () => !navigator.onLine
46
+ * }))
47
+ * .init();
48
+ * ```
49
+ *
50
+ * @see {@link ../../../../docs/plugins/unstorage.md} for detailed configuration guide
51
+ */
15
52
  declare function unstoragePlugin<T>(key: string, storage: Storage<Collection>, config?: UnstorageConfig): Plugin<T>;
16
53
  //#endregion
17
54
  export { type UnstorageConfig, unstoragePlugin };
@@ -1,27 +1,63 @@
1
1
  //#region src/plugins/unstorage/plugin.ts
2
+ /**
3
+ * Persistence plugin for Starling using unstorage backends.
4
+ *
5
+ * Automatically persists store snapshots and optionally polls for external changes.
6
+ *
7
+ * @param key - Storage key for this dataset
8
+ * @param storage - Unstorage instance (localStorage, HTTP, filesystem, etc.)
9
+ * @param config - Optional configuration for debouncing, polling, hooks, and conditional sync
10
+ * @returns Plugin instance for store.use()
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { unstoragePlugin } from "@byearlybird/starling/plugin-unstorage";
15
+ * import { createStorage } from "unstorage";
16
+ * import localStorageDriver from "unstorage/drivers/localstorage";
17
+ *
18
+ * const store = await new Store<Todo>()
19
+ * .use(unstoragePlugin('todos', createStorage({
20
+ * driver: localStorageDriver({ base: 'app:' })
21
+ * }), {
22
+ * debounceMs: 300,
23
+ * pollIntervalMs: 5000,
24
+ * skip: () => !navigator.onLine
25
+ * }))
26
+ * .init();
27
+ * ```
28
+ *
29
+ * @see {@link ../../../../docs/plugins/unstorage.md} for detailed configuration guide
30
+ */
2
31
  function unstoragePlugin(key, storage, config = {}) {
3
32
  const { debounceMs = 0, pollIntervalMs, onBeforeSet, onAfterGet, skip } = config;
4
33
  let debounceTimer = null;
5
34
  let pollInterval = null;
6
35
  let store = null;
36
+ let persistPromise = null;
7
37
  const persistSnapshot = async () => {
8
38
  if (!store) return;
9
39
  const data = store.collection();
10
40
  const persisted = onBeforeSet !== void 0 ? await onBeforeSet(data) : data;
11
41
  await storage.set(key, persisted);
12
42
  };
43
+ const runPersist = async () => {
44
+ debounceTimer = null;
45
+ persistPromise = persistSnapshot();
46
+ await persistPromise;
47
+ persistPromise = null;
48
+ };
13
49
  const schedulePersist = () => {
14
50
  if (skip?.()) return;
15
- const runPersist = () => {
16
- debounceTimer = null;
17
- persistSnapshot();
18
- };
19
51
  if (debounceMs === 0) {
20
- runPersist();
52
+ persistPromise = persistSnapshot().finally(() => {
53
+ persistPromise = null;
54
+ });
21
55
  return;
22
56
  }
23
57
  if (debounceTimer !== null) clearTimeout(debounceTimer);
24
- debounceTimer = setTimeout(runPersist, debounceMs);
58
+ debounceTimer = setTimeout(() => {
59
+ runPersist();
60
+ }, debounceMs);
25
61
  };
26
62
  const pollStorage = async () => {
27
63
  if (!store) return;
@@ -39,15 +75,17 @@ function unstoragePlugin(key, storage, config = {}) {
39
75
  pollStorage();
40
76
  }, pollIntervalMs);
41
77
  },
42
- onDispose: () => {
78
+ onDispose: async () => {
43
79
  if (debounceTimer !== null) {
44
80
  clearTimeout(debounceTimer);
45
81
  debounceTimer = null;
82
+ await runPersist();
46
83
  }
47
84
  if (pollInterval !== null) {
48
85
  clearInterval(pollInterval);
49
86
  pollInterval = null;
50
87
  }
88
+ if (persistPromise !== null) await persistPromise;
51
89
  store = null;
52
90
  },
53
91
  onAdd: () => {
@@ -0,0 +1,306 @@
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
+ /**
45
+ * Transform all values in a document using a provided function.
46
+ *
47
+ * Useful for custom serialization in plugin hooks (encryption, compression, etc.)
48
+ *
49
+ * @param doc - Document to transform
50
+ * @param process - Function to apply to each leaf value
51
+ * @returns New document with transformed values
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * // Encrypt all values before persisting
56
+ * const encrypted = processDocument(doc, (value) => ({
57
+ * ...value,
58
+ * "~value": encrypt(value["~value"])
59
+ * }));
60
+ * ```
61
+ */
62
+ declare function processDocument(doc: EncodedDocument, process: (value: EncodedValue<unknown>) => EncodedValue<unknown>): EncodedDocument;
63
+ //#endregion
64
+ //#region src/crdt/collection.d.ts
65
+ /**
66
+ * A collection represents the complete state of a store:
67
+ * - A set of documents (including soft-deleted ones)
68
+ * - The highest eventstamp observed across all operations
69
+ *
70
+ * Collections are the unit of synchronization between store replicas.
71
+ */
72
+ type Collection = {
73
+ /** Array of encoded documents with eventstamps and metadata */
74
+ "~docs": EncodedDocument[];
75
+ /** Latest eventstamp observed by this collection for clock synchronization */
76
+ "~eventstamp": string;
77
+ };
78
+ //#endregion
79
+ //#region src/store.d.ts
80
+ type NotPromise<T> = T extends Promise<any> ? never : T;
81
+ type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;
82
+ /**
83
+ * Options for adding documents to the store.
84
+ */
85
+ type StoreAddOptions = {
86
+ /** Provide a custom ID instead of generating one */
87
+ withId?: string;
88
+ };
89
+ /**
90
+ * Configuration options for creating a Store instance.
91
+ */
92
+ type StoreConfig = {
93
+ /** Custom ID generator. Defaults to crypto.randomUUID() */
94
+ getId?: () => string;
95
+ };
96
+ /**
97
+ * Transaction context for batching multiple operations with rollback support.
98
+ *
99
+ * Transactions allow you to group multiple mutations together and optionally
100
+ * abort all changes if validation fails.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * store.begin((tx) => {
105
+ * const id = tx.add({ name: 'Alice' });
106
+ * if (!isValid(tx.get(id))) {
107
+ * tx.rollback(); // Abort all changes
108
+ * }
109
+ * });
110
+ * ```
111
+ */
112
+ type StoreSetTransaction<T> = {
113
+ /** Add a document and return its ID */
114
+ add: (value: T, options?: StoreAddOptions) => string;
115
+ /** Update a document with a partial value (field-level merge) */
116
+ update: (key: string, value: DeepPartial<T>) => void;
117
+ /** Merge an encoded document (used by sync/persistence plugins) */
118
+ merge: (doc: EncodedDocument) => void;
119
+ /** Soft-delete a document */
120
+ del: (key: string) => void;
121
+ /** Get a document within this transaction */
122
+ get: (key: string) => T | null;
123
+ /** Abort the transaction and discard all changes */
124
+ rollback: () => void;
125
+ };
126
+ /**
127
+ * Plugin interface for extending store behavior with persistence, analytics, etc.
128
+ *
129
+ * All hooks are optional. Mutation hooks receive batched entries after each
130
+ * transaction commits.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * const loggingPlugin: Plugin<Todo> = {
135
+ * onInit: async () => console.log('Store initialized'),
136
+ * onAdd: (entries) => console.log('Added:', entries.length),
137
+ * onDispose: async () => console.log('Store disposed')
138
+ * };
139
+ * ```
140
+ */
141
+ type Plugin<T> = {
142
+ /** Called once when store.init() runs */
143
+ onInit: (store: Store<T>) => Promise<void> | void;
144
+ /** Called once when store.dispose() runs */
145
+ onDispose: () => Promise<void> | void;
146
+ /** Called after documents are added (batched per transaction) */
147
+ onAdd?: (entries: ReadonlyArray<readonly [string, T]>) => void;
148
+ /** Called after documents are updated (batched per transaction) */
149
+ onUpdate?: (entries: ReadonlyArray<readonly [string, T]>) => void;
150
+ /** Called after documents are deleted (batched per transaction) */
151
+ onDelete?: (keys: ReadonlyArray<string>) => void;
152
+ };
153
+ /**
154
+ * Configuration for creating a reactive query.
155
+ *
156
+ * Queries automatically update when matching documents change.
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * const config: QueryConfig<Todo> = {
161
+ * where: (todo) => !todo.completed,
162
+ * select: (todo) => todo.text,
163
+ * order: (a, b) => a.localeCompare(b)
164
+ * };
165
+ * ```
166
+ */
167
+ type QueryConfig<T, U = T> = {
168
+ /** Filter predicate - return true to include document in results */
169
+ where: (data: T) => boolean;
170
+ /** Optional projection - transform documents before returning */
171
+ select?: (data: T) => U;
172
+ /** Optional comparator for stable ordering of results */
173
+ order?: (a: U, b: U) => number;
174
+ };
175
+ /**
176
+ * A reactive query handle that tracks matching documents and notifies on changes.
177
+ *
178
+ * Call `dispose()` when done to clean up listeners and remove from the store.
179
+ */
180
+ type Query<U> = {
181
+ /** Get current matching documents as [id, document] tuples */
182
+ results: () => Array<readonly [string, U]>;
183
+ /** Register a change listener. Returns unsubscribe function. */
184
+ onChange: (callback: () => void) => () => void;
185
+ /** Remove this query from the store and clear all listeners */
186
+ dispose: () => void;
187
+ };
188
+ /**
189
+ * Lightweight local-first data store with built-in sync and reactive queries.
190
+ *
191
+ * Stores plain JavaScript objects with automatic field-level conflict resolution
192
+ * using Last-Write-Wins semantics powered by hybrid logical clocks.
193
+ *
194
+ * @template T - The type of documents stored in this collection
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * const store = await new Store<{ text: string; completed: boolean }>()
199
+ * .use(unstoragePlugin('todos', storage))
200
+ * .init();
201
+ *
202
+ * // Add, update, delete
203
+ * const id = store.add({ text: 'Buy milk', completed: false });
204
+ * store.update(id, { completed: true });
205
+ * store.del(id);
206
+ *
207
+ * // Reactive queries
208
+ * const activeTodos = store.query({ where: (todo) => !todo.completed });
209
+ * activeTodos.onChange(() => console.log('Todos changed!'));
210
+ * ```
211
+ */
212
+ declare class Store<T> {
213
+ #private;
214
+ constructor(config?: StoreConfig);
215
+ /**
216
+ * Get a document by ID.
217
+ * @returns The document, or null if not found or deleted
218
+ */
219
+ get(key: string): T | null;
220
+ /**
221
+ * Iterate over all non-deleted documents as [id, document] tuples.
222
+ */
223
+ entries(): IterableIterator<readonly [string, T]>;
224
+ /**
225
+ * Get the complete store state as a Collection for persistence or sync.
226
+ * @returns Collection containing all documents and the latest eventstamp
227
+ */
228
+ collection(): Collection;
229
+ /**
230
+ * Merge a collection from storage or another replica using field-level LWW.
231
+ * @param collection - Collection from storage or another store instance
232
+ */
233
+ merge(collection: Collection): void;
234
+ /**
235
+ * Run multiple operations in a transaction with rollback support.
236
+ *
237
+ * @param callback - Function receiving a transaction context
238
+ * @param opts - Optional config. Use `silent: true` to skip plugin hooks.
239
+ * @returns The callback's return value
240
+ *
241
+ * @example
242
+ * ```ts
243
+ * const id = store.begin((tx) => {
244
+ * const newId = tx.add({ text: 'Buy milk' });
245
+ * tx.update(newId, { priority: 'high' });
246
+ * return newId; // Return value becomes begin()'s return value
247
+ * });
248
+ * ```
249
+ */
250
+ begin<R = void>(callback: (tx: StoreSetTransaction<T>) => NotPromise<R>, opts?: {
251
+ silent?: boolean;
252
+ }): NotPromise<R>;
253
+ /**
254
+ * Add a document to the store.
255
+ * @returns The document's ID (generated or provided via options)
256
+ */
257
+ add(value: T, options?: StoreAddOptions): string;
258
+ /**
259
+ * Update a document with a partial value.
260
+ *
261
+ * Uses field-level merge - only specified fields are updated.
262
+ */
263
+ update(key: string, value: DeepPartial<T>): void;
264
+ /**
265
+ * Soft-delete a document.
266
+ *
267
+ * Deleted docs remain in snapshots for sync purposes but are
268
+ * excluded from queries and reads.
269
+ */
270
+ del(key: string): void;
271
+ /**
272
+ * Register a plugin for persistence, analytics, etc.
273
+ * @returns This store instance for chaining
274
+ */
275
+ use(plugin: Plugin<T>): this;
276
+ /**
277
+ * Initialize the store and run plugin onInit hooks.
278
+ *
279
+ * Must be called before using the store. Runs plugin setup (hydrate
280
+ * snapshots, start pollers, etc.) and hydrates existing queries.
281
+ *
282
+ * @returns This store instance for chaining
283
+ */
284
+ init(): Promise<this>;
285
+ /**
286
+ * Dispose the store and run plugin cleanup.
287
+ *
288
+ * Flushes pending operations, clears queries, and runs plugin teardown.
289
+ * Call when shutting down to avoid memory leaks.
290
+ */
291
+ dispose(): Promise<void>;
292
+ /**
293
+ * Create a reactive query that auto-updates when matching docs change.
294
+ *
295
+ * @example
296
+ * ```ts
297
+ * const active = store.query({ where: (todo) => !todo.completed });
298
+ * active.results(); // [[id, todo], ...]
299
+ * active.onChange(() => console.log('Updated!'));
300
+ * active.dispose(); // Clean up when done
301
+ * ```
302
+ */
303
+ query<U = T>(config: QueryConfig<T, U>): Query<U>;
304
+ }
305
+ //#endregion
306
+ export { StoreAddOptions as a, Collection as c, EncodedRecord as d, EncodedValue as f, 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.9.0",
3
+ "version": "0.9.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -1,114 +0,0 @@
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 };