@byearlybird/starling 0.13.2 → 0.14.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/README.md CHANGED
@@ -1,15 +1,15 @@
1
1
  # Starling
2
2
 
3
- A mergable data store for building local-first apps that sync.
3
+ Conflict-free replicated state for JavaScript. Bring your own reactivity.
4
4
 
5
- Starling lets you store data in memory with a fast, synchronous API. The merge system is fully built and ready—persistence and sync features are coming soon. When you need to sync with other devices or users, it automatically merges changes and resolves conflicts.
5
+ Starling is a CRDT (conflict-free replicated data type) library that provides automatic conflict resolution for distributed data. It manages state with Last-Write-Wins semantics using hybrid logical clocks, giving you a solid foundation for building local-first, collaborative applications.
6
6
 
7
7
  ## Installation
8
8
 
9
9
  ```bash
10
10
  npm install @byearlybird/starling
11
11
  # or
12
- bun add @byearlybird/starling
12
+ pnpm add @byearlybird/starling
13
13
  ```
14
14
 
15
15
  Requires TypeScript 5 or higher.
@@ -31,19 +31,19 @@ const store = createStore({
31
31
  },
32
32
  });
33
33
 
34
- store.users.add({ id: "1", name: "Alice" });
35
- const user = store.users.get("1"); // { id: "1", name: "Alice" }
34
+ store.add("users", { id: "1", name: "Alice" });
35
+ const user = store.get("users", "1"); // { id: "1", name: "Alice" }
36
36
  ```
37
37
 
38
38
  ## Features
39
39
 
40
- - **In-memory and fast**: All operations are synchronous for maximum performance
41
- - **Built for local-first**: API is ready for persistence and sync (coming soon)
42
- - **Works with any standard schema library**: Zod, Valibot, ArkType, and more
43
- - **Automatic conflict resolution**: When merging changes, conflicts are resolved automatically
44
- - **Reactive updates**: Data changes trigger updates automatically
40
+ - **CRDT-based**: Automatic conflict resolution with Last-Write-Wins semantics
41
+ - **Fast and synchronous**: All operations are in-memory and synchronous
42
+ - **Framework agnostic**: Works with React, Vue, Svelte, or vanilla JS
45
43
  - **Type-safe**: Full TypeScript support with type inference
44
+ - **Schema validation**: Works with Zod, Valibot, ArkType, and more
46
45
  - **Merge snapshots**: Sync data between devices or users easily
46
+ - **Change events**: Listen to data changes and integrate with your reactive system
47
47
 
48
48
  ## Basic Usage
49
49
 
@@ -79,7 +79,7 @@ const store = createStore({
79
79
  Add new items to a collection with `add()`:
80
80
 
81
81
  ```typescript
82
- store.users.add({
82
+ store.add("users", {
83
83
  id: "1",
84
84
  name: "Alice",
85
85
  email: "alice@example.com",
@@ -91,7 +91,7 @@ store.users.add({
91
91
  Update existing items with `update()`:
92
92
 
93
93
  ```typescript
94
- store.users.update("1", {
94
+ store.update("users", "1", {
95
95
  email: "newemail@example.com",
96
96
  });
97
97
  ```
@@ -101,71 +101,57 @@ store.users.update("1", {
101
101
  Remove items with `remove()`:
102
102
 
103
103
  ```typescript
104
- store.users.remove("1");
104
+ store.remove("users", "1");
105
105
  ```
106
106
 
107
107
  ### Reading Data
108
108
 
109
- Collections work like maps. You can read data in several ways:
109
+ The store provides simple getter methods:
110
110
 
111
111
  ```typescript
112
112
  // Get a single item
113
- const user = store.users.get("1");
113
+ const user = store.get("users", "1");
114
114
 
115
- // Check if an item exists
116
- if (store.users.has("1")) {
117
- // ...
118
- }
119
-
120
- // Get all items
121
- for (const [id, user] of store.users.entries()) {
122
- console.log(id, user);
123
- }
124
-
125
- // Query data reactively (recommended)
126
- const $userCount = store.query(["users"], (collections) => {
127
- return collections.users.size;
128
- });
115
+ // Get all items as an array
116
+ const allUsers = store.getAll("users");
129
117
 
130
- // Get current value
131
- console.log($userCount.get()); // 5
132
-
133
- // Subscribe to updates
134
- $userCount.subscribe((count) => {
135
- console.log("User count:", count);
136
- });
118
+ // You can easily derive other operations:
119
+ const userIds = allUsers.map((u) => u.id);
120
+ const hasUser = allUsers.some((u) => u.id === "1");
137
121
  ```
138
122
 
139
- ### Reactive Queries
123
+ ### Listening to Changes
140
124
 
141
- For reactive updates, use the `query()` method. It lets you combine data from multiple collections and automatically updates when any of them change:
125
+ Subscribe to changes with `onChange()`:
142
126
 
143
127
  ```typescript
144
- // Query multiple collections
145
- const $stats = store.query(["users", "notes"], (collections) => {
146
- return {
147
- totalUsers: collections.users.size,
148
- totalNotes: collections.notes.size,
149
- firstUser: collections.users.get("1"),
150
- };
128
+ // Listen to all store changes
129
+ store.onChange((event) => {
130
+ console.log(`${event.collection} changed:`, event.event.type);
131
+ // Invalidate queries, update UI, etc.
151
132
  });
152
133
 
153
- // Subscribe to changes
154
- $stats.subscribe((stats) => {
155
- console.log("Stats updated:", stats);
134
+ // Filter for specific collection changes
135
+ store.onChange((event) => {
136
+ if (event.collection === "users") {
137
+ console.log("User change:", event.event.type, event.event.id);
138
+ if (event.event.type === "add") {
139
+ console.log("New user:", event.event.data);
140
+ }
141
+ }
156
142
  });
157
143
  ```
158
144
 
159
145
  ## Merging Data
160
146
 
161
- Starling's merge system is fully built and ready to use. When you add persistence and sync (coming soon), you'll use snapshots to sync data. A snapshot is a copy of all your data at a point in time.
147
+ Starling's core feature is conflict-free merging. When data changes in multiple places, Starling automatically resolves conflicts using timestamps.
162
148
 
163
149
  ### Getting a Snapshot
164
150
 
165
151
  Get the current state of your store:
166
152
 
167
153
  ```typescript
168
- const snapshot = store.$snapshot.get();
154
+ const snapshot = store.getSnapshot();
169
155
  // { clock: { ms: ..., seq: ... }, collections: { ... } }
170
156
  ```
171
157
 
@@ -174,14 +160,14 @@ const snapshot = store.$snapshot.get();
174
160
  Merge a snapshot from another device or user:
175
161
 
176
162
  ```typescript
177
- // Get snapshot from another device (when you add sync)
178
- const otherSnapshot = getSnapshotFromServer();
163
+ // Get snapshot from another device
164
+ const otherSnapshot = await fetchFromServer();
179
165
 
180
166
  // Merge it into your store
181
167
  store.merge(otherSnapshot);
182
168
  ```
183
169
 
184
- Starling automatically resolves conflicts. If the same item was changed in both places, it keeps the change with the newer timestamp. The merge API is ready now—just add your persistence and sync layer on top.
170
+ Starling automatically resolves conflicts. If the same field was changed in both places, it keeps the change with the newer timestamp (Last-Write-Wins).
185
171
 
186
172
  ### Syncing Between Two Stores
187
173
 
@@ -192,14 +178,83 @@ const store1 = createStore({ collections: { users: { schema: userSchema } } });
192
178
  const store2 = createStore({ collections: { users: { schema: userSchema } } });
193
179
 
194
180
  // Add data to store1
195
- store1.users.add({ id: "1", name: "Alice" });
181
+ store1.add("users", { id: "1", name: "Alice" });
196
182
 
197
183
  // Sync to store2
198
- const snapshot = store1.$snapshot.get();
184
+ const snapshot = store1.getSnapshot();
199
185
  store2.merge(snapshot);
200
186
 
201
187
  // Now store2 has the same data
202
- console.log(store2.users.get("1")); // { id: "1", name: "Alice" }
188
+ console.log(store2.get("users", "1")); // { id: "1", name: "Alice" }
189
+ ```
190
+
191
+ ## Reactivity Integration
192
+
193
+ Starling is framework-agnostic. Use `onChange()` to integrate with your reactive system:
194
+
195
+ ### React with TanStack Query
196
+
197
+ ```typescript
198
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
199
+
200
+ function useUsers() {
201
+ const queryClient = useQueryClient();
202
+
203
+ useEffect(() => {
204
+ return store.onChange((event) => {
205
+ if (event.collection === "users") {
206
+ queryClient.invalidateQueries({ queryKey: ["users"] });
207
+ }
208
+ });
209
+ }, []);
210
+
211
+ return useQuery({
212
+ queryKey: ["users"],
213
+ queryFn: () => store.getAll("users"),
214
+ });
215
+ }
216
+ ```
217
+
218
+ ### React with useSyncExternalStore
219
+
220
+ ```typescript
221
+ import { useSyncExternalStore } from "react";
222
+
223
+ function useUsers() {
224
+ return useSyncExternalStore(
225
+ (callback) =>
226
+ store.onChange((event) => {
227
+ if (event.collection === "users") callback();
228
+ }),
229
+ () => store.getAll("users"),
230
+ );
231
+ }
232
+ ```
233
+
234
+ ### Svelte
235
+
236
+ ```typescript
237
+ import { writable } from "svelte/store";
238
+
239
+ const users = writable(store.getAll("users"));
240
+ store.onChange((event) => {
241
+ if (event.collection === "users") {
242
+ users.set(store.getAll("users"));
243
+ }
244
+ });
245
+ ```
246
+
247
+ ### Vue
248
+
249
+ ```typescript
250
+ import { ref } from "vue";
251
+
252
+ const users = ref(store.getAll("users"));
253
+ store.onChange((event) => {
254
+ if (event.collection === "users") {
255
+ users.value = store.getAll("users");
256
+ }
257
+ });
203
258
  ```
204
259
 
205
260
  ## Schema Support
@@ -233,27 +288,16 @@ const schema = type({ id: "string", name: "string" });
233
288
 
234
289
  - `createStore(config)` - Creates a new store with collections
235
290
 
236
- ### Collection Methods
237
-
238
- Each collection in your store has these methods:
239
-
240
- - `add(data)` - Add a new document
241
- - `update(id, data)` - Update an existing document
242
- - `remove(id)` - Remove a document
243
- - `merge(snapshot)` - Merge a collection snapshot
244
- - `get(id)` - Get a document by ID
245
- - `has(id)` - Check if a document exists
246
- - `keys()` - Get all document IDs
247
- - `values()` - Get all documents
248
- - `entries()` - Get all [id, document] pairs
249
- - `forEach(callback)` - Iterate over documents
250
- - `size` - Number of documents
251
-
252
291
  ### Store Methods
253
292
 
254
- - `$snapshot` - Reactive atom containing the full store snapshot
293
+ - `add(collection, data)` - Add a new document to a collection
294
+ - `get(collection, id)` - Get a document by ID from a collection
295
+ - `getAll(collection)` - Get all documents from a collection as an array
296
+ - `update(collection, id, data)` - Update an existing document in a collection
297
+ - `remove(collection, id)` - Remove a document from a collection
298
+ - `getSnapshot()` - Get the full store snapshot for syncing
255
299
  - `merge(snapshot)` - Merge a store snapshot
256
- - `query(collections, callback)` - Query multiple collections reactively (recommended for reactive code)
300
+ - `onChange(listener)` - Subscribe to all collection changes
257
301
 
258
302
  For full type definitions, see the TypeScript types exported from the package.
259
303
 
@@ -261,14 +305,14 @@ For full type definitions, see the TypeScript types exported from the package.
261
305
 
262
306
  ```bash
263
307
  # Install dependencies
264
- bun install
308
+ pnpm install
265
309
 
266
310
  # Build the library
267
- bun run build
311
+ pnpm run build
268
312
 
269
313
  # Run tests
270
- bun test
314
+ pnpm test
271
315
 
272
316
  # Watch mode for development
273
- bun run dev
317
+ pnpm run dev
274
318
  ```
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { ReadableAtom, atom } from "nanostores";
2
1
  import { StandardSchemaV1 } from "@standard-schema/spec";
3
2
 
4
3
  //#region lib/core/clock.d.ts
@@ -21,7 +20,6 @@ type Tombstones = Record<string, string>;
21
20
  type DocumentId = string;
22
21
  type Collection = {
23
22
  documents: Record<DocumentId, Document>;
24
- tombstones: Tombstones;
25
23
  };
26
24
  //#endregion
27
25
  //#region lib/store/schema.d.ts
@@ -29,45 +27,52 @@ type Collection = {
29
27
  * Base type constraint for any standard schema object
30
28
  */
31
29
  type AnyObject = StandardSchemaV1<Record<string, any>>;
32
- type SchemaWithId<T extends AnyObject> = StandardSchemaV1.InferOutput<T> extends {
33
- id: any;
34
- } ? T : never;
35
30
  type Output<T extends AnyObject> = StandardSchemaV1.InferOutput<T>;
36
31
  type Input<T extends AnyObject> = StandardSchemaV1.InferInput<T>;
37
32
  //#endregion
38
- //#region lib/store/collection.d.ts
33
+ //#region lib/store/store.d.ts
39
34
  type CollectionConfig<T extends AnyObject> = {
40
35
  schema: T;
41
- getId: (data: Output<T>) => DocumentId;
42
- } | {
43
- schema: SchemaWithId<T>;
36
+ keyPath: keyof Output<T> & string;
44
37
  };
45
- type CollectionApi<T extends AnyObject> = {
46
- $data: ReadableAtom<ReadonlyMap<DocumentId, Output<T>>>;
47
- $snapshot: ReadableAtom<Collection>;
48
- add(data: Input<T>): void;
49
- remove(id: DocumentId): void;
50
- update(id: DocumentId, document: Partial<Input<T>>): void;
51
- merge(snapshot: Collection): void;
52
- } & Pick<ReadonlyMap<DocumentId, Output<T>>, "get" | "has" | "keys" | "values" | "entries" | "forEach" | "size">;
53
- //#endregion
54
- //#region lib/store/store.d.ts
55
38
  type StoreSnapshot = {
56
39
  clock: Clock;
57
40
  collections: Record<string, Collection>;
41
+ tombstones: Tombstones;
58
42
  };
59
- type StoreCollections<T extends Record<string, CollectionConfig<any>>> = { [K in keyof T]: T[K] extends CollectionConfig<infer S> ? CollectionApi<S> : never };
60
- type QueryCollections<TCollections extends StoreCollections<any>, TKeys extends readonly (keyof TCollections)[]> = { [K in TKeys[number]]: TCollections[K] extends {
61
- $data: ReadableAtom<infer D>;
62
- } ? D : never };
63
- type StoreAPI<T extends Record<string, CollectionConfig<any>>> = StoreCollections<T> & {
64
- $snapshot: ReadableAtom<StoreSnapshot>;
65
- query<TKeys extends readonly (keyof StoreCollections<T>)[], TResult>(collections: TKeys, callback: (collections: QueryCollections<StoreCollections<T>, TKeys>) => TResult): ReadableAtom<TResult>;
43
+ type StoreChangeEvent<T extends Record<string, CollectionConfig<AnyObject>>> = { [K in keyof T]: {
44
+ type: "add";
45
+ collection: K;
46
+ id: DocumentId;
47
+ data: Output<T[K]["schema"]>;
48
+ } | {
49
+ type: "update";
50
+ collection: K;
51
+ id: DocumentId;
52
+ data: Output<T[K]["schema"]>;
53
+ } | {
54
+ type: "remove";
55
+ collection: K;
56
+ id: DocumentId;
57
+ } | {
58
+ type: "merge";
59
+ collection: K;
60
+ } }[keyof T];
61
+ type StoreAPI<T extends Record<string, CollectionConfig<AnyObject>>> = {
62
+ add<K$1 extends keyof T & string>(collection: K$1, data: Input<T[K$1]["schema"]>): void;
63
+ get<K$1 extends keyof T & string>(collection: K$1, id: DocumentId): Output<T[K$1]["schema"]> | undefined;
64
+ getAll<K$1 extends keyof T & string>(collection: K$1, options?: {
65
+ where?: (item: Output<T[K$1]["schema"]>) => boolean;
66
+ }): Output<T[K$1]["schema"]>[];
67
+ update<K$1 extends keyof T & string>(collection: K$1, id: DocumentId, data: Partial<Input<T[K$1]["schema"]>>): void;
68
+ remove<K$1 extends keyof T & string>(collection: K$1, id: DocumentId): void;
69
+ getSnapshot(): StoreSnapshot;
66
70
  merge(snapshot: StoreSnapshot): void;
71
+ onChange(listener: (event: StoreChangeEvent<T>) => void): () => void;
67
72
  };
68
- declare function createStore<T extends Record<string, CollectionConfig<any>>>(config: {
73
+ declare function createStore<T extends Record<string, CollectionConfig<AnyObject>>>(config: {
69
74
  collections: T;
70
75
  }): StoreAPI<T>;
71
76
  //#endregion
72
- export { type AnyObject, QueryCollections, StoreAPI, StoreCollections, StoreSnapshot, createStore };
77
+ export { type AnyObject, type CollectionConfig, type StoreAPI, type StoreChangeEvent, type StoreSnapshot, createStore };
73
78
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,5 +1,3 @@
1
- import { atom, computed } from "nanostores";
2
-
3
1
  //#region lib/store/schema.ts
4
2
  function validate(schema, input) {
5
3
  const result = schema["~standard"].validate(input);
@@ -119,253 +117,166 @@ function mergeDocuments(target, source) {
119
117
  return result;
120
118
  }
121
119
 
122
- //#endregion
123
- //#region lib/core/tombstone.ts
124
- function mergeTombstones(target, source) {
125
- const result = {};
126
- const keys = new Set([...Object.keys(target), ...Object.keys(source)]);
127
- for (const key of keys) {
128
- const targetStamp = target[key];
129
- const sourceStamp = source[key];
130
- if (targetStamp && sourceStamp) result[key] = targetStamp > sourceStamp ? targetStamp : sourceStamp;
131
- else if (targetStamp) result[key] = targetStamp;
132
- else if (sourceStamp) result[key] = sourceStamp;
133
- }
134
- return result;
135
- }
136
-
137
120
  //#endregion
138
121
  //#region lib/core/collection.ts
139
- function mergeCollections$1(target, source) {
140
- const mergedTombstones = mergeTombstones(target.tombstones, source.tombstones);
122
+ function mergeCollections(target, source, tombstones) {
141
123
  const mergedDocuments = {};
142
124
  const allDocumentIds = new Set([...Object.keys(target.documents), ...Object.keys(source.documents)]);
143
125
  for (const id of allDocumentIds) {
144
126
  const targetDoc = target.documents[id];
145
127
  const sourceDoc = source.documents[id];
146
- if (mergedTombstones[id]) continue;
128
+ if (tombstones[id]) continue;
147
129
  if (targetDoc && sourceDoc) mergedDocuments[id] = mergeDocuments(targetDoc, sourceDoc);
148
130
  else if (targetDoc) mergedDocuments[id] = targetDoc;
149
131
  else if (sourceDoc) mergedDocuments[id] = sourceDoc;
150
132
  }
151
- return {
152
- documents: mergedDocuments,
153
- tombstones: mergedTombstones
154
- };
133
+ return { documents: mergedDocuments };
155
134
  }
156
135
 
157
136
  //#endregion
158
- //#region lib/store/collection.ts
159
- function addDocument($state, config, tick, data) {
160
- const getId = defineGetId(config);
161
- const valid = validate(config.schema, data);
162
- const doc = makeDocument(valid, tick());
163
- const id = getId(valid);
164
- const current = $state.get();
165
- $state.set({
166
- ...current,
167
- documents: {
168
- ...current.documents,
169
- [id]: doc
170
- }
171
- });
172
- }
173
- function removeDocument($state, tick, id) {
174
- const current = $state.get();
175
- const { [id]: _removed, ...remainingDocs } = current.documents;
176
- $state.set({
177
- documents: remainingDocs,
178
- tombstones: {
179
- ...current.tombstones,
180
- [id]: tick()
181
- }
182
- });
183
- }
184
- function updateDocument($state, config, tick, id, document) {
185
- const current = $state.get();
186
- const currentDoc = current.documents[id];
187
- if (!currentDoc) return;
188
- const doc = mergeDocuments(currentDoc, makeDocument(document, tick()));
189
- validate(config.schema, parseDocument(doc));
190
- $state.set({
191
- ...current,
192
- documents: {
193
- ...current.documents,
194
- [id]: doc
195
- }
196
- });
197
- }
198
- function mergeCollectionSnapshot($state, currentSnapshot, incomingSnapshot) {
199
- const merged = mergeCollections$1(currentSnapshot, incomingSnapshot);
200
- $state.set({
201
- documents: merged.documents,
202
- tombstones: merged.tombstones
203
- });
204
- }
205
- function createCollection(config, clock) {
206
- const { $data, $snapshot, $state } = createCollectionState();
207
- return {
208
- $data,
209
- $snapshot,
210
- get(key) {
211
- return $data.get().get(key);
212
- },
213
- has(key) {
214
- return $data.get().has(key);
215
- },
216
- keys() {
217
- return $data.get().keys();
218
- },
219
- values() {
220
- return $data.get().values();
221
- },
222
- entries() {
223
- return $data.get().entries();
224
- },
225
- forEach(callbackfn, thisArg) {
226
- return $data.get().forEach(callbackfn, thisArg);
227
- },
228
- get size() {
229
- return $data.get().size;
230
- },
231
- add(data) {
232
- addDocument($state, config, clock.tick, data);
233
- },
234
- remove(id) {
235
- removeDocument($state, clock.tick, id);
236
- },
237
- update(id, document) {
238
- updateDocument($state, config, clock.tick, id, document);
239
- },
240
- merge(snapshot) {
241
- mergeCollectionSnapshot($state, $snapshot.get(), snapshot);
242
- }
243
- };
244
- }
245
- function createCollectionState() {
246
- const $state = atom({
247
- documents: {},
248
- tombstones: {}
249
- });
250
- const $snapshot = computed($state, (state) => {
251
- return parseSnapshot(state.documents, state.tombstones);
252
- });
253
- return {
254
- $data: computed($state, (state) => {
255
- return parseCollection(state.documents, state.tombstones);
256
- }),
257
- $snapshot,
258
- $state
259
- };
260
- }
261
- function hasIdProperty(data) {
262
- return typeof data === "object" && data !== null && "id" in data && typeof data.id === "string";
263
- }
264
- function parseCollection(documents, tombstones) {
265
- const result = /* @__PURE__ */ new Map();
266
- for (const [id, doc] of Object.entries(documents)) if (!tombstones[id] && doc) result.set(id, parseDocument(doc));
137
+ //#region lib/core/tombstone.ts
138
+ function mergeTombstones(target, source) {
139
+ const result = {};
140
+ const keys = new Set([...Object.keys(target), ...Object.keys(source)]);
141
+ for (const key of keys) {
142
+ const targetStamp = target[key];
143
+ const sourceStamp = source[key];
144
+ if (targetStamp && sourceStamp) result[key] = targetStamp > sourceStamp ? targetStamp : sourceStamp;
145
+ else if (targetStamp) result[key] = targetStamp;
146
+ else if (sourceStamp) result[key] = sourceStamp;
147
+ }
267
148
  return result;
268
149
  }
269
- function parseSnapshot(documents, tombstones) {
270
- return {
271
- documents,
272
- tombstones
273
- };
274
- }
275
- function hasGetId(config) {
276
- return "getId" in config && typeof config.getId === "function";
277
- }
278
- function defineGetId(config) {
279
- return hasGetId(config) ? config.getId : defaultGetId;
280
- }
281
- function defaultGetId(data) {
282
- if (hasIdProperty(data)) return data.id;
283
- throw new Error("Schema must have an 'id' property when getId is not provided");
284
- }
285
150
 
286
151
  //#endregion
287
- //#region lib/store/clock.ts
288
- function createClock() {
289
- const $state = atom(nowClock());
152
+ //#region lib/store/store.ts
153
+ function createStore(config) {
154
+ let clock = {
155
+ ms: Date.now(),
156
+ seq: 0
157
+ };
158
+ let tombstones = {};
159
+ const documents = {};
160
+ const configs = /* @__PURE__ */ new Map();
161
+ const listeners = /* @__PURE__ */ new Set();
290
162
  const tick = () => {
291
- const next = advanceClock($state.get(), nowClock());
292
- $state.set(next);
293
- return makeStamp(next.ms, next.seq);
163
+ advance(Date.now(), 0);
164
+ return makeStamp(clock.ms, clock.seq);
294
165
  };
295
166
  const advance = (ms, seq) => {
296
- const next = advanceClock($state.get(), {
167
+ clock = advanceClock(clock, {
297
168
  ms,
298
169
  seq
299
170
  });
300
- $state.set(next);
301
171
  };
302
- return {
303
- $state,
304
- tick,
305
- advance
172
+ const notify = (collectionName, event) => {
173
+ listeners.forEach((listener) => listener({
174
+ collection: collectionName,
175
+ ...event
176
+ }));
306
177
  };
307
- }
308
- function nowClock() {
309
- return {
310
- ms: Date.now(),
311
- seq: 0
178
+ const getConfig = (collectionName) => {
179
+ const config$1 = configs.get(collectionName);
180
+ if (!config$1) throw new Error(`Collection "${collectionName}" not found`);
181
+ return config$1;
312
182
  };
313
- }
314
-
315
- //#endregion
316
- //#region lib/store/store.ts
317
- function createStore(config) {
318
- const clock = createClock();
319
- const collections = initCollections(config.collections, clock);
320
- const $snapshot = parseCollections(collections, clock.$state);
321
- function getCollectionDataStores(collectionNames) {
322
- return collectionNames.map((name) => collections[name].$data);
183
+ const getDocs = (collectionName) => {
184
+ const docs = documents[collectionName];
185
+ if (!docs) throw new Error(`Collection "${collectionName}" not found`);
186
+ return docs;
187
+ };
188
+ for (const [name, collectionConfig] of Object.entries(config.collections)) {
189
+ configs.set(name, collectionConfig);
190
+ documents[name] = {};
323
191
  }
324
192
  return {
325
- ...collections,
326
- $snapshot,
327
- query: (collectionNames, callback) => {
328
- return computed(getCollectionDataStores(collectionNames), (...values) => {
329
- const entries = collectionNames.map((name, i) => [name, values[i]]);
330
- return callback(Object.fromEntries(entries));
193
+ add(collectionName, data) {
194
+ const collectionConfig = getConfig(collectionName);
195
+ const valid = validate(collectionConfig.schema, data);
196
+ const id = valid[collectionConfig.keyPath];
197
+ const doc = makeDocument(valid, tick());
198
+ documents[collectionName] = {
199
+ ...documents[collectionName],
200
+ [id]: doc
201
+ };
202
+ notify(collectionName, {
203
+ type: "add",
204
+ id,
205
+ data: valid
206
+ });
207
+ },
208
+ get(collectionName, id) {
209
+ if (tombstones[id]) return void 0;
210
+ const doc = getDocs(collectionName)[id];
211
+ if (!doc) return void 0;
212
+ return parseDocument(doc);
213
+ },
214
+ getAll(collectionName, options) {
215
+ const collectionDocs = getDocs(collectionName);
216
+ const resultDocs = [];
217
+ for (const [id, doc] of Object.entries(collectionDocs)) if (doc && !tombstones[id]) {
218
+ const parsed = parseDocument(doc);
219
+ if (!options?.where || options?.where(parsed)) resultDocs.push(parsed);
220
+ }
221
+ return resultDocs;
222
+ },
223
+ update(collectionName, id, data) {
224
+ const collectionDocs = getDocs(collectionName);
225
+ const currentDoc = collectionDocs[id];
226
+ if (!currentDoc) return;
227
+ const collectionConfig = getConfig(collectionName);
228
+ const mergedDoc = mergeDocuments(currentDoc, makeDocument(data, tick()));
229
+ const parsed = parseDocument(mergedDoc);
230
+ validate(collectionConfig.schema, parsed);
231
+ documents[collectionName] = {
232
+ ...collectionDocs,
233
+ [id]: mergedDoc
234
+ };
235
+ notify(collectionName, {
236
+ type: "update",
237
+ id,
238
+ data: parsed
331
239
  });
332
240
  },
333
- merge: (snapshot) => {
334
- clock.advance(snapshot.clock.ms, snapshot.clock.seq);
335
- mergeCollections(collections, snapshot.collections);
241
+ remove(collectionName, id) {
242
+ const collectionDocs = getDocs(collectionName);
243
+ tombstones = {
244
+ ...tombstones,
245
+ [id]: tick()
246
+ };
247
+ const { [id]: _removed, ...remainingDocs } = collectionDocs;
248
+ documents[collectionName] = remainingDocs;
249
+ notify(collectionName, {
250
+ type: "remove",
251
+ id
252
+ });
253
+ },
254
+ getSnapshot() {
255
+ const collectionsSnapshot = {};
256
+ for (const [name, collectionDocs] of Object.entries(documents)) collectionsSnapshot[name] = { documents: collectionDocs };
257
+ return {
258
+ clock,
259
+ collections: collectionsSnapshot,
260
+ tombstones
261
+ };
262
+ },
263
+ merge(snapshot) {
264
+ advance(snapshot.clock.ms, snapshot.clock.seq);
265
+ tombstones = mergeTombstones(tombstones, snapshot.tombstones);
266
+ for (const [name, collectionData] of Object.entries(snapshot.collections)) {
267
+ if (!documents[name]) documents[name] = {};
268
+ const filteredDocs = {};
269
+ for (const [id, doc] of Object.entries(collectionData.documents)) if (!tombstones[id]) filteredDocs[id] = doc;
270
+ documents[name] = mergeCollections({ documents: documents[name] }, { documents: filteredDocs }, tombstones).documents;
271
+ notify(name, { type: "merge" });
272
+ }
273
+ },
274
+ onChange(listener) {
275
+ listeners.add(listener);
276
+ return () => listeners.delete(listener);
336
277
  }
337
278
  };
338
279
  }
339
- function initCollections(collectionsConfig, clock) {
340
- return Object.fromEntries(Object.entries(collectionsConfig).map(([name, config]) => [name, createCollection(config, clock)]));
341
- }
342
- function parseCollections(collections, clockState) {
343
- const collectionNames = Object.keys(collections);
344
- const collectionSnapshotAtoms = [];
345
- for (const name of collectionNames) {
346
- const collection = collections[name];
347
- if (collection) collectionSnapshotAtoms.push(collection.$snapshot);
348
- }
349
- return computed(collectionSnapshotAtoms, (...snapshots) => {
350
- const clock = clockState.get();
351
- const collectionsSnapshot = {};
352
- for (let i = 0; i < collectionNames.length; i++) {
353
- const name = collectionNames[i];
354
- const snapshot = snapshots[i];
355
- if (name && snapshot !== void 0) collectionsSnapshot[name] = snapshot;
356
- }
357
- return {
358
- clock,
359
- collections: collectionsSnapshot
360
- };
361
- });
362
- }
363
- function mergeCollections(target, source) {
364
- for (const [collectionName, collectionSnapshot] of Object.entries(source)) {
365
- const collection = target[collectionName];
366
- if (collection) collection.merge(collectionSnapshot);
367
- }
368
- }
369
280
 
370
281
  //#endregion
371
282
  export { createStore };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["result: Record<string, R>","result: Record<string, unknown>","current: any","result: Document","result: Tombstones","mergeCollections","mergedDocuments: Record<DocumentId, Document>","mergeCollections","$state: ClockAtom","collectionSnapshotAtoms: ReadableAtom<Collection>[]","collectionsSnapshot: Record<string, Collection>"],"sources":["../lib/store/schema.ts","../lib/core/hex.ts","../lib/core/clock.ts","../lib/core/flatten.ts","../lib/core/document.ts","../lib/core/tombstone.ts","../lib/core/collection.ts","../lib/store/collection.ts","../lib/store/clock.ts","../lib/store/store.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\n\nexport function validate<T extends StandardSchemaV1>(\n schema: T,\n input: StandardSchemaV1.InferInput<T>,\n): StandardSchemaV1.InferOutput<T> {\n const result = schema[\"~standard\"].validate(input);\n if (result instanceof Promise) {\n throw new TypeError(\"Schema validation must be synchronous\");\n }\n\n if (result.issues) {\n throw new Error(JSON.stringify(result.issues, null, 2));\n }\n\n return result.value;\n}\n\n/**\n * Base type constraint for any standard schema object\n */\nexport type AnyObject = StandardSchemaV1<Record<string, any>>;\n\nexport type SchemaWithId<T extends AnyObject> =\n StandardSchemaV1.InferOutput<T> extends {\n id: any;\n }\n ? T\n : never;\n\nexport type Output<T extends AnyObject> = StandardSchemaV1.InferOutput<T>;\n\nexport type Input<T extends AnyObject> = StandardSchemaV1.InferInput<T>;\n","export function toHex(value: number, padLength: number): string {\n return value.toString(16).padStart(padLength, \"0\");\n}\n\nexport function nonce(length: number): string {\n const bytes = new Uint8Array(length / 2);\n crypto.getRandomValues(bytes);\n return Array.from(bytes)\n .map((b) => toHex(b, 2))\n .join(\"\");\n}\n","import { nonce, toHex } from \"./hex\";\n\nconst MS_LENGTH = 12;\nconst SEQ_LENGTH = 6;\nconst NONCE_LENGTH = 6;\n\nexport type Clock = {\n ms: number;\n seq: number;\n};\n\nexport function advanceClock(current: Clock, next: Clock): Clock {\n if (next.ms > current.ms) {\n return { ms: next.ms, seq: next.seq };\n } else if (next.ms === current.ms) {\n return { ms: current.ms, seq: Math.max(current.seq, next.seq) + 1 };\n } else {\n return { ms: current.ms, seq: current.seq + 1 };\n }\n}\n\nexport function makeStamp(ms: number, seq: number): string {\n return `${toHex(ms, MS_LENGTH)}${toHex(seq, SEQ_LENGTH)}${nonce(NONCE_LENGTH)}`;\n}\n\nexport function parseStamp(stamp: string): { ms: number; seq: number } {\n return {\n ms: parseInt(stamp.slice(0, MS_LENGTH), 16),\n seq: parseInt(stamp.slice(MS_LENGTH, MS_LENGTH + SEQ_LENGTH), 16),\n };\n}\n","/**\n * Flattens a nested object into a flat object with dot-notation keys\n * @param obj - The object to flatten\n * @param mapper - Optional callback to transform leaf values\n * @returns A flattened object with dot-notation keys\n */\nexport function flatten<T, R = unknown>(\n obj: T,\n mapper?: (value: unknown, path: string) => R,\n): Record<string, R> {\n const result: Record<string, R> = {};\n\n const addLeaf = (value: unknown, path: string) => {\n if (path) {\n result[path] = mapper ? mapper(value, path) : (value as R);\n }\n };\n\n function traverse(current: unknown, prefix: string = \"\"): void {\n if (!shouldTraverse(current)) {\n addLeaf(current, prefix);\n return;\n }\n\n for (const [key, value] of Object.entries(current)) {\n const newPath = prefix ? `${prefix}.${key}` : key;\n traverse(value, newPath);\n }\n }\n\n traverse(obj);\n return result;\n}\n\n/**\n * Unflattens a flat object with dot-notation keys into a nested object\n * @param obj - The flattened object to unflatten\n * @param mapper - Optional callback to transform leaf values before placing them\n * @returns A nested object\n */\nexport function unflatten<T = unknown, R = unknown>(\n obj: Record<string, T>,\n mapper?: (value: T, path: string) => R,\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n\n for (const [path, value] of Object.entries(obj)) {\n const keys = path.split(\".\");\n const mappedValue = mapper ? mapper(value, path) : value;\n\n let current: any = result;\n for (let i = 0; i < keys.length - 1; i++) {\n const key = keys[i]!;\n if (!(key in current)) {\n current[key] = {};\n }\n current = current[key];\n }\n\n const finalKey = keys[keys.length - 1]!;\n current[finalKey] = mappedValue;\n }\n\n return result;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n (value.constructor === Object || Object.getPrototypeOf(value) === null)\n );\n}\n\nfunction shouldTraverse(value: unknown): value is Record<string, unknown> {\n return isPlainObject(value) && Object.keys(value).length > 0;\n}\n","import { flatten, unflatten } from \"./flatten\";\n\ntype Field<T = unknown> = {\n \"~value\": T;\n \"~stamp\": string;\n};\n\nexport type Document = Record<string, Field>;\n\nexport function makeDocument(\n fields: Record<string, any>,\n stamp: string,\n): Document {\n return flatten(fields, (value) => ({ \"~value\": value, \"~stamp\": stamp }));\n}\n\nexport function parseDocument(document: Document): Record<string, any> {\n return unflatten(document, (field) => field[\"~value\"]);\n}\n\nexport function mergeDocuments(target: Document, source: Document): Document {\n const result: Document = {};\n const keys = new Set([...Object.keys(target), ...Object.keys(source)]);\n\n for (const key of keys) {\n const targetValue = target[key];\n const sourceValue = source[key];\n\n if (targetValue && sourceValue) {\n result[key] =\n targetValue[\"~stamp\"] > sourceValue[\"~stamp\"]\n ? targetValue\n : sourceValue;\n } else if (targetValue) {\n result[key] = targetValue;\n } else if (sourceValue) {\n result[key] = sourceValue;\n } else {\n throw new Error(`Key ${key} not found in either document`);\n }\n }\n\n return result;\n}\n","export type Tombstones = Record<string, string>;\n\nexport function mergeTombstones(\n target: Tombstones,\n source: Tombstones,\n): Tombstones {\n const result: Tombstones = {};\n const keys = new Set([...Object.keys(target), ...Object.keys(source)]);\n\n for (const key of keys) {\n const targetStamp = target[key];\n const sourceStamp = source[key];\n\n if (targetStamp && sourceStamp) {\n result[key] = targetStamp > sourceStamp ? targetStamp : sourceStamp;\n } else if (targetStamp) {\n result[key] = targetStamp;\n } else if (sourceStamp) {\n result[key] = sourceStamp;\n }\n }\n\n return result;\n}\n","import type { Document } from \"./document\";\nimport type { Tombstones } from \"./tombstone\";\nimport { mergeDocuments } from \"./document\";\nimport { mergeTombstones } from \"./tombstone\";\n\nexport type DocumentId = string;\n\nexport type Collection = {\n documents: Record<DocumentId, Document>;\n tombstones: Tombstones;\n};\n\nexport function mergeCollections(\n target: Collection,\n source: Collection,\n): Collection {\n const mergedTombstones = mergeTombstones(\n target.tombstones,\n source.tombstones,\n );\n\n const mergedDocuments: Record<DocumentId, Document> = {};\n const allDocumentIds = new Set([\n ...Object.keys(target.documents),\n ...Object.keys(source.documents),\n ]);\n\n for (const id of allDocumentIds) {\n const targetDoc = target.documents[id];\n const sourceDoc = source.documents[id];\n\n if (mergedTombstones[id]) {\n continue;\n }\n\n if (targetDoc && sourceDoc) {\n mergedDocuments[id] = mergeDocuments(targetDoc, sourceDoc);\n } else if (targetDoc) {\n mergedDocuments[id] = targetDoc;\n } else if (sourceDoc) {\n mergedDocuments[id] = sourceDoc;\n }\n }\n\n return {\n documents: mergedDocuments,\n tombstones: mergedTombstones,\n };\n}\nexport function mergeCollectionRecords(\n target: Record<string, Collection>,\n source: Record<string, Collection>,\n): Record<string, Collection> {\n const result: Record<string, Collection> = { ...target };\n\n for (const [collectionName, sourceCollection] of Object.entries(source)) {\n const targetCollection = result[collectionName];\n if (targetCollection) {\n result[collectionName] = mergeCollections(\n targetCollection,\n sourceCollection,\n );\n } else {\n result[collectionName] = sourceCollection;\n }\n }\n\n return result;\n}\n","import { atom, computed, type ReadableAtom } from \"nanostores\";\nimport { validate } from \"./schema\";\nimport {\n makeDocument,\n parseDocument,\n mergeDocuments,\n mergeCollections,\n type Collection,\n type DocumentId,\n} from \"../core\";\nimport type { AnyObject, SchemaWithId, Output, Input } from \"./schema\";\nimport type { ClockAPI } from \"./clock\";\n\nexport type CollectionConfig<T extends AnyObject> =\n | {\n schema: T;\n getId: (data: Output<T>) => DocumentId;\n }\n | {\n schema: SchemaWithId<T>;\n };\n\nexport type CollectionApi<T extends AnyObject> = {\n $data: ReadableAtom<ReadonlyMap<DocumentId, Output<T>>>;\n $snapshot: ReadableAtom<Collection>;\n add(data: Input<T>): void;\n remove(id: DocumentId): void;\n update(id: DocumentId, document: Partial<Input<T>>): void;\n merge(snapshot: Collection): void;\n} & Pick<\n ReadonlyMap<DocumentId, Output<T>>,\n \"get\" | \"has\" | \"keys\" | \"values\" | \"entries\" | \"forEach\" | \"size\"\n>;\n\ntype TickFunction = () => string;\n\n// Internal state atom that holds both documents and tombstones\n// This allows us to update both atomically with a single notification\ntype CollectionState = {\n documents: Collection[\"documents\"];\n tombstones: Collection[\"tombstones\"];\n};\n\nexport function addDocument<T extends AnyObject>(\n $state: ReturnType<typeof atom<CollectionState>>,\n config: CollectionConfig<T>,\n tick: TickFunction,\n data: Input<T>,\n): void {\n const getId = defineGetId(config);\n const valid = validate(config.schema, data);\n const doc = makeDocument(valid, tick());\n const id = getId(valid);\n const current = $state.get();\n $state.set({\n ...current,\n documents: { ...current.documents, [id]: doc },\n });\n}\n\nexport function removeDocument(\n $state: ReturnType<typeof atom<CollectionState>>,\n tick: TickFunction,\n id: DocumentId,\n): void {\n const current = $state.get();\n const { [id]: _removed, ...remainingDocs } = current.documents;\n $state.set({\n documents: remainingDocs,\n tombstones: { ...current.tombstones, [id]: tick() },\n });\n}\n\nexport function updateDocument<T extends AnyObject>(\n $state: ReturnType<typeof atom<CollectionState>>,\n config: CollectionConfig<T>,\n tick: TickFunction,\n id: DocumentId,\n document: Partial<Input<T>>,\n): void {\n const current = $state.get();\n const currentDoc = current.documents[id];\n if (!currentDoc) return;\n\n const newAttrs = makeDocument(document, tick());\n const doc = mergeDocuments(currentDoc, newAttrs);\n\n validate(config.schema, parseDocument(doc));\n\n $state.set({\n ...current,\n documents: { ...current.documents, [id]: doc },\n });\n}\n\nexport function mergeCollectionSnapshot(\n $state: ReturnType<typeof atom<CollectionState>>,\n currentSnapshot: Collection,\n incomingSnapshot: Collection,\n): void {\n const merged = mergeCollections(currentSnapshot, incomingSnapshot);\n $state.set({\n documents: merged.documents,\n tombstones: merged.tombstones,\n });\n}\n\nexport function createCollection<T extends AnyObject>(\n config: CollectionConfig<T>,\n clock: ClockAPI,\n): CollectionApi<T> {\n const { $data, $snapshot, $state } = createCollectionState<T>();\n\n return {\n $data,\n $snapshot,\n get(key: DocumentId) {\n return $data.get().get(key);\n },\n has(key: DocumentId) {\n return $data.get().has(key);\n },\n keys() {\n return $data.get().keys();\n },\n values() {\n return $data.get().values();\n },\n entries() {\n return $data.get().entries();\n },\n forEach(\n callbackfn: (\n value: Output<T>,\n key: DocumentId,\n map: ReadonlyMap<DocumentId, Output<T>>,\n ) => void,\n thisArg?: any,\n ) {\n return $data.get().forEach(callbackfn, thisArg);\n },\n get size() {\n return $data.get().size;\n },\n add(data: Input<T>) {\n addDocument($state, config, clock.tick, data);\n },\n remove(id: DocumentId) {\n removeDocument($state, clock.tick, id);\n },\n update(id: DocumentId, document: Partial<Input<T>>) {\n updateDocument($state, config, clock.tick, id, document);\n },\n merge(snapshot: Collection) {\n const currentSnapshot = $snapshot.get();\n mergeCollectionSnapshot($state, currentSnapshot, snapshot);\n },\n };\n}\n\nfunction createCollectionState<T extends AnyObject>(): {\n $data: ReadableAtom<ReadonlyMap<DocumentId, Output<T>>>;\n $snapshot: ReadableAtom<Collection>;\n $state: ReturnType<typeof atom<CollectionState>>;\n} {\n // Single atom holding both documents and tombstones for atomic updates\n const $state = atom<CollectionState>({\n documents: {},\n tombstones: {},\n });\n\n const $snapshot = computed($state, (state) => {\n return parseSnapshot(state.documents, state.tombstones);\n });\n\n const $data = computed($state, (state) => {\n return parseCollection<T>(state.documents, state.tombstones);\n });\n\n return {\n $data,\n $snapshot,\n $state,\n };\n}\n\nfunction hasIdProperty<T extends AnyObject>(\n data: Output<T>,\n): data is { id: DocumentId } {\n return (\n typeof data === \"object\" &&\n data !== null &&\n \"id\" in data &&\n typeof (data as any).id === \"string\"\n );\n}\n\nfunction parseCollection<T extends AnyObject>(\n documents: Collection[\"documents\"],\n tombstones: Collection[\"tombstones\"],\n): ReadonlyMap<DocumentId, Output<T>> {\n const result = new Map<DocumentId, Output<T>>();\n for (const [id, doc] of Object.entries(documents)) {\n if (!tombstones[id] && doc) {\n result.set(id, parseDocument(doc));\n }\n }\n return result;\n}\n\nfunction parseSnapshot(\n documents: Collection[\"documents\"],\n tombstones: Collection[\"tombstones\"],\n): Collection {\n return {\n documents,\n tombstones,\n };\n}\n\nfunction hasGetId<T extends AnyObject>(\n config: CollectionConfig<T>,\n): config is {\n schema: T;\n getId: (data: Output<T>) => DocumentId;\n} {\n return \"getId\" in config && typeof config.getId === \"function\";\n}\n\nfunction defineGetId<T extends AnyObject>(\n config: CollectionConfig<T>,\n): (data: Output<T>) => DocumentId {\n return hasGetId(config) ? config.getId : defaultGetId;\n}\n\nfunction defaultGetId<T extends AnyObject>(data: Output<T>): DocumentId {\n if (hasIdProperty(data)) {\n return data.id;\n }\n throw new Error(\n \"Schema must have an 'id' property when getId is not provided\",\n );\n}\n","import { atom } from \"nanostores\";\nimport type { Clock } from \"../core/clock\";\nimport { advanceClock, makeStamp } from \"../core/clock\";\n\ntype ClockAtom = ReturnType<typeof atom<Clock>>;\n\nexport type ClockAPI = {\n $state: ClockAtom;\n tick: () => string;\n advance: (ms: number, seq: number) => void;\n};\n\nexport function createClock(): ClockAPI {\n const $state: ClockAtom = atom<Clock>(nowClock());\n\n const tick = () => {\n const next = advanceClock($state.get(), nowClock());\n $state.set(next);\n return makeStamp(next.ms, next.seq);\n };\n\n const advance = (ms: number, seq: number) => {\n const next = advanceClock($state.get(), { ms, seq });\n $state.set(next);\n };\n\n return {\n $state,\n tick,\n advance,\n };\n}\n\nfunction nowClock(): Clock {\n return { ms: Date.now(), seq: 0 };\n}\n","import { computed, type ReadableAtom } from \"nanostores\";\nimport { createCollection } from \"./collection\";\nimport { createClock, type ClockAPI } from \"./clock\";\nimport type { CollectionConfig, CollectionApi } from \"./collection\";\nimport type { Clock } from \"../core/clock\";\nimport type { Collection } from \"../core/collection\";\n\nexport type StoreSnapshot = {\n clock: Clock;\n collections: Record<string, Collection>;\n};\n\nexport type StoreCollections<T extends Record<string, CollectionConfig<any>>> =\n {\n [K in keyof T]: T[K] extends CollectionConfig<infer S>\n ? CollectionApi<S>\n : never;\n };\n\nexport type QueryCollections<\n TCollections extends StoreCollections<any>,\n TKeys extends readonly (keyof TCollections)[],\n> = {\n [K in TKeys[number]]: TCollections[K] extends { $data: ReadableAtom<infer D> }\n ? D\n : never;\n};\n\nexport type StoreAPI<T extends Record<string, CollectionConfig<any>>> =\n StoreCollections<T> & {\n $snapshot: ReadableAtom<StoreSnapshot>;\n query<TKeys extends readonly (keyof StoreCollections<T>)[], TResult>(\n collections: TKeys,\n callback: (\n collections: QueryCollections<StoreCollections<T>, TKeys>,\n ) => TResult,\n ): ReadableAtom<TResult>;\n merge(snapshot: StoreSnapshot): void;\n };\n\nexport function createStore<\n T extends Record<string, CollectionConfig<any>>,\n>(config: { collections: T }): StoreAPI<T> {\n const clock = createClock();\n const collections = initCollections(config.collections, clock);\n const $snapshot = parseCollections(collections, clock.$state);\n\n function getCollectionDataStores(\n collectionNames: readonly (keyof StoreCollections<T>)[],\n ): ReadableAtom<any>[] {\n return collectionNames.map((name) => collections[name]!.$data);\n }\n\n return {\n ...collections,\n $snapshot,\n query: <TKeys extends readonly (keyof StoreCollections<T>)[], TResult>(\n collectionNames: TKeys,\n callback: (\n collections: QueryCollections<StoreCollections<T>, TKeys>,\n ) => TResult,\n ) => {\n const atoms = getCollectionDataStores(collectionNames);\n\n return computed(atoms, (...values) => {\n const entries = collectionNames.map((name, i) => [name, values[i]]);\n return callback(\n Object.fromEntries(entries) as QueryCollections<\n StoreCollections<T>,\n TKeys\n >,\n );\n });\n },\n merge: (snapshot) => {\n clock.advance(snapshot.clock.ms, snapshot.clock.seq);\n mergeCollections(collections, snapshot.collections);\n },\n };\n}\n\nfunction initCollections<T extends Record<string, CollectionConfig<any>>>(\n collectionsConfig: T,\n clock: ClockAPI,\n): StoreCollections<T> {\n return Object.fromEntries(\n Object.entries(collectionsConfig).map(([name, config]) => [\n name,\n createCollection(config, clock),\n ]),\n ) as StoreCollections<T>;\n}\n\nfunction parseCollections<T extends Record<string, CollectionConfig<any>>>(\n collections: StoreCollections<T>,\n clockState: ReadableAtom<Clock>,\n): ReadableAtom<StoreSnapshot> {\n const collectionNames = Object.keys(collections);\n const collectionSnapshotAtoms: ReadableAtom<Collection>[] = [];\n\n for (const name of collectionNames) {\n const collection = collections[name];\n if (collection) {\n collectionSnapshotAtoms.push(collection.$snapshot);\n }\n }\n\n // Note: We don't include clockState in the dependency array because the clock\n // is always updated together with collection changes (via tick()). Including it\n // would cause double notifications. Instead, we read it synchronously inside.\n return computed(collectionSnapshotAtoms, (...snapshots) => {\n const clock = clockState.get();\n const collectionsSnapshot: Record<string, Collection> = {};\n for (let i = 0; i < collectionNames.length; i++) {\n const name = collectionNames[i];\n const snapshot = snapshots[i];\n if (name && snapshot !== undefined) {\n collectionsSnapshot[name] = snapshot;\n }\n }\n\n return {\n clock,\n collections: collectionsSnapshot,\n };\n });\n}\n\nfunction mergeCollections(\n target: Record<string, CollectionApi<any>>,\n source: Record<string, Collection>,\n) {\n for (const [collectionName, collectionSnapshot] of Object.entries(source)) {\n const collection = target[collectionName];\n if (collection) {\n collection.merge(collectionSnapshot);\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["result: Record<string, R>","result: Record<string, unknown>","current: any","result: Document","mergedDocuments: Record<DocumentId, Document>","result: Tombstones","clock: Clock","tombstones: Tombstones","documents: Record<string, Record<DocumentId, Document>>","config","resultDocs: Output<T[typeof collectionName][\"schema\"]>[]","collectionsSnapshot: Record<string, Collection>","filteredDocs: Record<DocumentId, Document>"],"sources":["../lib/store/schema.ts","../lib/core/hex.ts","../lib/core/clock.ts","../lib/core/flatten.ts","../lib/core/document.ts","../lib/core/collection.ts","../lib/core/tombstone.ts","../lib/store/store.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\n\nexport function validate<T extends StandardSchemaV1>(\n schema: T,\n input: StandardSchemaV1.InferInput<T>,\n): StandardSchemaV1.InferOutput<T> {\n const result = schema[\"~standard\"].validate(input);\n if (result instanceof Promise) {\n throw new TypeError(\"Schema validation must be synchronous\");\n }\n\n if (result.issues) {\n throw new Error(JSON.stringify(result.issues, null, 2));\n }\n\n return result.value;\n}\n\n/**\n * Base type constraint for any standard schema object\n */\nexport type AnyObject = StandardSchemaV1<Record<string, any>>;\n\nexport type SchemaWithId<T extends AnyObject> =\n StandardSchemaV1.InferOutput<T> extends {\n id: any;\n }\n ? T\n : never;\n\nexport type Output<T extends AnyObject> = StandardSchemaV1.InferOutput<T>;\n\nexport type Input<T extends AnyObject> = StandardSchemaV1.InferInput<T>;\n","export function toHex(value: number, padLength: number): string {\n return value.toString(16).padStart(padLength, \"0\");\n}\n\nexport function nonce(length: number): string {\n const bytes = new Uint8Array(length / 2);\n crypto.getRandomValues(bytes);\n return Array.from(bytes)\n .map((b) => toHex(b, 2))\n .join(\"\");\n}\n","import { nonce, toHex } from \"./hex\";\n\nconst MS_LENGTH = 12;\nconst SEQ_LENGTH = 6;\nconst NONCE_LENGTH = 6;\n\nexport type Clock = {\n ms: number;\n seq: number;\n};\n\nexport function advanceClock(current: Clock, next: Clock): Clock {\n if (next.ms > current.ms) {\n return { ms: next.ms, seq: next.seq };\n } else if (next.ms === current.ms) {\n return { ms: current.ms, seq: Math.max(current.seq, next.seq) + 1 };\n } else {\n return { ms: current.ms, seq: current.seq + 1 };\n }\n}\n\nexport function makeStamp(ms: number, seq: number): string {\n return `${toHex(ms, MS_LENGTH)}${toHex(seq, SEQ_LENGTH)}${nonce(NONCE_LENGTH)}`;\n}\n\nexport function parseStamp(stamp: string): { ms: number; seq: number } {\n return {\n ms: parseInt(stamp.slice(0, MS_LENGTH), 16),\n seq: parseInt(stamp.slice(MS_LENGTH, MS_LENGTH + SEQ_LENGTH), 16),\n };\n}\n","/**\n * Flattens a nested object into a flat object with dot-notation keys\n * @param obj - The object to flatten\n * @param mapper - Optional callback to transform leaf values\n * @returns A flattened object with dot-notation keys\n */\nexport function flatten<T, R = unknown>(\n obj: T,\n mapper?: (value: unknown, path: string) => R,\n): Record<string, R> {\n const result: Record<string, R> = {};\n\n const addLeaf = (value: unknown, path: string) => {\n if (path) {\n result[path] = mapper ? mapper(value, path) : (value as R);\n }\n };\n\n function traverse(current: unknown, prefix: string = \"\"): void {\n if (!shouldTraverse(current)) {\n addLeaf(current, prefix);\n return;\n }\n\n for (const [key, value] of Object.entries(current)) {\n const newPath = prefix ? `${prefix}.${key}` : key;\n traverse(value, newPath);\n }\n }\n\n traverse(obj);\n return result;\n}\n\n/**\n * Unflattens a flat object with dot-notation keys into a nested object\n * @param obj - The flattened object to unflatten\n * @param mapper - Optional callback to transform leaf values before placing them\n * @returns A nested object\n */\nexport function unflatten<T = unknown, R = unknown>(\n obj: Record<string, T>,\n mapper?: (value: T, path: string) => R,\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n\n for (const [path, value] of Object.entries(obj)) {\n const keys = path.split(\".\");\n const mappedValue = mapper ? mapper(value, path) : value;\n\n let current: any = result;\n for (let i = 0; i < keys.length - 1; i++) {\n const key = keys[i]!;\n if (!(key in current)) {\n current[key] = {};\n }\n current = current[key];\n }\n\n const finalKey = keys[keys.length - 1]!;\n current[finalKey] = mappedValue;\n }\n\n return result;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n (value.constructor === Object || Object.getPrototypeOf(value) === null)\n );\n}\n\nfunction shouldTraverse(value: unknown): value is Record<string, unknown> {\n return isPlainObject(value) && Object.keys(value).length > 0;\n}\n","import { flatten, unflatten } from \"./flatten\";\n\ntype Field<T = unknown> = {\n \"~value\": T;\n \"~stamp\": string;\n};\n\nexport type Document = Record<string, Field>;\n\nexport function makeDocument(fields: Record<string, any>, stamp: string): Document {\n return flatten(fields, (value) => ({ \"~value\": value, \"~stamp\": stamp }));\n}\n\nexport function parseDocument(document: Document): Record<string, any> {\n return unflatten(document, (field) => field[\"~value\"]);\n}\n\nexport function mergeDocuments(target: Document, source: Document): Document {\n const result: Document = {};\n const keys = new Set([...Object.keys(target), ...Object.keys(source)]);\n\n for (const key of keys) {\n const targetValue = target[key];\n const sourceValue = source[key];\n\n if (targetValue && sourceValue) {\n result[key] = targetValue[\"~stamp\"] > sourceValue[\"~stamp\"] ? targetValue : sourceValue;\n } else if (targetValue) {\n result[key] = targetValue;\n } else if (sourceValue) {\n result[key] = sourceValue;\n } else {\n throw new Error(`Key ${key} not found in either document`);\n }\n }\n\n return result;\n}\n","import type { Document } from \"./document\";\nimport type { Tombstones } from \"./tombstone\";\nimport { mergeDocuments } from \"./document\";\n\nexport type DocumentId = string;\n\nexport type Collection = {\n documents: Record<DocumentId, Document>;\n};\n\nexport function mergeCollections(\n target: Collection,\n source: Collection,\n tombstones: Tombstones,\n): Collection {\n const mergedDocuments: Record<DocumentId, Document> = {};\n const allDocumentIds = new Set([\n ...Object.keys(target.documents),\n ...Object.keys(source.documents),\n ]);\n\n for (const id of allDocumentIds) {\n const targetDoc = target.documents[id];\n const sourceDoc = source.documents[id];\n\n if (tombstones[id]) {\n continue;\n }\n\n if (targetDoc && sourceDoc) {\n mergedDocuments[id] = mergeDocuments(targetDoc, sourceDoc);\n } else if (targetDoc) {\n mergedDocuments[id] = targetDoc;\n } else if (sourceDoc) {\n mergedDocuments[id] = sourceDoc;\n }\n }\n\n return {\n documents: mergedDocuments,\n };\n}\nexport function mergeCollectionRecords(\n target: Record<string, Collection>,\n source: Record<string, Collection>,\n tombstones: Tombstones,\n): Record<string, Collection> {\n const result: Record<string, Collection> = { ...target };\n\n for (const [collectionName, sourceCollection] of Object.entries(source)) {\n const targetCollection = result[collectionName];\n if (targetCollection) {\n result[collectionName] = mergeCollections(targetCollection, sourceCollection, tombstones);\n } else {\n result[collectionName] = sourceCollection;\n }\n }\n\n return result;\n}\n","export type Tombstones = Record<string, string>;\n\nexport function mergeTombstones(target: Tombstones, source: Tombstones): Tombstones {\n const result: Tombstones = {};\n const keys = new Set([...Object.keys(target), ...Object.keys(source)]);\n\n for (const key of keys) {\n const targetStamp = target[key];\n const sourceStamp = source[key];\n\n if (targetStamp && sourceStamp) {\n result[key] = targetStamp > sourceStamp ? targetStamp : sourceStamp;\n } else if (targetStamp) {\n result[key] = targetStamp;\n } else if (sourceStamp) {\n result[key] = sourceStamp;\n }\n }\n\n return result;\n}\n","import { validate } from \"./schema\";\nimport {\n makeDocument,\n parseDocument,\n mergeDocuments,\n mergeCollections,\n type Collection,\n type DocumentId,\n} from \"../core\";\nimport type { Clock } from \"../core/clock\";\nimport { advanceClock, makeStamp } from \"../core/clock\";\nimport type { Input, Output, AnyObject } from \"./schema\";\nimport type { Tombstones } from \"../core/tombstone\";\nimport { mergeTombstones } from \"../core/tombstone\";\nimport type { Document } from \"../core/document\";\n\nexport type CollectionConfig<T extends AnyObject> = {\n schema: T;\n keyPath: keyof Output<T> & string;\n};\n\nexport type StoreSnapshot = {\n clock: Clock;\n collections: Record<string, Collection>;\n tombstones: Tombstones;\n};\n\nexport type StoreChangeEvent<T extends Record<string, CollectionConfig<AnyObject>>> = {\n [K in keyof T]:\n | { type: \"add\"; collection: K; id: DocumentId; data: Output<T[K][\"schema\"]> }\n | { type: \"update\"; collection: K; id: DocumentId; data: Output<T[K][\"schema\"]> }\n | { type: \"remove\"; collection: K; id: DocumentId }\n | { type: \"merge\"; collection: K };\n}[keyof T];\n\nexport type StoreAPI<T extends Record<string, CollectionConfig<AnyObject>>> = {\n add<K extends keyof T & string>(collection: K, data: Input<T[K][\"schema\"]>): void;\n\n get<K extends keyof T & string>(\n collection: K,\n id: DocumentId,\n ): Output<T[K][\"schema\"]> | undefined;\n\n getAll<K extends keyof T & string>(\n collection: K,\n options?: { where?: (item: Output<T[K][\"schema\"]>) => boolean },\n ): Output<T[K][\"schema\"]>[];\n\n update<K extends keyof T & string>(\n collection: K,\n id: DocumentId,\n data: Partial<Input<T[K][\"schema\"]>>,\n ): void;\n\n remove<K extends keyof T & string>(collection: K, id: DocumentId): void;\n\n getSnapshot(): StoreSnapshot;\n merge(snapshot: StoreSnapshot): void;\n onChange(listener: (event: StoreChangeEvent<T>) => void): () => void;\n};\n\nexport function createStore<T extends Record<string, CollectionConfig<AnyObject>>>(config: {\n collections: T;\n}): StoreAPI<T> {\n let clock: Clock = { ms: Date.now(), seq: 0 };\n let tombstones: Tombstones = {};\n const documents: Record<string, Record<DocumentId, Document>> = {};\n const configs = new Map<string, CollectionConfig<AnyObject>>();\n const listeners = new Set<(event: StoreChangeEvent<T>) => void>();\n\n const tick = (): string => {\n advance(Date.now(), 0);\n return makeStamp(clock.ms, clock.seq);\n };\n\n const advance = (ms: number, seq: number): void => {\n clock = advanceClock(clock, { ms, seq });\n };\n\n const notify = (collectionName: string, event: { type: string; id?: DocumentId; data?: any }) => {\n listeners.forEach((listener) =>\n listener({ collection: collectionName, ...event } as StoreChangeEvent<T>),\n );\n };\n\n const getConfig = <K extends keyof T & string>(\n collectionName: K,\n ): CollectionConfig<AnyObject> => {\n const config = configs.get(collectionName);\n\n if (!config) {\n throw new Error(`Collection \"${collectionName}\" not found`);\n }\n\n return config;\n };\n\n const getDocs = <K extends keyof T & string>(collectionName: K): Record<DocumentId, Document> => {\n const docs = documents[collectionName];\n if (!docs) {\n throw new Error(`Collection \"${collectionName}\" not found`);\n }\n return docs;\n };\n\n // Initialize collections\n for (const [name, collectionConfig] of Object.entries(config.collections)) {\n configs.set(name, collectionConfig);\n documents[name] = {};\n }\n\n return {\n add(collectionName, data) {\n const collectionConfig = getConfig(collectionName);\n const valid = validate(collectionConfig.schema, data);\n const id = valid[collectionConfig.keyPath] as DocumentId;\n const doc = makeDocument(valid, tick());\n\n documents[collectionName] = {\n ...documents[collectionName],\n [id]: doc,\n };\n\n notify(collectionName, { type: \"add\", id, data: valid });\n },\n\n get(collectionName, id) {\n if (tombstones[id]) return undefined;\n const collectionDocs = getDocs(collectionName);\n const doc = collectionDocs[id];\n\n if (!doc) return undefined;\n\n return parseDocument(doc) as Output<T[typeof collectionName][\"schema\"]>;\n },\n\n getAll(collectionName, options) {\n const collectionDocs = getDocs(collectionName);\n const resultDocs: Output<T[typeof collectionName][\"schema\"]>[] = [];\n\n for (const [id, doc] of Object.entries(collectionDocs)) {\n if (doc && !tombstones[id]) {\n const parsed = parseDocument(doc) as Output<T[typeof collectionName][\"schema\"]>;\n if (!options?.where || options?.where(parsed)) {\n resultDocs.push(parsed);\n }\n }\n }\n\n return resultDocs;\n },\n\n update(collectionName, id, data) {\n const collectionDocs = getDocs(collectionName);\n const currentDoc = collectionDocs[id];\n\n if (!currentDoc) return;\n\n const collectionConfig = getConfig(collectionName);\n const newAttrs = makeDocument(data, tick());\n const mergedDoc = mergeDocuments(currentDoc, newAttrs);\n\n const parsed = parseDocument(mergedDoc);\n validate(collectionConfig.schema, parsed);\n\n documents[collectionName] = { ...collectionDocs, [id]: mergedDoc };\n\n notify(collectionName, { type: \"update\", id, data: parsed });\n },\n\n remove(collectionName, id) {\n const collectionDocs = getDocs(collectionName);\n tombstones = { ...tombstones, [id]: tick() };\n const { [id]: _removed, ...remainingDocs } = collectionDocs;\n documents[collectionName] = remainingDocs;\n notify(collectionName, { type: \"remove\", id });\n },\n\n getSnapshot(): StoreSnapshot {\n const collectionsSnapshot: Record<string, Collection> = {};\n for (const [name, collectionDocs] of Object.entries(documents)) {\n collectionsSnapshot[name] = { documents: collectionDocs };\n }\n return {\n clock,\n collections: collectionsSnapshot,\n tombstones,\n };\n },\n\n merge(snapshot: StoreSnapshot): void {\n advance(snapshot.clock.ms, snapshot.clock.seq);\n\n tombstones = mergeTombstones(tombstones, snapshot.tombstones);\n\n for (const [name, collectionData] of Object.entries(snapshot.collections)) {\n // Initialize collection if it doesn't exist\n if (!documents[name]) {\n documents[name] = {};\n }\n\n // Filter out tombstoned documents before merging\n const filteredDocs: Record<DocumentId, Document> = {};\n for (const [id, doc] of Object.entries(collectionData.documents)) {\n if (!tombstones[id]) {\n filteredDocs[id] = doc;\n }\n }\n\n // Merge collections using core mergeCollections function\n const currentCollection: Collection = {\n documents: documents[name],\n };\n\n const sourceCollection: Collection = {\n documents: filteredDocs,\n };\n\n const merged = mergeCollections(currentCollection, sourceCollection, tombstones);\n documents[name] = merged.documents;\n\n // Notify merge event\n notify(name, { type: \"merge\" });\n }\n },\n\n onChange(listener: (event: StoreChangeEvent<T>) => void): () => void {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@byearlybird/starling",
3
- "version": "0.13.2",
3
+ "version": "0.14.0",
4
+ "files": [
5
+ "dist"
6
+ ],
4
7
  "type": "module",
5
8
  "main": "./dist/index.js",
6
9
  "module": "./dist/index.js",
@@ -11,28 +14,31 @@
11
14
  "import": "./dist/index.js"
12
15
  }
13
16
  },
14
- "files": [
15
- "dist"
16
- ],
17
- "scripts": {
18
- "build": "tsdown",
19
- "dev": "tsdown --watch",
20
- "format": "prettier . --write",
21
- "format:check": "prettier . --check",
22
- "typecheck": "tsc --noEmit",
23
- "prepublishOnly": "bun run build"
17
+ "dependencies": {
18
+ "@standard-schema/spec": "^1.1.0"
24
19
  },
25
20
  "devDependencies": {
26
- "@types/bun": "latest",
27
- "prettier": "3.7.4",
21
+ "@types/node": "^22.10.5",
22
+ "@vitest/coverage-v8": "^4.0.17",
23
+ "oxfmt": "^0.24.0",
24
+ "oxlint": "^1.39.0",
28
25
  "tsdown": "^0.18.3",
26
+ "typescript": "^5.7.3",
27
+ "vitest": "^4.0.17",
29
28
  "zod": "^4.2.1"
30
29
  },
31
30
  "peerDependencies": {
32
31
  "typescript": "^5"
33
32
  },
34
- "dependencies": {
35
- "@standard-schema/spec": "^1.1.0",
36
- "nanostores": "^1.1.0"
33
+ "scripts": {
34
+ "build": "tsdown",
35
+ "dev": "tsdown --watch",
36
+ "test": "vitest run",
37
+ "test:watch": "vitest",
38
+ "fmt": "oxfmt",
39
+ "fmt:check": "oxfmt --check",
40
+ "lint": "oxlint",
41
+ "lint:fix": "oxlint --fix",
42
+ "typecheck": "tsc --noEmit"
37
43
  }
38
- }
44
+ }