@byearlybird/starling 0.9.2 → 0.10.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/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@byearlybird/starling",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
+ "description": "Lightweight local-first data store with automatic cross-device sync. Plain JavaScript queries, framework-agnostic, zero dependencies. Offline-first apps without the complexity, in just 4KB.",
4
5
  "type": "module",
5
6
  "license": "MIT",
6
7
  "main": "./dist/index.js",
@@ -10,11 +11,6 @@
10
11
  "types": "./dist/index.d.ts",
11
12
  "import": "./dist/index.js",
12
13
  "default": "./dist/index.js"
13
- },
14
- "./plugin-unstorage": {
15
- "types": "./dist/plugins/unstorage/plugin.d.ts",
16
- "import": "./dist/plugins/unstorage/plugin.js",
17
- "default": "./dist/plugins/unstorage/plugin.js"
18
14
  }
19
15
  },
20
16
  "files": [
@@ -24,14 +20,6 @@
24
20
  "build": "bun run build.ts",
25
21
  "prepublishOnly": "bun run build.ts"
26
22
  },
27
- "peerDependencies": {
28
- "unstorage": "^1.17.1"
29
- },
30
- "peerDependenciesMeta": {
31
- "unstorage": {
32
- "optional": true
33
- }
34
- },
35
23
  "publishConfig": {
36
24
  "access": "public"
37
25
  }
@@ -1,54 +0,0 @@
1
- import { c as Collection, t as Plugin } from "../../store-F8qPSj_p.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
- /**
9
- * Configuration options for the unstorage persistence plugin.
10
- */
11
- type UnstorageConfig = {
12
- /** Delay in ms to collapse rapid mutations into a single write. Default: 0 (immediate) */
13
- debounceMs?: number;
14
- /** Interval in ms to poll storage for external changes. When set, enables automatic sync. */
15
- pollIntervalMs?: number;
16
- /** Hook invoked before persisting to storage. Use for encryption, compression, etc. */
17
- onBeforeSet?: UnstorageOnBeforeSet;
18
- /** Hook invoked after loading from storage. Use for decryption, validation, etc. */
19
- onAfterGet?: UnstorageOnAfterGet;
20
- /** Function that returns true to skip persistence operations. Use for conditional sync. */
21
- skip?: () => boolean;
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
- */
52
- declare function unstoragePlugin<T>(key: string, storage: Storage<Collection>, config?: UnstorageConfig): Plugin<T>;
53
- //#endregion
54
- export { type UnstorageConfig, unstoragePlugin };
@@ -1,104 +0,0 @@
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
- */
31
- function unstoragePlugin(key, storage, config = {}) {
32
- const { debounceMs = 0, pollIntervalMs, onBeforeSet, onAfterGet, skip } = config;
33
- let debounceTimer = null;
34
- let pollInterval = null;
35
- let store = null;
36
- let persistPromise = null;
37
- const persistSnapshot = async () => {
38
- if (!store) return;
39
- const data = store.collection();
40
- const persisted = onBeforeSet !== void 0 ? await onBeforeSet(data) : data;
41
- await storage.set(key, persisted);
42
- };
43
- const runPersist = async () => {
44
- debounceTimer = null;
45
- persistPromise = persistSnapshot();
46
- await persistPromise;
47
- persistPromise = null;
48
- };
49
- const schedulePersist = () => {
50
- if (skip?.()) return;
51
- if (debounceMs === 0) {
52
- persistPromise = persistSnapshot().finally(() => {
53
- persistPromise = null;
54
- });
55
- return;
56
- }
57
- if (debounceTimer !== null) clearTimeout(debounceTimer);
58
- debounceTimer = setTimeout(() => {
59
- runPersist();
60
- }, debounceMs);
61
- };
62
- const pollStorage = async () => {
63
- if (!store) return;
64
- if (skip?.()) return;
65
- const persisted = await storage.get(key);
66
- if (!persisted) return;
67
- const data = onAfterGet !== void 0 ? await onAfterGet(persisted) : persisted;
68
- store.merge(data);
69
- };
70
- return {
71
- onInit: async (s) => {
72
- store = s;
73
- await pollStorage();
74
- if (pollIntervalMs !== void 0 && pollIntervalMs > 0) pollInterval = setInterval(() => {
75
- pollStorage();
76
- }, pollIntervalMs);
77
- },
78
- onDispose: async () => {
79
- if (debounceTimer !== null) {
80
- clearTimeout(debounceTimer);
81
- debounceTimer = null;
82
- await runPersist();
83
- }
84
- if (pollInterval !== null) {
85
- clearInterval(pollInterval);
86
- pollInterval = null;
87
- }
88
- if (persistPromise !== null) await persistPromise;
89
- store = null;
90
- },
91
- onAdd: () => {
92
- schedulePersist();
93
- },
94
- onUpdate: () => {
95
- schedulePersist();
96
- },
97
- onDelete: () => {
98
- schedulePersist();
99
- }
100
- };
101
- }
102
-
103
- //#endregion
104
- export { unstoragePlugin };
@@ -1,306 +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
- /**
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 };