@byearlybird/starling 0.12.0 → 0.13.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 +274 -0
- package/dist/index.d.ts +73 -3
- package/dist/index.js +291 -410
- package/dist/index.js.map +1 -0
- package/package.json +36 -44
- package/dist/core-UUzgRHaU.js +0 -420
- package/dist/core.d.ts +0 -2
- package/dist/core.js +0 -3
- package/dist/db-DY3UcmfV.d.ts +0 -199
- package/dist/index-BIpu-1zO.d.ts +0 -265
- package/dist/plugin-http.d.ts +0 -139
- package/dist/plugin-http.js +0 -191
- package/dist/plugin-idb.d.ts +0 -59
- package/dist/plugin-idb.js +0 -169
package/README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# Starling
|
|
2
|
+
|
|
3
|
+
A mergable data store for building local-first apps that sync.
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @byearlybird/starling
|
|
11
|
+
# or
|
|
12
|
+
bun add @byearlybird/starling
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires TypeScript 5 or higher.
|
|
16
|
+
|
|
17
|
+
## Quick Example
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { createStore } from "@byearlybird/starling";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
|
|
23
|
+
const userSchema = z.object({
|
|
24
|
+
id: z.string(),
|
|
25
|
+
name: z.string(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const store = createStore({
|
|
29
|
+
collections: {
|
|
30
|
+
users: { schema: userSchema },
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
store.users.add({ id: "1", name: "Alice" });
|
|
35
|
+
const user = store.users.get("1"); // { id: "1", name: "Alice" }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Features
|
|
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
|
|
45
|
+
- **Type-safe**: Full TypeScript support with type inference
|
|
46
|
+
- **Merge snapshots**: Sync data between devices or users easily
|
|
47
|
+
|
|
48
|
+
## Basic Usage
|
|
49
|
+
|
|
50
|
+
### Creating a Store
|
|
51
|
+
|
|
52
|
+
A store holds one or more collections. Each collection has a schema that defines what data it can store.
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { createStore } from "@byearlybird/starling";
|
|
56
|
+
import { z } from "zod";
|
|
57
|
+
|
|
58
|
+
const store = createStore({
|
|
59
|
+
collections: {
|
|
60
|
+
users: {
|
|
61
|
+
schema: z.object({
|
|
62
|
+
id: z.string(),
|
|
63
|
+
name: z.string(),
|
|
64
|
+
email: z.string().optional(),
|
|
65
|
+
}),
|
|
66
|
+
},
|
|
67
|
+
notes: {
|
|
68
|
+
schema: z.object({
|
|
69
|
+
id: z.string(),
|
|
70
|
+
content: z.string(),
|
|
71
|
+
}),
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Adding Documents
|
|
78
|
+
|
|
79
|
+
Add new items to a collection with `add()`:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
store.users.add({
|
|
83
|
+
id: "1",
|
|
84
|
+
name: "Alice",
|
|
85
|
+
email: "alice@example.com",
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Updating Documents
|
|
90
|
+
|
|
91
|
+
Update existing items with `update()`:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
store.users.update("1", {
|
|
95
|
+
email: "newemail@example.com",
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Removing Documents
|
|
100
|
+
|
|
101
|
+
Remove items with `remove()`:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
store.users.remove("1");
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Reading Data
|
|
108
|
+
|
|
109
|
+
Collections work like maps. You can read data in several ways:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// Get a single item
|
|
113
|
+
const user = store.users.get("1");
|
|
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
|
+
});
|
|
129
|
+
|
|
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
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Reactive Queries
|
|
140
|
+
|
|
141
|
+
For reactive updates, use the `query()` method. It lets you combine data from multiple collections and automatically updates when any of them change:
|
|
142
|
+
|
|
143
|
+
```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
|
+
};
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Subscribe to changes
|
|
154
|
+
$stats.subscribe((stats) => {
|
|
155
|
+
console.log("Stats updated:", stats);
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Merging Data
|
|
160
|
+
|
|
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.
|
|
162
|
+
|
|
163
|
+
### Getting a Snapshot
|
|
164
|
+
|
|
165
|
+
Get the current state of your store:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
const snapshot = store.$snapshot.get();
|
|
169
|
+
// { clock: { ms: ..., seq: ... }, collections: { ... } }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Merging a Snapshot
|
|
173
|
+
|
|
174
|
+
Merge a snapshot from another device or user:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// Get snapshot from another device (when you add sync)
|
|
178
|
+
const otherSnapshot = getSnapshotFromServer();
|
|
179
|
+
|
|
180
|
+
// Merge it into your store
|
|
181
|
+
store.merge(otherSnapshot);
|
|
182
|
+
```
|
|
183
|
+
|
|
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.
|
|
185
|
+
|
|
186
|
+
### Syncing Between Two Stores
|
|
187
|
+
|
|
188
|
+
Here's a simple example of syncing between two stores:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
const store1 = createStore({ collections: { users: { schema: userSchema } } });
|
|
192
|
+
const store2 = createStore({ collections: { users: { schema: userSchema } } });
|
|
193
|
+
|
|
194
|
+
// Add data to store1
|
|
195
|
+
store1.users.add({ id: "1", name: "Alice" });
|
|
196
|
+
|
|
197
|
+
// Sync to store2
|
|
198
|
+
const snapshot = store1.$snapshot.get();
|
|
199
|
+
store2.merge(snapshot);
|
|
200
|
+
|
|
201
|
+
// Now store2 has the same data
|
|
202
|
+
console.log(store2.users.get("1")); // { id: "1", name: "Alice" }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Schema Support
|
|
206
|
+
|
|
207
|
+
Starling works with any library that follows the [Standard Schema](https://github.com/standard-schema/spec) specification. This includes:
|
|
208
|
+
|
|
209
|
+
- **Zod** - Most popular schema library
|
|
210
|
+
- **Valibot** - Lightweight alternative
|
|
211
|
+
- **ArkType** - TypeScript-first schemas
|
|
212
|
+
|
|
213
|
+
You can use any of these to define your data shapes. Starling will validate your data and give you full TypeScript types.
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { z } from "zod";
|
|
217
|
+
// or
|
|
218
|
+
import * as v from "valibot";
|
|
219
|
+
// or
|
|
220
|
+
import { type } from "arktype";
|
|
221
|
+
|
|
222
|
+
// All of these work the same way
|
|
223
|
+
const schema = z.object({ id: z.string(), name: z.string() });
|
|
224
|
+
// or
|
|
225
|
+
const schema = v.object({ id: v.string(), name: v.string() });
|
|
226
|
+
// or
|
|
227
|
+
const schema = type({ id: "string", name: "string" });
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## API Overview
|
|
231
|
+
|
|
232
|
+
### Main Export
|
|
233
|
+
|
|
234
|
+
- `createStore(config)` - Creates a new store with collections
|
|
235
|
+
|
|
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
|
+
### Store Methods
|
|
253
|
+
|
|
254
|
+
- `$snapshot` - Reactive atom containing the full store snapshot
|
|
255
|
+
- `merge(snapshot)` - Merge a store snapshot
|
|
256
|
+
- `query(collections, callback)` - Query multiple collections reactively (recommended for reactive code)
|
|
257
|
+
|
|
258
|
+
For full type definitions, see the TypeScript types exported from the package.
|
|
259
|
+
|
|
260
|
+
## Development
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
# Install dependencies
|
|
264
|
+
bun install
|
|
265
|
+
|
|
266
|
+
# Build the library
|
|
267
|
+
bun run build
|
|
268
|
+
|
|
269
|
+
# Run tests
|
|
270
|
+
bun test
|
|
271
|
+
|
|
272
|
+
# Watch mode for development
|
|
273
|
+
bun run dev
|
|
274
|
+
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,73 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { ReadableAtom, atom, map } from "nanostores";
|
|
2
|
+
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
3
|
+
|
|
4
|
+
//#region lib/core/clock.d.ts
|
|
5
|
+
type Clock = {
|
|
6
|
+
ms: number;
|
|
7
|
+
seq: number;
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region lib/core/document.d.ts
|
|
11
|
+
type Field<T = unknown> = {
|
|
12
|
+
"~value": T;
|
|
13
|
+
"~stamp": string;
|
|
14
|
+
};
|
|
15
|
+
type Document = Record<string, Field>;
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region lib/core/tombstone.d.ts
|
|
18
|
+
type Tombstones = Record<string, string>;
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region lib/core/collection.d.ts
|
|
21
|
+
type DocumentId = string;
|
|
22
|
+
type Collection = {
|
|
23
|
+
documents: Record<DocumentId, Document>;
|
|
24
|
+
tombstones: Tombstones;
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region lib/store/schema.d.ts
|
|
28
|
+
/**
|
|
29
|
+
* Base type constraint for any standard schema object
|
|
30
|
+
*/
|
|
31
|
+
type AnyObject = StandardSchemaV1<Record<string, any>>;
|
|
32
|
+
type SchemaWithId<T extends AnyObject> = StandardSchemaV1.InferOutput<T> extends {
|
|
33
|
+
id: any;
|
|
34
|
+
} ? T : never;
|
|
35
|
+
type Output<T extends AnyObject> = StandardSchemaV1.InferOutput<T>;
|
|
36
|
+
type Input<T extends AnyObject> = StandardSchemaV1.InferInput<T>;
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region lib/store/collection.d.ts
|
|
39
|
+
type CollectionConfig<T extends AnyObject> = {
|
|
40
|
+
schema: T;
|
|
41
|
+
getId: (data: Output<T>) => DocumentId;
|
|
42
|
+
} | {
|
|
43
|
+
schema: SchemaWithId<T>;
|
|
44
|
+
};
|
|
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
|
+
type StoreSnapshot = {
|
|
56
|
+
clock: Clock;
|
|
57
|
+
collections: Record<string, Collection>;
|
|
58
|
+
};
|
|
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>;
|
|
66
|
+
merge(snapshot: StoreSnapshot): void;
|
|
67
|
+
};
|
|
68
|
+
declare function createStore<T extends Record<string, CollectionConfig<any>>>(config: {
|
|
69
|
+
collections: T;
|
|
70
|
+
}): StoreAPI<T>;
|
|
71
|
+
//#endregion
|
|
72
|
+
export { type AnyObject, QueryCollections, StoreAPI, StoreCollections, StoreSnapshot, createStore };
|
|
73
|
+
//# sourceMappingURL=index.d.ts.map
|