@byearlybird/starling 0.9.0 → 0.9.1
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 +2 -2
- package/dist/index.js +117 -0
- package/dist/plugins/unstorage/plugin.d.ts +38 -1
- package/dist/plugins/unstorage/plugin.js +29 -0
- package/dist/store-F8qPSj_p.d.ts +306 -0
- package/package.json +1 -1
- package/dist/store-Dc-hIF56.d.ts +0 -114
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-
|
|
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-
|
|
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,4 +1,33 @@
|
|
|
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;
|
|
@@ -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
package/dist/store-Dc-hIF56.d.ts
DELETED
|
@@ -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 };
|