@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 +122 -78
- package/dist/index.d.ts +33 -28
- package/dist/index.js +126 -215
- package/dist/index.js.map +1 -1
- package/package.json +23 -17
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# Starling
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Conflict-free replicated state for JavaScript. Bring your own reactivity.
|
|
4
4
|
|
|
5
|
-
Starling
|
|
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
|
-
|
|
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.
|
|
35
|
-
const user = store.
|
|
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
|
-
- **
|
|
41
|
-
- **
|
|
42
|
-
- **Works with
|
|
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.
|
|
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.
|
|
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.
|
|
104
|
+
store.remove("users", "1");
|
|
105
105
|
```
|
|
106
106
|
|
|
107
107
|
### Reading Data
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
The store provides simple getter methods:
|
|
110
110
|
|
|
111
111
|
```typescript
|
|
112
112
|
// Get a single item
|
|
113
|
-
const user = store.
|
|
113
|
+
const user = store.get("users", "1");
|
|
114
114
|
|
|
115
|
-
//
|
|
116
|
-
|
|
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
|
-
//
|
|
131
|
-
|
|
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
|
-
###
|
|
123
|
+
### Listening to Changes
|
|
140
124
|
|
|
141
|
-
|
|
125
|
+
Subscribe to changes with `onChange()`:
|
|
142
126
|
|
|
143
127
|
```typescript
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
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
|
|
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
|
|
178
|
-
const otherSnapshot =
|
|
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
|
|
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.
|
|
181
|
+
store1.add("users", { id: "1", name: "Alice" });
|
|
196
182
|
|
|
197
183
|
// Sync to store2
|
|
198
|
-
const snapshot = store1
|
|
184
|
+
const snapshot = store1.getSnapshot();
|
|
199
185
|
store2.merge(snapshot);
|
|
200
186
|
|
|
201
187
|
// Now store2 has the same data
|
|
202
|
-
console.log(store2.
|
|
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
|
-
-
|
|
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
|
-
- `
|
|
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
|
-
|
|
308
|
+
pnpm install
|
|
265
309
|
|
|
266
310
|
# Build the library
|
|
267
|
-
|
|
311
|
+
pnpm run build
|
|
268
312
|
|
|
269
313
|
# Run tests
|
|
270
|
-
|
|
314
|
+
pnpm test
|
|
271
315
|
|
|
272
316
|
# Watch mode for development
|
|
273
|
-
|
|
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/
|
|
33
|
+
//#region lib/store/store.d.ts
|
|
39
34
|
type CollectionConfig<T extends AnyObject> = {
|
|
40
35
|
schema: T;
|
|
41
|
-
|
|
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
|
|
60
|
-
type
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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<
|
|
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,
|
|
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
|
|
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 (
|
|
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/
|
|
159
|
-
function
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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/
|
|
288
|
-
function
|
|
289
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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
|
-
|
|
167
|
+
clock = advanceClock(clock, {
|
|
297
168
|
ms,
|
|
298
169
|
seq
|
|
299
170
|
});
|
|
300
|
-
$state.set(next);
|
|
301
171
|
};
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
172
|
+
const notify = (collectionName, event) => {
|
|
173
|
+
listeners.forEach((listener) => listener({
|
|
174
|
+
collection: collectionName,
|
|
175
|
+
...event
|
|
176
|
+
}));
|
|
306
177
|
};
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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.
|
|
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
|
-
"
|
|
15
|
-
"
|
|
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/
|
|
27
|
-
"
|
|
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
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
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
|
+
}
|