@byearlybird/starling 0.1.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.
Files changed (52) hide show
  1. package/README.md +375 -0
  2. package/dist/core/crdt/clock.d.ts +15 -0
  3. package/dist/core/crdt/clock.d.ts.map +1 -0
  4. package/dist/core/crdt/operations.d.ts +8 -0
  5. package/dist/core/crdt/operations.d.ts.map +1 -0
  6. package/dist/core/index.d.ts +5 -0
  7. package/dist/core/index.d.ts.map +1 -0
  8. package/dist/core/index.js +454 -0
  9. package/dist/core/shared/types.d.ts +25 -0
  10. package/dist/core/shared/types.d.ts.map +1 -0
  11. package/dist/core/shared/utils.d.ts +5 -0
  12. package/dist/core/shared/utils.d.ts.map +1 -0
  13. package/dist/core/store/mutations.d.ts +16 -0
  14. package/dist/core/store/mutations.d.ts.map +1 -0
  15. package/dist/core/store/query.d.ts +16 -0
  16. package/dist/core/store/query.d.ts.map +1 -0
  17. package/dist/core/store/store.d.ts +77 -0
  18. package/dist/core/store/store.d.ts.map +1 -0
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +454 -0
  22. package/dist/plugins/index.d.ts +3 -0
  23. package/dist/plugins/index.d.ts.map +1 -0
  24. package/dist/plugins/index.js +188 -0
  25. package/dist/plugins/persistence/index.d.ts +2 -0
  26. package/dist/plugins/persistence/index.d.ts.map +1 -0
  27. package/dist/plugins/persistence/unstorage-plugin.d.ts +5 -0
  28. package/dist/plugins/persistence/unstorage-plugin.d.ts.map +1 -0
  29. package/dist/plugins/push-pull-plugin.d.ts +13 -0
  30. package/dist/plugins/push-pull-plugin.d.ts.map +1 -0
  31. package/dist/plugins/sync/index.d.ts +3 -0
  32. package/dist/plugins/sync/index.d.ts.map +1 -0
  33. package/dist/plugins/sync/index.js +65 -0
  34. package/dist/plugins/sync/push-pull-plugin.d.ts +13 -0
  35. package/dist/plugins/sync/push-pull-plugin.d.ts.map +1 -0
  36. package/dist/plugins/unstorage-plugin.d.ts +5 -0
  37. package/dist/plugins/unstorage-plugin.d.ts.map +1 -0
  38. package/dist/react/index.d.ts +3 -0
  39. package/dist/react/index.d.ts.map +1 -0
  40. package/dist/react/index.js +893 -0
  41. package/dist/react/use-data.d.ts +9 -0
  42. package/dist/react/use-data.d.ts.map +1 -0
  43. package/dist/react/use-query.d.ts +7 -0
  44. package/dist/react/use-query.d.ts.map +1 -0
  45. package/dist/solid/index.d.ts +3 -0
  46. package/dist/solid/index.d.ts.map +1 -0
  47. package/dist/solid/index.js +299 -0
  48. package/dist/solid/use-data.d.ts +3 -0
  49. package/dist/solid/use-data.d.ts.map +1 -0
  50. package/dist/solid/use-query.d.ts +3 -0
  51. package/dist/solid/use-query.d.ts.map +1 -0
  52. package/package.json +65 -0
package/README.md ADDED
@@ -0,0 +1,375 @@
1
+ # @byearlybird/starling
2
+
3
+ A reactive, framework-agnostic data synchronization library with CRDT-like merge capabilities. Starling provides a simple yet powerful way to manage, query, and synchronize application state across clients and servers with automatic conflict resolution.
4
+
5
+ ## Features
6
+
7
+ - **Reactive Stores**: Event-driven data stores with automatic change notifications
8
+ - **Query System**: Predicate-based filtering with reactive updates
9
+ - **CRDT-like Merging**: Conflict-free state synchronization using eventstamps (ULID-based monotonic timestamps)
10
+ - **HTTP Synchronization**: Bidirectional client-server sync with customizable push/pull strategies
11
+ - **Framework Agnostic**: Works standalone or with React and Solid via dedicated hooks
12
+ - **Storage Abstraction**: Powered by `unstorage` for flexible persistence (localStorage, filesystem, Redis, etc.)
13
+ - **TypeScript First**: Full type safety with strict TypeScript support
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ # npm
19
+ npm install @byearlybird/starling unstorage
20
+
21
+ # bun
22
+ bun add @byearlybird/starling unstorage
23
+
24
+ # yarn
25
+ yarn add @byearlybird/starling unstorage
26
+ ```
27
+
28
+ ### Optional Framework Dependencies
29
+
30
+ For React:
31
+ ```bash
32
+ npm install react@^19 react-dom@^19
33
+ ```
34
+
35
+ For Solid:
36
+ ```bash
37
+ npm install solid-js@^1.9.9
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```typescript
43
+ import { createStore } from "@byearlybird/starling";
44
+ import { createStorage } from "unstorage";
45
+
46
+ // Create a store
47
+ const todoStore = createStore<{ text: string; completed: boolean }>("todos", {
48
+ storage: createStorage(),
49
+ });
50
+
51
+ // Insert items
52
+ await todoStore.insert("todo-1", {
53
+ text: "Learn Starling",
54
+ completed: false,
55
+ });
56
+
57
+ // Update items (supports partial updates)
58
+ await todoStore.update("todo-1", { completed: true });
59
+
60
+ // Get all values
61
+ const todos = await todoStore.values();
62
+ console.log(todos); // { "todo-1": { text: "Learn Starling", completed: true, __eventstamp: "..." } }
63
+
64
+ // Listen to changes
65
+ const unsubscribe = todoStore.on("update", (updates) => {
66
+ console.log("Updated:", updates);
67
+ });
68
+ ```
69
+
70
+ ## Core API
71
+
72
+ ### Creating a Store
73
+
74
+ ```typescript
75
+ import { createStore } from "@byearlybird/starling";
76
+ import { createStorage } from "unstorage";
77
+ import localStorageDriver from "unstorage/drivers/localstorage";
78
+
79
+ const store = createStore<YourType>("collectionName", {
80
+ storage: createStorage({
81
+ driver: localStorageDriver({ base: "app:" }),
82
+ }),
83
+ });
84
+ ```
85
+
86
+ ### Store Methods
87
+
88
+ #### `insert(key: string, value: T): Promise<void>`
89
+ Insert a new item into the store. Each value is automatically encoded with an `__eventstamp` for conflict resolution.
90
+
91
+ ```typescript
92
+ await store.insert("user-1", { name: "Alice", email: "alice@example.com" });
93
+ ```
94
+
95
+ #### `update(key: string, value: DeepPartial<T>): Promise<void>`
96
+ Update an existing item with partial data. Supports nested updates via dot notation.
97
+
98
+ ```typescript
99
+ await store.update("user-1", { email: "alice@newdomain.com" });
100
+ ```
101
+
102
+ #### `values(): Promise<Record<string, T>>`
103
+ Get all decoded values from the store.
104
+
105
+ ```typescript
106
+ const allUsers = await store.values();
107
+ ```
108
+
109
+ #### `state(): Promise<EncodedRecord>`
110
+ Get the raw encoded state with eventstamps (useful for synchronization).
111
+
112
+ ```typescript
113
+ const encodedState = await store.state();
114
+ ```
115
+
116
+ #### `mergeState(data: EncodedRecord): Promise<void>`
117
+ Merge external state into the store. Conflicts are resolved using eventstamp comparison (Last-Write-Wins).
118
+
119
+ ```typescript
120
+ await store.mergeState(incomingState);
121
+ ```
122
+
123
+ ### Store Events
124
+
125
+ Subscribe to store changes using the event emitter:
126
+
127
+ ```typescript
128
+ // Listen for new insertions
129
+ store.on("insert", (items) => {
130
+ items.forEach(({ key, value }) => console.log(`Inserted: ${key}`, value));
131
+ });
132
+
133
+ // Listen for updates
134
+ store.on("update", (items) => {
135
+ items.forEach(({ key, value }) => console.log(`Updated: ${key}`, value));
136
+ });
137
+
138
+ // Listen for any mutation (insert or update)
139
+ store.on("mutate", () => {
140
+ console.log("Store has changed");
141
+ });
142
+
143
+ // Unsubscribe
144
+ const unsubscribe = store.on("update", callback);
145
+ unsubscribe();
146
+ ```
147
+
148
+ ## Queries
149
+
150
+ Queries provide reactive, filtered views of store data.
151
+
152
+ ```typescript
153
+ import { createQuery } from "@byearlybird/starling";
154
+
155
+ const query = createQuery(
156
+ todoStore,
157
+ (todo) => !todo.completed // Predicate function
158
+ );
159
+
160
+ // Listen for initial data load
161
+ query.on("init", (todos) => {
162
+ console.log("Initial todos:", todos);
163
+ });
164
+
165
+ // Listen for changes
166
+ query.on("change", (todos) => {
167
+ console.log("Todos updated:", todos);
168
+ });
169
+
170
+ // Clean up
171
+ query.dispose();
172
+ ```
173
+
174
+ ## Synchronization
175
+
176
+ Starling provides an HTTP synchronizer for bidirectional client-server sync.
177
+
178
+ ```typescript
179
+ import { createHttpSynchronizer } from "@byearlybird/starling/sync";
180
+
181
+ const sync = createHttpSynchronizer(todoStore, {
182
+ pullInterval: 5000, // Pull from server every 5 seconds
183
+
184
+ // Push local changes to server
185
+ push: async (data) => {
186
+ await fetch("/api/todos", {
187
+ method: "PUT",
188
+ headers: { "Content-Type": "application/json" },
189
+ body: JSON.stringify({ todos: data }),
190
+ });
191
+ },
192
+
193
+ // Pull remote changes from server
194
+ pull: async () => {
195
+ const response = await fetch("/api/todos");
196
+ const { todos } = await response.json();
197
+ return todos;
198
+ },
199
+
200
+ // Optional: preprocess data before merging (e.g., encryption)
201
+ preprocess: (data) => {
202
+ // Transform or decrypt data before merging
203
+ return data;
204
+ },
205
+ });
206
+
207
+ // Start synchronization
208
+ await sync.start();
209
+
210
+ // Stop synchronization
211
+ sync.stop();
212
+ ```
213
+
214
+ ### Server-Side Merging
215
+
216
+ On the server, use `mergeState` to handle incoming updates:
217
+
218
+ ```typescript
219
+ // Server endpoint (e.g., using Hono, Express, etc.)
220
+ app.put("/api/todos", async (c) => {
221
+ const { todos } = await c.req.json();
222
+
223
+ // Merge client state into server store
224
+ await serverTodoStore.mergeState(todos);
225
+
226
+ return c.json({ success: true });
227
+ });
228
+
229
+ app.get("/api/todos", async (c) => {
230
+ const state = await serverTodoStore.state();
231
+ return c.json({ todos: state });
232
+ });
233
+ ```
234
+
235
+ ## Framework Bindings
236
+
237
+ ### React
238
+
239
+ ```typescript
240
+ import { useData, useQuery } from "@byearlybird/starling/react";
241
+
242
+ function TodoList() {
243
+ // Get all data from store
244
+ const { data: allTodos, isLoading } = useData(todoStore);
245
+
246
+ // Or use a query for filtered data
247
+ const { data: activeTodos, isLoading } = useQuery(
248
+ todoStore,
249
+ (todo) => !todo.completed,
250
+ [] // dependency array (like useEffect)
251
+ );
252
+
253
+ if (isLoading) return <div>Loading...</div>;
254
+
255
+ return (
256
+ <ul>
257
+ {Object.entries(activeTodos).map(([id, todo]) => (
258
+ <li key={id}>{todo.text}</li>
259
+ ))}
260
+ </ul>
261
+ );
262
+ }
263
+ ```
264
+
265
+ ### Solid
266
+
267
+ ```typescript
268
+ import { useData, useQuery } from "@byearlybird/starling/solid";
269
+
270
+ function TodoList() {
271
+ // Get all data from store
272
+ const { data: allTodos, isLoading } = useData(todoStore);
273
+
274
+ // Or use a query for filtered data
275
+ const { data: activeTodos, isLoading } = useQuery(
276
+ todoStore,
277
+ (todo) => !todo.completed
278
+ );
279
+
280
+ return (
281
+ <Show when={!isLoading()} fallback={<div>Loading...</div>}>
282
+ <ul>
283
+ <For each={Object.entries(activeTodos())}>
284
+ {([id, todo]) => <li>{todo.text}</li>}
285
+ </For>
286
+ </ul>
287
+ </Show>
288
+ );
289
+ }
290
+ ```
291
+
292
+ ## Architecture
293
+
294
+ ### Eventstamps
295
+
296
+ Every value in Starling is encoded with an `__eventstamp` field containing a ULID (Universally Unique Lexicographically Sortable Identifier). This enables:
297
+
298
+ - **Monotonic timestamps**: Later events always have higher eventstamps
299
+ - **Conflict resolution**: When two clients update the same field, the update with the higher eventstamp wins (Last-Write-Wins)
300
+ - **Distributed consistency**: Multiple clients can sync without coordination
301
+
302
+ ### CRDT-like Merging
303
+
304
+ When merging states, Starling compares eventstamps at the field level:
305
+
306
+ ```typescript
307
+ // Client A updates
308
+ { name: "Alice", email: "alice@old.com", __eventstamp: "01H..." }
309
+
310
+ // Client B updates (newer eventstamp for email only)
311
+ {
312
+ name: { value: "Alice", __eventstamp: "01H..." },
313
+ email: { value: "alice@new.com", __eventstamp: "01J..." }
314
+ }
315
+
316
+ // Merged result: email takes precedence due to higher eventstamp
317
+ { name: "Alice", email: "alice@new.com" }
318
+ ```
319
+
320
+ ## Package Exports
321
+
322
+ Starling provides multiple entry points for different use cases:
323
+
324
+ - `@byearlybird/starling` - Core library (stores, queries, operations)
325
+ - `@byearlybird/starling/react` - React hooks (`useData`, `useQuery`)
326
+ - `@byearlybird/starling/solid` - Solid hooks (`useData`, `useQuery`)
327
+ - `@byearlybird/starling/sync` - HTTP synchronizer
328
+
329
+ ## Development
330
+
331
+ ### Running Tests
332
+
333
+ ```bash
334
+ bun test
335
+
336
+ # Watch mode
337
+ bun test --watch
338
+
339
+ # Specific test file
340
+ bun test lib/core/store.test.ts
341
+ ```
342
+
343
+ ### Linting and Formatting
344
+
345
+ ```bash
346
+ # Check code
347
+ bun biome check .
348
+
349
+ # Format code
350
+ bun biome format --write .
351
+
352
+ # Lint code
353
+ bun biome lint .
354
+ ```
355
+
356
+ ### Running Demo Apps
357
+
358
+ ```bash
359
+ # Start demo server (port 3000)
360
+ cd demo-server && bun run index.ts
361
+
362
+ # Start React demo (separate terminal)
363
+ cd demo-react && bun run dev
364
+
365
+ # Start Solid demo (separate terminal)
366
+ cd demo-solid && bun run dev
367
+ ```
368
+
369
+ ## License
370
+
371
+ MIT
372
+
373
+ ## Credits
374
+
375
+ Built with [Bun](https://bun.sh) by [@byearlybird](https://github.com/byearlybird)
@@ -0,0 +1,15 @@
1
+ declare const formatEventstamp: (timestampMs: number, counter: number) => string;
2
+ declare const parseEventstamp: (eventstamp: string) => {
3
+ timestampMs: number;
4
+ counter: number;
5
+ };
6
+ declare const createClock: () => {
7
+ /**
8
+ * Returns the next monotonically increasing eventstamp.
9
+ */
10
+ now(): string;
11
+ latest(): string;
12
+ forward(eventstamp: string): void;
13
+ };
14
+ export { createClock, formatEventstamp, parseEventstamp };
15
+ //# sourceMappingURL=clock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clock.d.ts","sourceRoot":"","sources":["../../../lib/core/crdt/clock.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,gBAAgB,GAAI,aAAa,MAAM,EAAE,SAAS,MAAM,KAAG,MAGhE,CAAC;AAEF,QAAA,MAAM,eAAe,GACpB,YAAY,MAAM,KAChB;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CASxC,CAAC;AAEF,QAAA,MAAM,WAAW;IAKf;;OAEG;WACI,MAAM;cAaH,MAAM;wBAII,MAAM,GAAG,IAAI;CASlC,CAAC;AAEF,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,eAAe,EAAE,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { ArrayKV, EncodedObject, EventstampFn } from "@core/shared/types";
2
+ export declare function encode<T extends object>(obj: T, eventstamp: string): EncodedObject;
3
+ export declare function decode<T extends object>(obj: EncodedObject): T;
4
+ export declare function merge(obj1: EncodedObject, obj2: EncodedObject): [EncodedObject, boolean];
5
+ export declare function mergeArray(current: ArrayKV<EncodedObject>, updates: ArrayKV<EncodedObject>): [ArrayKV<EncodedObject>, boolean];
6
+ export declare const encodeMany: <TValue extends object>(data: ArrayKV<TValue>, eventstampFn: EventstampFn) => ArrayKV<EncodedObject>;
7
+ export declare const decodeMany: <TValue extends object>(data: ArrayKV<EncodedObject>) => ArrayKV<TValue>;
8
+ //# sourceMappingURL=operations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operations.d.ts","sourceRoot":"","sources":["../../../lib/core/crdt/operations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE/E,wBAAgB,MAAM,CAAC,CAAC,SAAS,MAAM,EACtC,GAAG,EAAE,CAAC,EACN,UAAU,EAAE,MAAM,GAChB,aAAa,CA+Bf;AAED,wBAAgB,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,aAAa,GAAG,CAAC,CA4B9D;AAED,wBAAgB,KAAK,CACpB,IAAI,EAAE,aAAa,EACnB,IAAI,EAAE,aAAa,GACjB,CAAC,aAAa,EAAE,OAAO,CAAC,CAmE1B;AAED,wBAAgB,UAAU,CACzB,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,EAC/B,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,GAC7B,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC,CAiCnC;AAED,eAAO,MAAM,UAAU,GAAI,MAAM,SAAS,MAAM,EAC/C,MAAM,OAAO,CAAC,MAAM,CAAC,EACrB,cAAc,YAAY,KACxB,OAAO,CAAC,aAAa,CACsD,CAAC;AAE/E,eAAO,MAAM,UAAU,GAAI,MAAM,SAAS,MAAM,EAC/C,MAAM,OAAO,CAAC,aAAa,CAAC,KAC1B,OAAO,CAAC,MAAM,CACqD,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { decode, encode, merge, mergeArray } from "./crdt/operations";
2
+ export * from "./shared/types";
3
+ export type { Store } from "./store/store";
4
+ export { createStore } from "./store/store";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../lib/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACtE,cAAc,gBAAgB,CAAC;AAC/B,YAAY,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}