@byearlybird/starling 0.8.1 → 0.9.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/dist/index.d.ts +2 -2
- package/dist/index.js +383 -282
- package/dist/plugins/unstorage/plugin.d.ts +17 -0
- package/dist/{plugin-unstorage.js → plugins/unstorage/plugin.js} +5 -5
- package/dist/store-Dc-hIF56.d.ts +114 -0
- package/package.json +4 -9
- package/dist/plugin-query.d.ts +0 -19
- package/dist/plugin-query.js +0 -102
- package/dist/plugin-unstorage.d.ts +0 -17
- package/dist/store-BGsRIZeM.d.ts +0 -86
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { type EncodedDocument, Plugin,
|
|
1
|
+
import { a as StoreAddOptions, i as Store, l as EncodedDocument, n as Query, o as StoreConfig, r as QueryConfig, s as StoreSetTransaction, t as Plugin, u as processDocument } from "./store-Dc-hIF56.js";
|
|
2
|
+
export { type EncodedDocument, Plugin, Query, QueryConfig, Store, StoreAddOptions, StoreConfig, StoreSetTransaction, processDocument };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
//#region src/eventstamp.ts
|
|
2
|
-
|
|
1
|
+
//#region src/crdt/eventstamp.ts
|
|
2
|
+
function generateNonce() {
|
|
3
3
|
return Math.random().toString(16).slice(2, 6).padStart(4, "0");
|
|
4
|
-
}
|
|
5
|
-
|
|
4
|
+
}
|
|
5
|
+
function encodeEventstamp(timestampMs, counter, nonce) {
|
|
6
6
|
return `${new Date(timestampMs).toISOString()}|${counter.toString(16).padStart(4, "0")}|${nonce}`;
|
|
7
|
-
}
|
|
8
|
-
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Validates whether a string is a properly formatted eventstamp.
|
|
10
|
+
* Expected format: YYYY-MM-DDTHH:mm:ss.SSSZ|HHHH+|HHHH
|
|
11
|
+
* where HHHH+ represents 4 or more hex characters for the counter,
|
|
12
|
+
* and HHHH represents exactly 4 hex characters for the nonce.
|
|
13
|
+
*/
|
|
14
|
+
function isValidEventstamp(stamp) {
|
|
15
|
+
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\|[0-9a-f]{4,}\|[0-9a-f]{4}$/.test(stamp);
|
|
16
|
+
}
|
|
17
|
+
function decodeEventstamp(eventstamp) {
|
|
18
|
+
if (!isValidEventstamp(eventstamp)) throw new Error(`Invalid eventstamp format: "${eventstamp}". Expected format: YYYY-MM-DDTHH:mm:ss.SSSZ|HHHH+|HHHH`);
|
|
9
19
|
const parts = eventstamp.split("|");
|
|
10
20
|
const isoString = parts[0];
|
|
11
21
|
const hexCounter = parts[1];
|
|
@@ -15,26 +25,36 @@ const decodeEventstamp = (eventstamp) => {
|
|
|
15
25
|
counter: parseInt(hexCounter, 16),
|
|
16
26
|
nonce
|
|
17
27
|
};
|
|
18
|
-
}
|
|
28
|
+
}
|
|
19
29
|
const MIN_EVENTSTAMP = encodeEventstamp(0, 0, "0000");
|
|
20
30
|
|
|
21
31
|
//#endregion
|
|
22
|
-
//#region src/utils.ts
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
//#region src/crdt/utils.ts
|
|
33
|
+
function isObject(value) {
|
|
34
|
+
return !!(value != null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype);
|
|
35
|
+
}
|
|
36
|
+
function isEncodedValue(value) {
|
|
37
|
+
return !!(typeof value === "object" && value !== null && "~value" in value && "~eventstamp" in value);
|
|
38
|
+
}
|
|
25
39
|
|
|
26
40
|
//#endregion
|
|
27
|
-
//#region src/value.ts
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
//#region src/crdt/value.ts
|
|
42
|
+
function encodeValue(value, eventstamp) {
|
|
43
|
+
return {
|
|
44
|
+
"~value": value,
|
|
45
|
+
"~eventstamp": eventstamp
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function decodeValue(value) {
|
|
49
|
+
return value["~value"];
|
|
50
|
+
}
|
|
51
|
+
function mergeValues(into, from) {
|
|
52
|
+
return into["~eventstamp"] > from["~eventstamp"] ? [into, into["~eventstamp"]] : [from, from["~eventstamp"]];
|
|
53
|
+
}
|
|
34
54
|
|
|
35
55
|
//#endregion
|
|
36
|
-
//#region src/record.ts
|
|
37
|
-
|
|
56
|
+
//#region src/crdt/record.ts
|
|
57
|
+
function processRecord(source, process) {
|
|
38
58
|
const result = {};
|
|
39
59
|
const step = (input, output) => {
|
|
40
60
|
for (const key in input) {
|
|
@@ -49,8 +69,8 @@ const processRecord = (source, process) => {
|
|
|
49
69
|
};
|
|
50
70
|
step(source, result);
|
|
51
71
|
return result;
|
|
52
|
-
}
|
|
53
|
-
|
|
72
|
+
}
|
|
73
|
+
function encodeRecord(obj, eventstamp) {
|
|
54
74
|
const result = {};
|
|
55
75
|
const step = (input, output) => {
|
|
56
76
|
for (const key in input) {
|
|
@@ -64,8 +84,8 @@ const encodeRecord = (obj, eventstamp) => {
|
|
|
64
84
|
};
|
|
65
85
|
step(obj, result);
|
|
66
86
|
return result;
|
|
67
|
-
}
|
|
68
|
-
|
|
87
|
+
}
|
|
88
|
+
function decodeRecord(obj) {
|
|
69
89
|
const result = {};
|
|
70
90
|
const step = (input, output) => {
|
|
71
91
|
for (const key in input) {
|
|
@@ -80,8 +100,8 @@ const decodeRecord = (obj) => {
|
|
|
80
100
|
};
|
|
81
101
|
step(obj, result);
|
|
82
102
|
return result;
|
|
83
|
-
}
|
|
84
|
-
|
|
103
|
+
}
|
|
104
|
+
function mergeRecords(into, from) {
|
|
85
105
|
const result = {};
|
|
86
106
|
let greatestEventstamp = MIN_EVENTSTAMP;
|
|
87
107
|
const step = (v1, v2, output) => {
|
|
@@ -116,21 +136,25 @@ const mergeRecords = (into, from) => {
|
|
|
116
136
|
};
|
|
117
137
|
step(into, from, result);
|
|
118
138
|
return [result, greatestEventstamp];
|
|
119
|
-
}
|
|
139
|
+
}
|
|
120
140
|
|
|
121
141
|
//#endregion
|
|
122
|
-
//#region src/document.ts
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
142
|
+
//#region src/crdt/document.ts
|
|
143
|
+
function encodeDoc(id, obj, eventstamp, deletedAt = null) {
|
|
144
|
+
return {
|
|
145
|
+
"~id": id,
|
|
146
|
+
"~data": isObject(obj) ? encodeRecord(obj, eventstamp) : encodeValue(obj, eventstamp),
|
|
147
|
+
"~deletedAt": deletedAt
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function decodeDoc(doc) {
|
|
151
|
+
return {
|
|
152
|
+
"~id": doc["~id"],
|
|
153
|
+
"~data": isEncodedValue(doc["~data"]) ? decodeValue(doc["~data"]) : decodeRecord(doc["~data"]),
|
|
154
|
+
"~deletedAt": doc["~deletedAt"]
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function mergeDocs(into, from) {
|
|
134
158
|
const intoIsValue = isEncodedValue(into["~data"]);
|
|
135
159
|
const fromIsValue = isEncodedValue(from["~data"]);
|
|
136
160
|
if (intoIsValue !== fromIsValue) throw new Error("Merge error: Incompatible types");
|
|
@@ -143,277 +167,354 @@ const mergeDocs = (into, from) => {
|
|
|
143
167
|
"~data": mergedData,
|
|
144
168
|
"~deletedAt": mergedDeletedAt
|
|
145
169
|
}, greatestEventstamp];
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
170
|
+
}
|
|
171
|
+
function deleteDoc(doc, eventstamp) {
|
|
172
|
+
return {
|
|
173
|
+
"~id": doc["~id"],
|
|
174
|
+
"~data": doc["~data"],
|
|
175
|
+
"~deletedAt": eventstamp
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function processDocument(doc, process) {
|
|
153
179
|
const processedData = isEncodedValue(doc["~data"]) ? process(doc["~data"]) : processRecord(doc["~data"], process);
|
|
154
180
|
return {
|
|
155
181
|
"~id": doc["~id"],
|
|
156
182
|
"~data": processedData,
|
|
157
183
|
"~deletedAt": doc["~deletedAt"]
|
|
158
184
|
};
|
|
159
|
-
}
|
|
185
|
+
}
|
|
160
186
|
|
|
161
187
|
//#endregion
|
|
162
|
-
//#region src/
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
//#region src/crdt/collection.ts
|
|
189
|
+
/**
|
|
190
|
+
* Merges two collections using field-level Last-Write-Wins semantics.
|
|
191
|
+
*
|
|
192
|
+
* The merge operation:
|
|
193
|
+
* 1. Forwards the clock to the newest eventstamp from either collection
|
|
194
|
+
* 2. Merges each document pair using field-level LWW (via mergeDocs)
|
|
195
|
+
* 3. Tracks what changed for hook notifications (added/updated/deleted)
|
|
196
|
+
*
|
|
197
|
+
* Deletion is final: once a document is deleted, updates to it are merged into
|
|
198
|
+
* the document's data but don't restore visibility. Only new documents or
|
|
199
|
+
* transitions into the deleted state are tracked.
|
|
200
|
+
*
|
|
201
|
+
* @param into - The base collection to merge into
|
|
202
|
+
* @param from - The source collection to merge from
|
|
203
|
+
* @returns Merged collection and categorized changes
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```typescript
|
|
207
|
+
* const into = {
|
|
208
|
+
* "~docs": [{ "~id": "doc1", "~data": {...}, "~deletedAt": null }],
|
|
209
|
+
* "~eventstamp": "2025-01-01T00:00:00.000Z|0001|a1b2"
|
|
210
|
+
* };
|
|
211
|
+
*
|
|
212
|
+
* const from = {
|
|
213
|
+
* "~docs": [
|
|
214
|
+
* { "~id": "doc1", "~data": {...}, "~deletedAt": null }, // updated
|
|
215
|
+
* { "~id": "doc2", "~data": {...}, "~deletedAt": null } // new
|
|
216
|
+
* ],
|
|
217
|
+
* "~eventstamp": "2025-01-01T00:05:00.000Z|0001|c3d4"
|
|
218
|
+
* };
|
|
219
|
+
*
|
|
220
|
+
* const result = mergeCollections(into, from);
|
|
221
|
+
* // result.collection.~eventstamp === "2025-01-01T00:05:00.000Z|0001|c3d4"
|
|
222
|
+
* // result.changes.added has "doc2"
|
|
223
|
+
* // result.changes.updated has "doc1"
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
function mergeCollections(into, from) {
|
|
227
|
+
const intoDocsById = /* @__PURE__ */ new Map();
|
|
228
|
+
for (const doc of into["~docs"]) intoDocsById.set(doc["~id"], doc);
|
|
229
|
+
const added = /* @__PURE__ */ new Map();
|
|
230
|
+
const updated = /* @__PURE__ */ new Map();
|
|
231
|
+
const deleted = /* @__PURE__ */ new Set();
|
|
232
|
+
const mergedDocsById = new Map(intoDocsById);
|
|
233
|
+
for (const fromDoc of from["~docs"]) {
|
|
234
|
+
const id = fromDoc["~id"];
|
|
235
|
+
const intoDoc = intoDocsById.get(id);
|
|
236
|
+
if (!intoDoc) {
|
|
237
|
+
mergedDocsById.set(id, fromDoc);
|
|
238
|
+
if (!fromDoc["~deletedAt"]) added.set(id, fromDoc);
|
|
239
|
+
} else {
|
|
240
|
+
if (intoDoc === fromDoc) continue;
|
|
241
|
+
const [mergedDoc] = mergeDocs(intoDoc, fromDoc);
|
|
242
|
+
mergedDocsById.set(id, mergedDoc);
|
|
243
|
+
const wasDeleted = intoDoc["~deletedAt"] !== null;
|
|
244
|
+
const isDeleted = mergedDoc["~deletedAt"] !== null;
|
|
245
|
+
if (!wasDeleted && isDeleted) deleted.add(id);
|
|
246
|
+
else if (!isDeleted) updated.set(id, mergedDoc);
|
|
190
247
|
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
//#endregion
|
|
195
|
-
//#region src/kv.ts
|
|
196
|
-
const createKV = (iterable) => {
|
|
197
|
-
let readMap = new Map(iterable);
|
|
248
|
+
}
|
|
249
|
+
const newestEventstamp = into["~eventstamp"] >= from["~eventstamp"] ? into["~eventstamp"] : from["~eventstamp"];
|
|
198
250
|
return {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
values() {
|
|
203
|
-
return readMap.values();
|
|
251
|
+
collection: {
|
|
252
|
+
"~docs": Array.from(mergedDocsById.values()),
|
|
253
|
+
"~eventstamp": newestEventstamp
|
|
204
254
|
},
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
return readMap.size;
|
|
210
|
-
},
|
|
211
|
-
begin(callback) {
|
|
212
|
-
const staging = new Map(readMap);
|
|
213
|
-
let rolledBack = false;
|
|
214
|
-
callback({
|
|
215
|
-
get(key) {
|
|
216
|
-
return staging.get(key) ?? null;
|
|
217
|
-
},
|
|
218
|
-
set(key, value, opts) {
|
|
219
|
-
if (opts?.replace) {
|
|
220
|
-
staging.set(key, value);
|
|
221
|
-
return null;
|
|
222
|
-
} else {
|
|
223
|
-
const prev = staging.get(key);
|
|
224
|
-
if (prev) {
|
|
225
|
-
const [merged, eventstamp] = mergeDocs(prev, value);
|
|
226
|
-
staging.set(key, merged);
|
|
227
|
-
return eventstamp;
|
|
228
|
-
} else {
|
|
229
|
-
staging.set(key, value);
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
},
|
|
234
|
-
del(key, eventstamp) {
|
|
235
|
-
const prev = staging.get(key);
|
|
236
|
-
if (prev) staging.set(key, deleteDoc(prev, eventstamp));
|
|
237
|
-
},
|
|
238
|
-
rollback() {
|
|
239
|
-
rolledBack = true;
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
if (!rolledBack) readMap = staging;
|
|
255
|
+
changes: {
|
|
256
|
+
added,
|
|
257
|
+
updated,
|
|
258
|
+
deleted
|
|
243
259
|
}
|
|
244
260
|
};
|
|
245
|
-
}
|
|
261
|
+
}
|
|
246
262
|
|
|
247
263
|
//#endregion
|
|
248
|
-
//#region src/
|
|
264
|
+
//#region src/clock.ts
|
|
249
265
|
/**
|
|
250
|
-
*
|
|
251
|
-
*
|
|
266
|
+
* A Hybrid Logical Clock that generates monotonically increasing eventstamps.
|
|
267
|
+
* Combines wall-clock time with a counter for handling clock stalls and a
|
|
268
|
+
* random nonce for tie-breaking.
|
|
269
|
+
*
|
|
270
|
+
* The clock automatically increments the counter when the wall clock doesn't
|
|
271
|
+
* advance, ensuring eventstamps are always unique and monotonic.
|
|
252
272
|
*/
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
},
|
|
268
|
-
merge(doc) {
|
|
269
|
-
if (kvTx.get(doc["~id"])) kvTx.set(doc["~id"], doc);
|
|
270
|
-
else kvTx.set(doc["~id"], doc, { replace: true });
|
|
271
|
-
const currentDoc = kvTx.get(doc["~id"]);
|
|
272
|
-
if (currentDoc) if (currentDoc["~deletedAt"]) deleteKeys.push(doc["~id"]);
|
|
273
|
-
else {
|
|
274
|
-
const merged = decodeDoc(currentDoc)["~data"];
|
|
275
|
-
patchKeyValues.push([doc["~id"], merged]);
|
|
276
|
-
}
|
|
277
|
-
},
|
|
278
|
-
del(key) {
|
|
279
|
-
if (!kvTx.get(key)) return;
|
|
280
|
-
kvTx.del(key, clock.now());
|
|
281
|
-
deleteKeys.push(key);
|
|
282
|
-
},
|
|
283
|
-
get(key) {
|
|
284
|
-
return decodeActive(kvTx.get(key));
|
|
285
|
-
},
|
|
286
|
-
rollback() {
|
|
287
|
-
state.rolledBack = true;
|
|
288
|
-
tx.rolledBack = true;
|
|
289
|
-
kvTx.rollback();
|
|
273
|
+
var Clock = class {
|
|
274
|
+
#counter = 0;
|
|
275
|
+
#lastMs = Date.now();
|
|
276
|
+
#lastNonce = generateNonce();
|
|
277
|
+
/** Generates a new eventstamp, advancing the clock */
|
|
278
|
+
now() {
|
|
279
|
+
const wallMs = Date.now();
|
|
280
|
+
if (wallMs > this.#lastMs) {
|
|
281
|
+
this.#lastMs = wallMs;
|
|
282
|
+
this.#counter = 0;
|
|
283
|
+
this.#lastNonce = generateNonce();
|
|
284
|
+
} else {
|
|
285
|
+
this.#counter++;
|
|
286
|
+
this.#lastNonce = generateNonce();
|
|
290
287
|
}
|
|
291
|
-
|
|
292
|
-
|
|
288
|
+
return encodeEventstamp(this.#lastMs, this.#counter, this.#lastNonce);
|
|
289
|
+
}
|
|
290
|
+
/** Returns the most recent eventstamp without advancing the clock */
|
|
291
|
+
latest() {
|
|
292
|
+
return encodeEventstamp(this.#lastMs, this.#counter, this.#lastNonce);
|
|
293
|
+
}
|
|
294
|
+
/** Fast-forwards the clock to match a newer remote eventstamp */
|
|
295
|
+
forward(eventstamp) {
|
|
296
|
+
if (eventstamp > this.latest()) {
|
|
297
|
+
const newer = decodeEventstamp(eventstamp);
|
|
298
|
+
this.#lastMs = newer.timestampMs;
|
|
299
|
+
this.#counter = newer.counter;
|
|
300
|
+
this.#lastNonce = newer.nonce;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
293
303
|
};
|
|
294
304
|
|
|
295
305
|
//#endregion
|
|
296
306
|
//#region src/store.ts
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
307
|
+
var Store = class {
|
|
308
|
+
#readMap = /* @__PURE__ */ new Map();
|
|
309
|
+
#clock = new Clock();
|
|
310
|
+
#getId;
|
|
311
|
+
#onInitHandlers = [];
|
|
312
|
+
#onDisposeHandlers = [];
|
|
313
|
+
#onAddHandlers = [];
|
|
314
|
+
#onUpdateHandlers = [];
|
|
315
|
+
#onDeleteHandlers = [];
|
|
316
|
+
#queries = /* @__PURE__ */ new Set();
|
|
317
|
+
constructor(config = {}) {
|
|
318
|
+
this.#getId = config.getId ?? (() => crypto.randomUUID());
|
|
319
|
+
}
|
|
320
|
+
get(key) {
|
|
321
|
+
return this.#decodeActive(this.#readMap.get(key) ?? null);
|
|
322
|
+
}
|
|
323
|
+
entries() {
|
|
324
|
+
const self = this;
|
|
325
|
+
function* iterator() {
|
|
326
|
+
for (const [key, doc] of self.#readMap.entries()) {
|
|
327
|
+
const data = self.#decodeActive(doc);
|
|
328
|
+
if (data !== null) yield [key, data];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return iterator();
|
|
332
|
+
}
|
|
333
|
+
collection() {
|
|
334
|
+
return {
|
|
335
|
+
"~docs": Array.from(this.#readMap.values()),
|
|
336
|
+
"~eventstamp": this.#clock.latest()
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
merge(collection) {
|
|
340
|
+
const result = mergeCollections(this.collection(), collection);
|
|
341
|
+
this.#clock.forward(result.collection["~eventstamp"]);
|
|
342
|
+
this.#readMap = new Map(result.collection["~docs"].map((doc) => [doc["~id"], doc]));
|
|
343
|
+
const addEntries = Array.from(result.changes.added.entries()).map(([key, doc]) => [key, decodeDoc(doc)["~data"]]);
|
|
344
|
+
const updateEntries = Array.from(result.changes.updated.entries()).map(([key, doc]) => [key, decodeDoc(doc)["~data"]]);
|
|
345
|
+
const deleteKeys = Array.from(result.changes.deleted);
|
|
346
|
+
if (addEntries.length > 0 || updateEntries.length > 0 || deleteKeys.length > 0) this.#emitMutations(addEntries, updateEntries, deleteKeys);
|
|
347
|
+
}
|
|
348
|
+
begin(callback, opts) {
|
|
349
|
+
const silent = opts?.silent ?? false;
|
|
350
|
+
const addEntries = [];
|
|
351
|
+
const updateEntries = [];
|
|
352
|
+
const deleteKeys = [];
|
|
353
|
+
const staging = new Map(this.#readMap);
|
|
354
|
+
let rolledBack = false;
|
|
355
|
+
const result = callback({
|
|
356
|
+
add: (value, options) => {
|
|
357
|
+
const key = options?.withId ?? this.#getId();
|
|
358
|
+
staging.set(key, this.#encodeValue(key, value));
|
|
359
|
+
addEntries.push([key, value]);
|
|
360
|
+
return key;
|
|
361
|
+
},
|
|
362
|
+
update: (key, value) => {
|
|
363
|
+
const doc = encodeDoc(key, value, this.#clock.now());
|
|
364
|
+
const prev = staging.get(key);
|
|
365
|
+
const mergedDoc = prev ? mergeDocs(prev, doc)[0] : doc;
|
|
366
|
+
staging.set(key, mergedDoc);
|
|
367
|
+
const merged = this.#decodeActive(mergedDoc);
|
|
368
|
+
if (merged !== null) updateEntries.push([key, merged]);
|
|
369
|
+
},
|
|
370
|
+
merge: (doc) => {
|
|
371
|
+
const existing = staging.get(doc["~id"]);
|
|
372
|
+
const mergedDoc = existing ? mergeDocs(existing, doc)[0] : doc;
|
|
373
|
+
staging.set(doc["~id"], mergedDoc);
|
|
374
|
+
const decoded = this.#decodeActive(mergedDoc);
|
|
375
|
+
const isNew = !this.#readMap.has(doc["~id"]);
|
|
376
|
+
if (mergedDoc["~deletedAt"]) deleteKeys.push(doc["~id"]);
|
|
377
|
+
else if (decoded !== null) if (isNew) addEntries.push([doc["~id"], decoded]);
|
|
378
|
+
else updateEntries.push([doc["~id"], decoded]);
|
|
379
|
+
},
|
|
380
|
+
del: (key) => {
|
|
381
|
+
const currentDoc = staging.get(key);
|
|
382
|
+
if (!currentDoc) return;
|
|
383
|
+
staging.set(key, deleteDoc(currentDoc, this.#clock.now()));
|
|
384
|
+
deleteKeys.push(key);
|
|
385
|
+
},
|
|
386
|
+
get: (key) => this.#decodeActive(staging.get(key) ?? null),
|
|
387
|
+
rollback: () => {
|
|
388
|
+
rolledBack = true;
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
if (!rolledBack) {
|
|
392
|
+
this.#readMap = staging;
|
|
393
|
+
if (!silent) this.#emitMutations(addEntries, updateEntries, deleteKeys);
|
|
394
|
+
}
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
add(value, options) {
|
|
398
|
+
return this.begin((tx) => tx.add(value, options));
|
|
399
|
+
}
|
|
400
|
+
update(key, value) {
|
|
401
|
+
this.begin((tx) => tx.update(key, value));
|
|
402
|
+
}
|
|
403
|
+
del(key) {
|
|
404
|
+
this.begin((tx) => tx.del(key));
|
|
405
|
+
}
|
|
406
|
+
use(plugin) {
|
|
407
|
+
this.#onInitHandlers.push(plugin.onInit);
|
|
408
|
+
this.#onDisposeHandlers.push(plugin.onDispose);
|
|
409
|
+
if (plugin.onAdd) this.#onAddHandlers.push(plugin.onAdd);
|
|
410
|
+
if (plugin.onUpdate) this.#onUpdateHandlers.push(plugin.onUpdate);
|
|
411
|
+
if (plugin.onDelete) this.#onDeleteHandlers.push(plugin.onDelete);
|
|
412
|
+
return this;
|
|
413
|
+
}
|
|
414
|
+
async init() {
|
|
415
|
+
for (const hook of this.#onInitHandlers) await hook(this);
|
|
416
|
+
for (const query of this.#queries) this.#hydrateQuery(query);
|
|
417
|
+
return this;
|
|
418
|
+
}
|
|
419
|
+
async dispose() {
|
|
420
|
+
for (let i = this.#onDisposeHandlers.length - 1; i >= 0; i--) await this.#onDisposeHandlers[i]?.();
|
|
421
|
+
for (const query of this.#queries) {
|
|
422
|
+
query.callbacks.clear();
|
|
423
|
+
query.results.clear();
|
|
424
|
+
}
|
|
425
|
+
this.#queries.clear();
|
|
426
|
+
this.#onInitHandlers = [];
|
|
427
|
+
this.#onDisposeHandlers = [];
|
|
428
|
+
this.#onAddHandlers = [];
|
|
429
|
+
this.#onUpdateHandlers = [];
|
|
430
|
+
this.#onDeleteHandlers = [];
|
|
431
|
+
}
|
|
432
|
+
query(config) {
|
|
433
|
+
const query = {
|
|
434
|
+
where: config.where,
|
|
435
|
+
select: config.select,
|
|
436
|
+
order: config.order,
|
|
437
|
+
results: /* @__PURE__ */ new Map(),
|
|
438
|
+
callbacks: /* @__PURE__ */ new Set()
|
|
439
|
+
};
|
|
440
|
+
this.#queries.add(query);
|
|
441
|
+
this.#hydrateQuery(query);
|
|
442
|
+
return {
|
|
443
|
+
results: () => {
|
|
444
|
+
if (query.order) return Array.from(query.results).sort(([, a], [, b]) => query.order(a, b));
|
|
445
|
+
return Array.from(query.results);
|
|
446
|
+
},
|
|
447
|
+
onChange: (callback) => {
|
|
448
|
+
query.callbacks.add(callback);
|
|
449
|
+
return () => {
|
|
450
|
+
query.callbacks.delete(callback);
|
|
451
|
+
};
|
|
452
|
+
},
|
|
453
|
+
dispose: () => {
|
|
454
|
+
this.#queries.delete(query);
|
|
455
|
+
query.callbacks.clear();
|
|
456
|
+
query.results.clear();
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
#encodeValue(key, value) {
|
|
461
|
+
return encodeDoc(key, value, this.#clock.now());
|
|
462
|
+
}
|
|
463
|
+
#decodeActive(doc) {
|
|
303
464
|
if (!doc || doc["~deletedAt"]) return null;
|
|
304
465
|
return decodeDoc(doc)["~data"];
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
466
|
+
}
|
|
467
|
+
#emitMutations(addEntries, updateEntries, deleteKeys) {
|
|
468
|
+
this.#notifyQueries(addEntries, updateEntries, deleteKeys);
|
|
469
|
+
if (addEntries.length > 0) for (const handler of this.#onAddHandlers) handler(addEntries);
|
|
470
|
+
if (updateEntries.length > 0) for (const handler of this.#onUpdateHandlers) handler(updateEntries);
|
|
471
|
+
if (deleteKeys.length > 0) for (const handler of this.#onDeleteHandlers) handler(deleteKeys);
|
|
472
|
+
}
|
|
473
|
+
#notifyQueries(addEntries, updateEntries, deleteKeys) {
|
|
474
|
+
if (this.#queries.size === 0) return;
|
|
475
|
+
const dirtyQueries = /* @__PURE__ */ new Set();
|
|
476
|
+
if (addEntries.length > 0) {
|
|
477
|
+
for (const [key, value] of addEntries) for (const query of this.#queries) if (query.where(value)) {
|
|
478
|
+
const selected = this.#selectValue(query, value);
|
|
479
|
+
query.results.set(key, selected);
|
|
480
|
+
dirtyQueries.add(query);
|
|
321
481
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const silent = opts?.silent ?? false;
|
|
338
|
-
const putKeyValues = [];
|
|
339
|
-
const patchKeyValues = [];
|
|
340
|
-
const deleteKeys = [];
|
|
341
|
-
let result;
|
|
342
|
-
let shouldNotify = false;
|
|
343
|
-
kv.begin((kvTx) => {
|
|
344
|
-
const tx = createTransaction(kvTx, clock, getId, encodeValue$1, decodeActive, putKeyValues, patchKeyValues, deleteKeys);
|
|
345
|
-
result = callback(tx);
|
|
346
|
-
if (!tx.rolledBack && !silent) shouldNotify = true;
|
|
347
|
-
});
|
|
348
|
-
if (shouldNotify) {
|
|
349
|
-
if (putKeyValues.length > 0) onAddHandlers.forEach((fn) => {
|
|
350
|
-
fn(putKeyValues);
|
|
351
|
-
});
|
|
352
|
-
if (patchKeyValues.length > 0) onUpdateHandlers.forEach((fn) => {
|
|
353
|
-
fn(patchKeyValues);
|
|
354
|
-
});
|
|
355
|
-
if (deleteKeys.length > 0) onDeleteHandlers.forEach((fn) => {
|
|
356
|
-
fn(deleteKeys);
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
return result;
|
|
360
|
-
},
|
|
361
|
-
add(value, options) {
|
|
362
|
-
return this.begin((tx) => tx.add(value, options));
|
|
363
|
-
},
|
|
364
|
-
update(key, value) {
|
|
365
|
-
return this.begin((tx) => tx.update(key, value));
|
|
366
|
-
},
|
|
367
|
-
del(key) {
|
|
368
|
-
return this.begin((tx) => tx.del(key));
|
|
369
|
-
},
|
|
370
|
-
use(plugin) {
|
|
371
|
-
const { hooks: pluginHooks, methods } = plugin;
|
|
372
|
-
if (pluginHooks.onAdd || pluginHooks.onUpdate || pluginHooks.onDelete) {
|
|
373
|
-
if (pluginHooks.onAdd) {
|
|
374
|
-
const onAdd = pluginHooks.onAdd;
|
|
375
|
-
onAddHandlers.add(onAdd);
|
|
376
|
-
onDisposeHandlers.add(() => {
|
|
377
|
-
onAddHandlers.delete(onAdd);
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
if (pluginHooks.onUpdate) {
|
|
381
|
-
const onUpdate = pluginHooks.onUpdate;
|
|
382
|
-
onUpdateHandlers.add(onUpdate);
|
|
383
|
-
onDisposeHandlers.add(() => {
|
|
384
|
-
onUpdateHandlers.delete(onUpdate);
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
if (pluginHooks.onDelete) {
|
|
388
|
-
const onDelete = pluginHooks.onDelete;
|
|
389
|
-
onDeleteHandlers.add(onDelete);
|
|
390
|
-
onDisposeHandlers.add(() => {
|
|
391
|
-
onDeleteHandlers.delete(onDelete);
|
|
392
|
-
});
|
|
393
|
-
}
|
|
482
|
+
}
|
|
483
|
+
if (updateEntries.length > 0) for (const [key, value] of updateEntries) for (const query of this.#queries) {
|
|
484
|
+
const matches = query.where(value);
|
|
485
|
+
const inResults = query.results.has(key);
|
|
486
|
+
if (matches && !inResults) {
|
|
487
|
+
const selected = this.#selectValue(query, value);
|
|
488
|
+
query.results.set(key, selected);
|
|
489
|
+
dirtyQueries.add(query);
|
|
490
|
+
} else if (!matches && inResults) {
|
|
491
|
+
query.results.delete(key);
|
|
492
|
+
dirtyQueries.add(query);
|
|
493
|
+
} else if (matches && inResults) {
|
|
494
|
+
const selected = this.#selectValue(query, value);
|
|
495
|
+
query.results.set(key, selected);
|
|
496
|
+
dirtyQueries.add(query);
|
|
394
497
|
}
|
|
395
|
-
if (methods) Object.assign(this, methods);
|
|
396
|
-
onInitHandlers.add(pluginHooks.onInit);
|
|
397
|
-
onDisposeHandlers.add(pluginHooks.onDispose);
|
|
398
|
-
return this;
|
|
399
|
-
},
|
|
400
|
-
async init() {
|
|
401
|
-
for (const fn of onInitHandlers) await fn(this);
|
|
402
|
-
return this;
|
|
403
|
-
},
|
|
404
|
-
async dispose() {
|
|
405
|
-
const disposerArray = Array.from(onDisposeHandlers);
|
|
406
|
-
disposerArray.reverse();
|
|
407
|
-
for (const fn of disposerArray) await fn();
|
|
408
|
-
},
|
|
409
|
-
latestEventstamp() {
|
|
410
|
-
return clock.latest();
|
|
411
|
-
},
|
|
412
|
-
forwardClock(eventstamp) {
|
|
413
|
-
clock.forward(eventstamp);
|
|
414
498
|
}
|
|
415
|
-
|
|
499
|
+
if (deleteKeys.length > 0) {
|
|
500
|
+
for (const key of deleteKeys) for (const query of this.#queries) if (query.results.delete(key)) dirtyQueries.add(query);
|
|
501
|
+
}
|
|
502
|
+
if (dirtyQueries.size > 0) this.#runQueryCallbacks(dirtyQueries);
|
|
503
|
+
}
|
|
504
|
+
#runQueryCallbacks(dirtyQueries) {
|
|
505
|
+
for (const query of dirtyQueries) for (const callback of query.callbacks) callback();
|
|
506
|
+
}
|
|
507
|
+
#hydrateQuery(query) {
|
|
508
|
+
query.results.clear();
|
|
509
|
+
for (const [key, value] of this.entries()) if (query.where(value)) {
|
|
510
|
+
const selected = this.#selectValue(query, value);
|
|
511
|
+
query.results.set(key, selected);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
#selectValue(query, value) {
|
|
515
|
+
return query.select ? query.select(value) : value;
|
|
516
|
+
}
|
|
416
517
|
};
|
|
417
518
|
|
|
418
519
|
//#endregion
|
|
419
|
-
export {
|
|
520
|
+
export { Store, processDocument };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { c as Collection, t as Plugin } from "../../store-Dc-hIF56.js";
|
|
2
|
+
import { Storage } from "unstorage";
|
|
3
|
+
|
|
4
|
+
//#region src/plugins/unstorage/plugin.d.ts
|
|
5
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
6
|
+
type UnstorageOnBeforeSet = (data: Collection) => MaybePromise<Collection>;
|
|
7
|
+
type UnstorageOnAfterGet = (data: Collection) => MaybePromise<Collection>;
|
|
8
|
+
type UnstorageConfig = {
|
|
9
|
+
debounceMs?: number;
|
|
10
|
+
pollIntervalMs?: number;
|
|
11
|
+
onBeforeSet?: UnstorageOnBeforeSet;
|
|
12
|
+
onAfterGet?: UnstorageOnAfterGet;
|
|
13
|
+
skip?: () => boolean;
|
|
14
|
+
};
|
|
15
|
+
declare function unstoragePlugin<T>(key: string, storage: Storage<Collection>, config?: UnstorageConfig): Plugin<T>;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { type UnstorageConfig, unstoragePlugin };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
//#region src/plugins/unstorage/plugin.ts
|
|
2
|
-
|
|
2
|
+
function unstoragePlugin(key, storage, config = {}) {
|
|
3
3
|
const { debounceMs = 0, pollIntervalMs, onBeforeSet, onAfterGet, skip } = config;
|
|
4
4
|
let debounceTimer = null;
|
|
5
5
|
let pollInterval = null;
|
|
6
6
|
let store = null;
|
|
7
7
|
const persistSnapshot = async () => {
|
|
8
8
|
if (!store) return;
|
|
9
|
-
const data = store.
|
|
9
|
+
const data = store.collection();
|
|
10
10
|
const persisted = onBeforeSet !== void 0 ? await onBeforeSet(data) : data;
|
|
11
11
|
await storage.set(key, persisted);
|
|
12
12
|
};
|
|
@@ -31,7 +31,7 @@ const unstoragePlugin = (key, storage, config = {}) => {
|
|
|
31
31
|
const data = onAfterGet !== void 0 ? await onAfterGet(persisted) : persisted;
|
|
32
32
|
store.merge(data);
|
|
33
33
|
};
|
|
34
|
-
return {
|
|
34
|
+
return {
|
|
35
35
|
onInit: async (s) => {
|
|
36
36
|
store = s;
|
|
37
37
|
await pollStorage();
|
|
@@ -59,8 +59,8 @@ const unstoragePlugin = (key, storage, config = {}) => {
|
|
|
59
59
|
onDelete: () => {
|
|
60
60
|
schedulePersist();
|
|
61
61
|
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
64
|
|
|
65
65
|
//#endregion
|
|
66
66
|
export { unstoragePlugin };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
//#region src/crdt/value.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A primitive value wrapped with its eventstamp for Last-Write-Wins conflict resolution.
|
|
4
|
+
* Used as the leaf nodes in the CRDT data structure.
|
|
5
|
+
*
|
|
6
|
+
* @template T - The type of the wrapped value (primitive or complex type)
|
|
7
|
+
*/
|
|
8
|
+
type EncodedValue<T> = {
|
|
9
|
+
/** The actual value being stored */
|
|
10
|
+
"~value": T;
|
|
11
|
+
/** The eventstamp indicating when this value was last written (ISO|counter|nonce) */
|
|
12
|
+
"~eventstamp": string;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/crdt/record.d.ts
|
|
16
|
+
/**
|
|
17
|
+
* A nested object structure where each field is either an EncodedValue (leaf)
|
|
18
|
+
* or another EncodedRecord (nested object). This enables field-level
|
|
19
|
+
* Last-Write-Wins merging for complex data structures.
|
|
20
|
+
*
|
|
21
|
+
* Each field maintains its own eventstamp, allowing concurrent updates to
|
|
22
|
+
* different fields to be preserved during merge operations.
|
|
23
|
+
*/
|
|
24
|
+
type EncodedRecord = {
|
|
25
|
+
[key: string]: EncodedValue<unknown> | EncodedRecord;
|
|
26
|
+
};
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/crdt/document.d.ts
|
|
29
|
+
/**
|
|
30
|
+
* Top-level document structure with system metadata for tracking identity,
|
|
31
|
+
* data, and deletion state. Documents are the primary unit of storage and
|
|
32
|
+
* synchronization in Starling.
|
|
33
|
+
*
|
|
34
|
+
* The tilde prefix (~) distinguishes system metadata from user-defined data.
|
|
35
|
+
*/
|
|
36
|
+
type EncodedDocument = {
|
|
37
|
+
/** Unique identifier for this document */
|
|
38
|
+
"~id": string;
|
|
39
|
+
/** The document's data, either a primitive value or nested object structure */
|
|
40
|
+
"~data": EncodedValue<unknown> | EncodedRecord;
|
|
41
|
+
/** Eventstamp when this document was soft-deleted, or null if not deleted */
|
|
42
|
+
"~deletedAt": string | null;
|
|
43
|
+
};
|
|
44
|
+
declare function processDocument(doc: EncodedDocument, process: (value: EncodedValue<unknown>) => EncodedValue<unknown>): EncodedDocument;
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/crdt/collection.d.ts
|
|
47
|
+
/**
|
|
48
|
+
* A collection represents the complete state of a store:
|
|
49
|
+
* - A set of documents (including soft-deleted ones)
|
|
50
|
+
* - The highest eventstamp observed across all operations
|
|
51
|
+
*
|
|
52
|
+
* Collections are the unit of synchronization between store replicas.
|
|
53
|
+
*/
|
|
54
|
+
type Collection = {
|
|
55
|
+
/** Array of encoded documents with eventstamps and metadata */
|
|
56
|
+
"~docs": EncodedDocument[];
|
|
57
|
+
/** Latest eventstamp observed by this collection for clock synchronization */
|
|
58
|
+
"~eventstamp": string;
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/store.d.ts
|
|
62
|
+
type NotPromise<T> = T extends Promise<any> ? never : T;
|
|
63
|
+
type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;
|
|
64
|
+
type StoreAddOptions = {
|
|
65
|
+
withId?: string;
|
|
66
|
+
};
|
|
67
|
+
type StoreConfig = {
|
|
68
|
+
getId?: () => string;
|
|
69
|
+
};
|
|
70
|
+
type StoreSetTransaction<T> = {
|
|
71
|
+
add: (value: T, options?: StoreAddOptions) => string;
|
|
72
|
+
update: (key: string, value: DeepPartial<T>) => void;
|
|
73
|
+
merge: (doc: EncodedDocument) => void;
|
|
74
|
+
del: (key: string) => void;
|
|
75
|
+
get: (key: string) => T | null;
|
|
76
|
+
rollback: () => void;
|
|
77
|
+
};
|
|
78
|
+
type Plugin<T> = {
|
|
79
|
+
onInit: (store: Store<T>) => Promise<void> | void;
|
|
80
|
+
onDispose: () => Promise<void> | void;
|
|
81
|
+
onAdd?: (entries: ReadonlyArray<readonly [string, T]>) => void;
|
|
82
|
+
onUpdate?: (entries: ReadonlyArray<readonly [string, T]>) => void;
|
|
83
|
+
onDelete?: (keys: ReadonlyArray<string>) => void;
|
|
84
|
+
};
|
|
85
|
+
type QueryConfig<T, U = T> = {
|
|
86
|
+
where: (data: T) => boolean;
|
|
87
|
+
select?: (data: T) => U;
|
|
88
|
+
order?: (a: U, b: U) => number;
|
|
89
|
+
};
|
|
90
|
+
type Query<U> = {
|
|
91
|
+
results: () => Array<readonly [string, U]>;
|
|
92
|
+
onChange: (callback: () => void) => () => void;
|
|
93
|
+
dispose: () => void;
|
|
94
|
+
};
|
|
95
|
+
declare class Store<T> {
|
|
96
|
+
#private;
|
|
97
|
+
constructor(config?: StoreConfig);
|
|
98
|
+
get(key: string): T | null;
|
|
99
|
+
entries(): IterableIterator<readonly [string, T]>;
|
|
100
|
+
collection(): Collection;
|
|
101
|
+
merge(collection: Collection): void;
|
|
102
|
+
begin<R = void>(callback: (tx: StoreSetTransaction<T>) => NotPromise<R>, opts?: {
|
|
103
|
+
silent?: boolean;
|
|
104
|
+
}): NotPromise<R>;
|
|
105
|
+
add(value: T, options?: StoreAddOptions): string;
|
|
106
|
+
update(key: string, value: DeepPartial<T>): void;
|
|
107
|
+
del(key: string): void;
|
|
108
|
+
use(plugin: Plugin<T>): this;
|
|
109
|
+
init(): Promise<this>;
|
|
110
|
+
dispose(): Promise<void>;
|
|
111
|
+
query<U = T>(config: QueryConfig<T, U>): Query<U>;
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
export { StoreAddOptions as a, Collection as c, Store as i, EncodedDocument as l, Query as n, StoreConfig as o, QueryConfig as r, StoreSetTransaction as s, Plugin as t, processDocument as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byearlybird/starling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -11,15 +11,10 @@
|
|
|
11
11
|
"import": "./dist/index.js",
|
|
12
12
|
"default": "./dist/index.js"
|
|
13
13
|
},
|
|
14
|
-
"./plugin-query": {
|
|
15
|
-
"types": "./dist/plugin-query.d.ts",
|
|
16
|
-
"import": "./dist/plugin-query.js",
|
|
17
|
-
"default": "./dist/plugin-query.js"
|
|
18
|
-
},
|
|
19
14
|
"./plugin-unstorage": {
|
|
20
|
-
"types": "./dist/plugin
|
|
21
|
-
"import": "./dist/plugin
|
|
22
|
-
"default": "./dist/plugin
|
|
15
|
+
"types": "./dist/plugins/unstorage/plugin.d.ts",
|
|
16
|
+
"import": "./dist/plugins/unstorage/plugin.js",
|
|
17
|
+
"default": "./dist/plugins/unstorage/plugin.js"
|
|
23
18
|
}
|
|
24
19
|
},
|
|
25
20
|
"files": [
|
package/dist/plugin-query.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { t as Plugin } from "./store-BGsRIZeM.js";
|
|
2
|
-
|
|
3
|
-
//#region src/plugins/query/plugin.d.ts
|
|
4
|
-
type QueryConfig<T, U = T> = {
|
|
5
|
-
where: (data: T) => boolean;
|
|
6
|
-
select?: (data: T) => U;
|
|
7
|
-
order?: (a: U, b: U) => number;
|
|
8
|
-
};
|
|
9
|
-
type Query<U> = {
|
|
10
|
-
results: () => Map<string, U>;
|
|
11
|
-
onChange: (callback: () => void) => () => void;
|
|
12
|
-
dispose: () => void;
|
|
13
|
-
};
|
|
14
|
-
type QueryMethods<T> = {
|
|
15
|
-
query: <U = T>(config: QueryConfig<T, U>) => Query<U>;
|
|
16
|
-
};
|
|
17
|
-
declare const queryPlugin: <T>() => Plugin<T, QueryMethods<T>>;
|
|
18
|
-
//#endregion
|
|
19
|
-
export { type Query, type QueryConfig, type QueryMethods, queryPlugin };
|
package/dist/plugin-query.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
//#region src/plugins/query/plugin.ts
|
|
2
|
-
const queryPlugin = () => {
|
|
3
|
-
const queries = /* @__PURE__ */ new Set();
|
|
4
|
-
let store = null;
|
|
5
|
-
const hydrateQuery = (query) => {
|
|
6
|
-
if (!store) return;
|
|
7
|
-
query.results.clear();
|
|
8
|
-
for (const [key, value] of store.entries()) if (query.where(value)) {
|
|
9
|
-
const selected = query.select ? query.select(value) : value;
|
|
10
|
-
query.results.set(key, selected);
|
|
11
|
-
}
|
|
12
|
-
};
|
|
13
|
-
const runCallbacks = (dirtyQueries) => {
|
|
14
|
-
for (const query of dirtyQueries) for (const callback of query.callbacks) callback();
|
|
15
|
-
dirtyQueries.clear();
|
|
16
|
-
};
|
|
17
|
-
const onAdd = (entries) => {
|
|
18
|
-
const dirtyQueries = /* @__PURE__ */ new Set();
|
|
19
|
-
for (const [key, value] of entries) for (const q of queries) if (q.where(value)) {
|
|
20
|
-
const selected = q.select ? q.select(value) : value;
|
|
21
|
-
q.results.set(key, selected);
|
|
22
|
-
dirtyQueries.add(q);
|
|
23
|
-
}
|
|
24
|
-
runCallbacks(dirtyQueries);
|
|
25
|
-
};
|
|
26
|
-
const onUpdate = (entries) => {
|
|
27
|
-
const dirtyQueries = /* @__PURE__ */ new Set();
|
|
28
|
-
for (const [key, value] of entries) for (const q of queries) {
|
|
29
|
-
const matches = q.where(value);
|
|
30
|
-
const inResults = q.results.has(key);
|
|
31
|
-
if (matches && !inResults) {
|
|
32
|
-
const selected = q.select ? q.select(value) : value;
|
|
33
|
-
q.results.set(key, selected);
|
|
34
|
-
dirtyQueries.add(q);
|
|
35
|
-
} else if (!matches && inResults) {
|
|
36
|
-
q.results.delete(key);
|
|
37
|
-
dirtyQueries.add(q);
|
|
38
|
-
} else if (matches && inResults) {
|
|
39
|
-
const selected = q.select ? q.select(value) : value;
|
|
40
|
-
q.results.set(key, selected);
|
|
41
|
-
dirtyQueries.add(q);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
runCallbacks(dirtyQueries);
|
|
45
|
-
};
|
|
46
|
-
const onDelete = (keys) => {
|
|
47
|
-
const dirtyQueries = /* @__PURE__ */ new Set();
|
|
48
|
-
for (const key of keys) for (const q of queries) if (q.results.has(key)) {
|
|
49
|
-
q.results.delete(key);
|
|
50
|
-
dirtyQueries.add(q);
|
|
51
|
-
}
|
|
52
|
-
runCallbacks(dirtyQueries);
|
|
53
|
-
};
|
|
54
|
-
return {
|
|
55
|
-
hooks: {
|
|
56
|
-
onInit: (s) => {
|
|
57
|
-
store = s;
|
|
58
|
-
for (const q of queries) hydrateQuery(q);
|
|
59
|
-
},
|
|
60
|
-
onDispose: () => {
|
|
61
|
-
queries.clear();
|
|
62
|
-
store = null;
|
|
63
|
-
},
|
|
64
|
-
onAdd,
|
|
65
|
-
onUpdate,
|
|
66
|
-
onDelete
|
|
67
|
-
},
|
|
68
|
-
methods: { query: (config) => {
|
|
69
|
-
const query = {
|
|
70
|
-
where: config.where,
|
|
71
|
-
...config.select && { select: config.select },
|
|
72
|
-
...config.order && { order: config.order },
|
|
73
|
-
results: /* @__PURE__ */ new Map(),
|
|
74
|
-
callbacks: /* @__PURE__ */ new Set()
|
|
75
|
-
};
|
|
76
|
-
queries.add(query);
|
|
77
|
-
hydrateQuery(query);
|
|
78
|
-
return {
|
|
79
|
-
results: () => {
|
|
80
|
-
if (query.order) {
|
|
81
|
-
const orderFn = query.order;
|
|
82
|
-
const sorted = Array.from(query.results).sort(([, a], [, b]) => orderFn(a, b));
|
|
83
|
-
return new Map(sorted);
|
|
84
|
-
} else return new Map(query.results);
|
|
85
|
-
},
|
|
86
|
-
onChange: (callback) => {
|
|
87
|
-
query.callbacks.add(callback);
|
|
88
|
-
return () => {
|
|
89
|
-
query.callbacks.delete(callback);
|
|
90
|
-
};
|
|
91
|
-
},
|
|
92
|
-
dispose: () => {
|
|
93
|
-
queries.delete(query);
|
|
94
|
-
query.callbacks.clear();
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
} }
|
|
98
|
-
};
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
//#endregion
|
|
102
|
-
export { queryPlugin };
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { a as StoreSnapshot, t as Plugin } from "./store-BGsRIZeM.js";
|
|
2
|
-
import { Storage } from "unstorage";
|
|
3
|
-
|
|
4
|
-
//#region src/plugins/unstorage/plugin.d.ts
|
|
5
|
-
type MaybePromise<T> = T | Promise<T>;
|
|
6
|
-
type UnstorageOnBeforeSet = (data: StoreSnapshot) => MaybePromise<StoreSnapshot>;
|
|
7
|
-
type UnstorageOnAfterGet = (data: StoreSnapshot) => MaybePromise<StoreSnapshot>;
|
|
8
|
-
type UnstorageConfig = {
|
|
9
|
-
debounceMs?: number;
|
|
10
|
-
pollIntervalMs?: number;
|
|
11
|
-
onBeforeSet?: UnstorageOnBeforeSet;
|
|
12
|
-
onAfterGet?: UnstorageOnAfterGet;
|
|
13
|
-
skip?: () => boolean;
|
|
14
|
-
};
|
|
15
|
-
declare const unstoragePlugin: <T>(key: string, storage: Storage<StoreSnapshot>, config?: UnstorageConfig) => Plugin<T>;
|
|
16
|
-
//#endregion
|
|
17
|
-
export { type UnstorageConfig, unstoragePlugin };
|
package/dist/store-BGsRIZeM.d.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
//#region src/value.d.ts
|
|
2
|
-
type EncodedValue<T> = {
|
|
3
|
-
"~value": T;
|
|
4
|
-
"~eventstamp": string;
|
|
5
|
-
};
|
|
6
|
-
//#endregion
|
|
7
|
-
//#region src/record.d.ts
|
|
8
|
-
type EncodedRecord = {
|
|
9
|
-
[key: string]: EncodedValue<unknown> | EncodedRecord;
|
|
10
|
-
};
|
|
11
|
-
//#endregion
|
|
12
|
-
//#region src/document.d.ts
|
|
13
|
-
type EncodedDocument = {
|
|
14
|
-
"~id": string;
|
|
15
|
-
"~data": EncodedValue<unknown> | EncodedRecord;
|
|
16
|
-
"~deletedAt": string | null;
|
|
17
|
-
};
|
|
18
|
-
declare const processDocument: (doc: EncodedDocument, process: (value: EncodedValue<unknown>) => EncodedValue<unknown>) => EncodedDocument;
|
|
19
|
-
//#endregion
|
|
20
|
-
//#region src/transaction.d.ts
|
|
21
|
-
type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;
|
|
22
|
-
type StorePutOptions = {
|
|
23
|
-
withId?: string;
|
|
24
|
-
};
|
|
25
|
-
type StoreSetTransaction<T> = {
|
|
26
|
-
add: (value: T, options?: StorePutOptions) => string;
|
|
27
|
-
update: (key: string, value: DeepPartial<T>) => void;
|
|
28
|
-
merge: (doc: EncodedDocument) => void;
|
|
29
|
-
del: (key: string) => void;
|
|
30
|
-
get: (key: string) => T | null;
|
|
31
|
-
rollback: () => void;
|
|
32
|
-
};
|
|
33
|
-
//#endregion
|
|
34
|
-
//#region src/store.d.ts
|
|
35
|
-
/**
|
|
36
|
-
* Type constraint to prevent Promise returns from set callbacks.
|
|
37
|
-
* Transactions must be synchronous operations.
|
|
38
|
-
*/
|
|
39
|
-
type NotPromise<T> = T extends Promise<any> ? never : T;
|
|
40
|
-
/**
|
|
41
|
-
* Plugin lifecycle and event hooks.
|
|
42
|
-
* All hooks are optional except onInit and onDispose, which are required.
|
|
43
|
-
*/
|
|
44
|
-
type PluginHooks<T> = {
|
|
45
|
-
onInit: (store: Store<T>) => Promise<void> | void;
|
|
46
|
-
onDispose: () => Promise<void> | void;
|
|
47
|
-
onAdd?: (entries: ReadonlyArray<readonly [string, T]>) => void;
|
|
48
|
-
onUpdate?: (entries: ReadonlyArray<readonly [string, T]>) => void;
|
|
49
|
-
onDelete?: (keys: ReadonlyArray<string>) => void;
|
|
50
|
-
};
|
|
51
|
-
type PluginMethods = Record<string, (...args: any[]) => any>;
|
|
52
|
-
type Plugin<T, M extends PluginMethods = {}> = {
|
|
53
|
-
hooks: PluginHooks<T>;
|
|
54
|
-
methods?: M;
|
|
55
|
-
};
|
|
56
|
-
/**
|
|
57
|
-
* Complete persistent state of a store.
|
|
58
|
-
* Contains all encoded documents (including deleted ones with ~deletedAt metadata)
|
|
59
|
-
* and the latest eventstamp for clock synchronization during merges.
|
|
60
|
-
*/
|
|
61
|
-
type StoreSnapshot = {
|
|
62
|
-
docs: EncodedDocument[];
|
|
63
|
-
latestEventstamp: string;
|
|
64
|
-
};
|
|
65
|
-
type Store<T, Extended = {}> = {
|
|
66
|
-
get: (key: string) => T | null;
|
|
67
|
-
begin: <R = void>(callback: (tx: StoreSetTransaction<T>) => NotPromise<R>, opts?: {
|
|
68
|
-
silent?: boolean;
|
|
69
|
-
}) => NotPromise<R>;
|
|
70
|
-
add: (value: T, options?: StorePutOptions) => string;
|
|
71
|
-
update: (key: string, value: DeepPartial<T>) => void;
|
|
72
|
-
del: (key: string) => void;
|
|
73
|
-
entries: () => IterableIterator<readonly [string, T]>;
|
|
74
|
-
snapshot: () => StoreSnapshot;
|
|
75
|
-
merge: (snapshot: StoreSnapshot) => void;
|
|
76
|
-
use: <M extends PluginMethods>(plugin: Plugin<T, M>) => Store<T, Extended & M>;
|
|
77
|
-
init: () => Promise<Store<T, Extended>>;
|
|
78
|
-
dispose: () => Promise<void>;
|
|
79
|
-
latestEventstamp: () => string;
|
|
80
|
-
forwardClock: (eventstamp: string) => void;
|
|
81
|
-
} & Extended;
|
|
82
|
-
declare const createStore: <T>(config?: {
|
|
83
|
-
getId?: () => string;
|
|
84
|
-
}) => Store<T, {}>;
|
|
85
|
-
//#endregion
|
|
86
|
-
export { StoreSnapshot as a, processDocument as c, Store as i, PluginHooks as n, createStore as o, PluginMethods as r, EncodedDocument as s, Plugin as t };
|