@byearlybird/starling 0.6.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,74 +1,2 @@
1
- //#region src/value.d.ts
2
- type EncodedValue<T$1> = {
3
- "~value": T$1;
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$1> = T$1 extends object ? { [P in keyof T$1]?: DeepPartial<T$1[P]> } : T$1;
22
- type StorePutOptions = {
23
- withId?: string;
24
- };
25
- type StoreSetTransaction<T$1> = {
26
- add: (value: T$1, options?: StorePutOptions) => string;
27
- update: (key: string, value: DeepPartial<T$1>) => void;
28
- merge: (doc: EncodedDocument) => void;
29
- del: (key: string) => void;
30
- get: (key: string) => T$1 | 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$1> = T$1 extends Promise<any> ? never : T$1;
40
- /**
41
- * Plugin lifecycle and event hooks.
42
- * All hooks are optional except onInit and onDispose, which are required.
43
- */
44
- type PluginHooks<T$1> = {
45
- onInit: (store: Store<T$1>) => Promise<void> | void;
46
- onDispose: () => Promise<void> | void;
47
- onAdd?: (entries: ReadonlyArray<readonly [string, T$1]>) => void;
48
- onUpdate?: (entries: ReadonlyArray<readonly [string, T$1]>) => void;
49
- onDelete?: (keys: ReadonlyArray<string>) => void;
50
- };
51
- type PluginMethods = Record<string, (...args: any[]) => any>;
52
- type Plugin<T$1, M$1 extends PluginMethods = {}> = {
53
- hooks: PluginHooks<T$1>;
54
- methods?: M$1;
55
- };
56
- type Store<T$1, Extended = {}> = {
57
- get: (key: string) => T$1 | null;
58
- begin: <R = void>(callback: (tx: StoreSetTransaction<T$1>) => NotPromise<R>, opts?: {
59
- silent?: boolean;
60
- }) => NotPromise<R>;
61
- add: (value: T$1, options?: StorePutOptions) => string;
62
- update: (key: string, value: DeepPartial<T$1>) => void;
63
- del: (key: string) => void;
64
- entries: () => IterableIterator<readonly [string, T$1]>;
65
- snapshot: () => EncodedDocument[];
66
- use: <M extends PluginMethods>(plugin: Plugin<T$1, M>) => Store<T$1, Extended & M>;
67
- init: () => Promise<Store<T$1, Extended>>;
68
- dispose: () => Promise<void>;
69
- } & Extended;
70
- declare const createStore: <T>(config?: {
71
- getId?: () => string;
72
- }) => Store<T, {}>;
73
- //#endregion
74
- export { type EncodedDocument, Plugin, PluginHooks, PluginMethods, Store, createStore, processDocument };
1
+ import { a as StoreSnapshot, c as processDocument, i as Store, n as PluginHooks, o as createStore, r as PluginMethods, s as EncodedDocument, t as Plugin } from "./store-CMzcvcsT.js";
2
+ export { type EncodedDocument, Plugin, PluginHooks, PluginMethods, Store, StoreSnapshot, createStore, processDocument };
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ const encodeValue = (value, eventstamp) => ({
8
8
  "~eventstamp": eventstamp
9
9
  });
10
10
  const decodeValue = (value) => value["~value"];
11
- const mergeValues = (into, from) => into["~eventstamp"] > from["~eventstamp"] ? into : from;
11
+ const mergeValues = (into, from) => into["~eventstamp"] > from["~eventstamp"] ? [into, into["~eventstamp"]] : [from, from["~eventstamp"]];
12
12
  const isEncodedValue = (value) => !!(typeof value === "object" && value !== null && "~value" in value && "~eventstamp" in value);
13
13
 
14
14
  //#endregion
@@ -62,14 +62,21 @@ const decodeRecord = (obj) => {
62
62
  };
63
63
  const mergeRecords = (into, from) => {
64
64
  const result = {};
65
+ let greatestEventstamp = null;
65
66
  const step = (v1, v2, output) => {
66
67
  for (const key in v1) {
67
68
  if (!Object.hasOwn(v1, key)) continue;
68
69
  const value1 = v1[key];
69
70
  const value2 = v2[key];
70
- if (isEncodedValue(value1) && isEncodedValue(value2)) output[key] = mergeValues(value1, value2);
71
- else if (isEncodedValue(value1)) output[key] = value1;
72
- else if (isObject(value1) && isObject(value2)) {
71
+ if (isEncodedValue(value1) && isEncodedValue(value2)) {
72
+ const [win, eventstamp] = mergeValues(value1, value2);
73
+ output[key] = win;
74
+ if (!greatestEventstamp || eventstamp > greatestEventstamp) greatestEventstamp = eventstamp;
75
+ } else if (isEncodedValue(value1)) {
76
+ output[key] = value1;
77
+ const eventstamp = value1["~eventstamp"];
78
+ if (!greatestEventstamp || eventstamp > greatestEventstamp) greatestEventstamp = eventstamp;
79
+ } else if (isObject(value1) && isObject(value2)) {
73
80
  output[key] = {};
74
81
  step(value1, value2, output[key]);
75
82
  } else if (value1) output[key] = value1;
@@ -77,11 +84,17 @@ const mergeRecords = (into, from) => {
77
84
  for (const key in v2) {
78
85
  if (!Object.hasOwn(v2, key) || Object.hasOwn(output, key)) continue;
79
86
  const value = v2[key];
80
- if (value !== void 0) output[key] = value;
87
+ if (value !== void 0) {
88
+ output[key] = value;
89
+ if (isEncodedValue(value)) {
90
+ const eventstamp = value["~eventstamp"];
91
+ if (!greatestEventstamp || eventstamp > greatestEventstamp) greatestEventstamp = eventstamp;
92
+ }
93
+ }
81
94
  }
82
95
  };
83
96
  step(into, from, result);
84
- return result;
97
+ return [result, greatestEventstamp];
85
98
  };
86
99
 
87
100
  //#endregion
@@ -100,13 +113,17 @@ const mergeDocs = (into, from) => {
100
113
  const intoIsValue = isEncodedValue(into["~data"]);
101
114
  const fromIsValue = isEncodedValue(from["~data"]);
102
115
  if (intoIsValue !== fromIsValue) throw new Error("Merge error: Incompatible types");
103
- const mergedData = intoIsValue && fromIsValue ? mergeValues(into["~data"], from["~data"]) : mergeRecords(into["~data"], from["~data"]);
116
+ const [mergedData, dataEventstamp] = intoIsValue && fromIsValue ? mergeValues(into["~data"], from["~data"]) : mergeRecords(into["~data"], from["~data"]);
104
117
  const mergedDeletedAt = into["~deletedAt"] && from["~deletedAt"] ? into["~deletedAt"] > from["~deletedAt"] ? into["~deletedAt"] : from["~deletedAt"] : into["~deletedAt"] || from["~deletedAt"] || null;
105
- return {
118
+ let greatestEventstamp = dataEventstamp;
119
+ if (mergedDeletedAt) {
120
+ if (!greatestEventstamp || mergedDeletedAt > greatestEventstamp) greatestEventstamp = mergedDeletedAt;
121
+ }
122
+ return [{
106
123
  "~id": into["~id"],
107
124
  "~data": mergedData,
108
125
  "~deletedAt": mergedDeletedAt
109
- };
126
+ }, greatestEventstamp];
110
127
  };
111
128
  const deleteDoc = (doc, eventstamp) => ({
112
129
  "~id": doc["~id"],
@@ -144,13 +161,12 @@ const createClock = () => {
144
161
  let lastMs = Date.now();
145
162
  return {
146
163
  now: () => {
147
- const nowMs = Date.now();
148
- if (nowMs <= lastMs) counter++;
149
- else {
150
- lastMs = nowMs;
164
+ const wallMs = Date.now();
165
+ if (wallMs > lastMs) {
166
+ lastMs = wallMs;
151
167
  counter = 0;
152
- }
153
- return encodeEventstamp(nowMs, counter);
168
+ } else counter++;
169
+ return encodeEventstamp(lastMs, counter);
154
170
  },
155
171
  latest() {
156
172
  return encodeEventstamp(lastMs, counter);
@@ -190,10 +206,19 @@ const createKV = (iterable) => {
190
206
  return staging.get(key) ?? null;
191
207
  },
192
208
  set(key, value, opts) {
193
- if (opts?.replace) staging.set(key, value);
194
- else {
209
+ if (opts?.replace) {
210
+ staging.set(key, value);
211
+ return null;
212
+ } else {
195
213
  const prev = staging.get(key);
196
- staging.set(key, prev ? mergeDocs(prev, value) : value);
214
+ if (prev) {
215
+ const [merged, eventstamp] = mergeDocs(prev, value);
216
+ staging.set(key, merged);
217
+ return eventstamp;
218
+ } else {
219
+ staging.set(key, value);
220
+ return null;
221
+ }
197
222
  }
198
223
  },
199
224
  del(key, eventstamp) {
@@ -286,7 +311,10 @@ const createStore = (config = {}) => {
286
311
  return iterator();
287
312
  },
288
313
  snapshot() {
289
- return Array.from(kv.values());
314
+ return {
315
+ docs: Array.from(kv.values()),
316
+ latestEventstamp: clock.latest()
317
+ };
290
318
  },
291
319
  begin(callback, opts) {
292
320
  const silent = opts?.silent ?? false;
@@ -360,6 +388,12 @@ const createStore = (config = {}) => {
360
388
  const disposerArray = Array.from(onDisposeHandlers);
361
389
  disposerArray.reverse();
362
390
  for (const fn of disposerArray) await fn();
391
+ },
392
+ latestEventstamp() {
393
+ return clock.latest();
394
+ },
395
+ forwardClock(eventstamp) {
396
+ clock.forward(eventstamp);
363
397
  }
364
398
  };
365
399
  };
@@ -0,0 +1,19 @@
1
+ import { t as Plugin } from "./store-CMzcvcsT.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 };
@@ -0,0 +1,102 @@
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 };
@@ -0,0 +1,16 @@
1
+ import { a as StoreSnapshot, t as Plugin } from "./store-CMzcvcsT.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
+ };
14
+ declare const unstoragePlugin: <T>(key: string, storage: Storage<StoreSnapshot>, config?: UnstorageConfig) => Plugin<T>;
15
+ //#endregion
16
+ export { type UnstorageConfig, unstoragePlugin };
@@ -0,0 +1,68 @@
1
+ //#region src/plugins/unstorage/plugin.ts
2
+ const unstoragePlugin = (key, storage, config = {}) => {
3
+ const { debounceMs = 0, pollIntervalMs, onBeforeSet, onAfterGet } = config;
4
+ let debounceTimer = null;
5
+ let pollInterval = null;
6
+ let store = null;
7
+ const persistSnapshot = async () => {
8
+ if (!store) return;
9
+ const data = store.snapshot();
10
+ const persisted = onBeforeSet !== void 0 ? await onBeforeSet(data) : data;
11
+ await storage.set(key, persisted);
12
+ };
13
+ const schedulePersist = () => {
14
+ const runPersist = () => {
15
+ debounceTimer = null;
16
+ persistSnapshot();
17
+ };
18
+ if (debounceMs === 0) {
19
+ runPersist();
20
+ return;
21
+ }
22
+ if (debounceTimer !== null) clearTimeout(debounceTimer);
23
+ debounceTimer = setTimeout(runPersist, debounceMs);
24
+ };
25
+ const pollStorage = async () => {
26
+ if (!store) return;
27
+ const persisted = await storage.get(key);
28
+ if (!persisted) return;
29
+ const data = onAfterGet !== void 0 ? await onAfterGet(persisted) : persisted;
30
+ if (!data || !data.docs || data.docs.length === 0) return;
31
+ store.forwardClock(data.latestEventstamp);
32
+ store.begin((tx) => {
33
+ for (const doc of data.docs) tx.merge(doc);
34
+ });
35
+ };
36
+ return { hooks: {
37
+ onInit: async (s) => {
38
+ store = s;
39
+ await pollStorage();
40
+ if (pollIntervalMs !== void 0 && pollIntervalMs > 0) pollInterval = setInterval(() => {
41
+ pollStorage();
42
+ }, pollIntervalMs);
43
+ },
44
+ onDispose: () => {
45
+ if (debounceTimer !== null) {
46
+ clearTimeout(debounceTimer);
47
+ debounceTimer = null;
48
+ }
49
+ if (pollInterval !== null) {
50
+ clearInterval(pollInterval);
51
+ pollInterval = null;
52
+ }
53
+ store = null;
54
+ },
55
+ onAdd: () => {
56
+ schedulePersist();
57
+ },
58
+ onUpdate: () => {
59
+ schedulePersist();
60
+ },
61
+ onDelete: () => {
62
+ schedulePersist();
63
+ }
64
+ } };
65
+ };
66
+
67
+ //#endregion
68
+ export { unstoragePlugin };
@@ -0,0 +1,85 @@
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
+ use: <M extends PluginMethods>(plugin: Plugin<T, M>) => Store<T, Extended & M>;
76
+ init: () => Promise<Store<T, Extended>>;
77
+ dispose: () => Promise<void>;
78
+ latestEventstamp: () => string;
79
+ forwardClock: (eventstamp: string) => void;
80
+ } & Extended;
81
+ declare const createStore: <T>(config?: {
82
+ getId?: () => string;
83
+ }) => Store<T, {}>;
84
+ //#endregion
85
+ 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 };
package/package.json CHANGED
@@ -1,10 +1,27 @@
1
1
  {
2
2
  "name": "@byearlybird/starling",
3
- "version": "0.6.1",
3
+ "version": "0.7.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
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
+ "./plugin-unstorage": {
20
+ "types": "./dist/plugin-unstorage.d.ts",
21
+ "import": "./dist/plugin-unstorage.js",
22
+ "default": "./dist/plugin-unstorage.js"
23
+ }
24
+ },
8
25
  "files": [
9
26
  "dist"
10
27
  ],
@@ -12,6 +29,14 @@
12
29
  "build": "bun run build.ts",
13
30
  "prepublishOnly": "bun run build.ts"
14
31
  },
32
+ "peerDependencies": {
33
+ "unstorage": "^1.17.1"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "unstorage": {
37
+ "optional": true
38
+ }
39
+ },
15
40
  "publishConfig": {
16
41
  "access": "public"
17
42
  }