@signaldb/core 2.0.0-beta.13 → 2.0.0-beta.14

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.
Files changed (82) hide show
  1. package/dist/.vite/manifest.json +72 -46
  2. package/dist/AsyncDataAdapter.d.ts +11 -2
  3. package/dist/Collection/Cursor.d.ts +11 -1
  4. package/dist/Collection/Observer.d.ts +31 -0
  5. package/dist/DataAdapter.d.ts +15 -2
  6. package/dist/WorkerDataAdapter.d.ts +25 -2
  7. package/dist/WorkerDataAdapterHost.d.ts +2 -0
  8. package/dist/index.cjs.js +15 -15
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.mjs +15 -15
  11. package/dist/index10.cjs.js +7 -19
  12. package/dist/index10.mjs +7 -19
  13. package/dist/index11.cjs.js +19 -41
  14. package/dist/index11.mjs +19 -41
  15. package/dist/index12.cjs.js +41 -18
  16. package/dist/index12.mjs +41 -18
  17. package/dist/index13.cjs.js +18 -41
  18. package/dist/index13.mjs +18 -41
  19. package/dist/index14.cjs.js +39 -80
  20. package/dist/index14.mjs +39 -80
  21. package/dist/index15.cjs.js +82 -11
  22. package/dist/index15.mjs +82 -11
  23. package/dist/index16.cjs.js +11 -132
  24. package/dist/index16.mjs +11 -132
  25. package/dist/index17.cjs.js +127 -38
  26. package/dist/index17.mjs +127 -38
  27. package/dist/index18.cjs.js +43 -11
  28. package/dist/index18.mjs +43 -11
  29. package/dist/index19.cjs.js +10 -18
  30. package/dist/index19.mjs +11 -19
  31. package/dist/index20.cjs.js +19 -33
  32. package/dist/index20.mjs +19 -33
  33. package/dist/index21.cjs.js +33 -31
  34. package/dist/index21.mjs +33 -31
  35. package/dist/index22.cjs.js +31 -21
  36. package/dist/index22.mjs +31 -21
  37. package/dist/index23.cjs.js +16 -14
  38. package/dist/index23.mjs +16 -14
  39. package/dist/index24.cjs.js +15 -338
  40. package/dist/index24.mjs +15 -338
  41. package/dist/index25.cjs.js +122 -554
  42. package/dist/index25.mjs +121 -554
  43. package/dist/index26.cjs.js +34 -7
  44. package/dist/index26.mjs +34 -7
  45. package/dist/index27.cjs.js +339 -7
  46. package/dist/index27.mjs +339 -7
  47. package/dist/index28.cjs.js +550 -83
  48. package/dist/index28.mjs +550 -82
  49. package/dist/index29.cjs.js +8 -422
  50. package/dist/index29.mjs +8 -422
  51. package/dist/index30.cjs.js +7 -68
  52. package/dist/index30.mjs +7 -68
  53. package/dist/index31.cjs.js +86 -28
  54. package/dist/index31.mjs +85 -28
  55. package/dist/index32.cjs.js +446 -278
  56. package/dist/index32.mjs +446 -278
  57. package/dist/index33.cjs.js +68 -17
  58. package/dist/index33.mjs +68 -17
  59. package/dist/index34.cjs.js +460 -304
  60. package/dist/index34.mjs +460 -304
  61. package/dist/index35.cjs.js +14 -532
  62. package/dist/index35.mjs +14 -532
  63. package/dist/index36.cjs.js +389 -0
  64. package/dist/index36.mjs +389 -0
  65. package/dist/index37.cjs.js +539 -0
  66. package/dist/index37.mjs +539 -0
  67. package/dist/index4.cjs.js +199 -140
  68. package/dist/index4.mjs +195 -140
  69. package/dist/index5.cjs.js +152 -253
  70. package/dist/index5.mjs +152 -253
  71. package/dist/index6.cjs.js +267 -89
  72. package/dist/index6.mjs +267 -89
  73. package/dist/index7.cjs.js +121 -29
  74. package/dist/index7.mjs +121 -29
  75. package/dist/index8.cjs.js +29 -8
  76. package/dist/index8.mjs +29 -8
  77. package/dist/index9.cjs.js +8 -7
  78. package/dist/index9.mjs +8 -7
  79. package/dist/utils/incrementalQueryUpdate.d.ts +60 -0
  80. package/dist/utils/projectItems.d.ts +12 -0
  81. package/dist/utils/queryDelta.d.ts +83 -0
  82. package/package.json +1 -1
package/dist/index7.mjs CHANGED
@@ -1,33 +1,125 @@
1
- //#region src/utils/createSignal.ts
1
+ //#region src/utils/EventEmitter.ts
2
2
  /**
3
- * Creates a reactive signal for managing state and triggering dependencies.
4
- * The signal holds a value and provides methods to get and set the value,
5
- * with optional equality checks and dependency tracking.
6
- * @template T - The type of the value held by the signal.
7
- * @param reactivityAdapter - An optional reactivity adapter for managing dependencies.
8
- * @param initialValue - The initial value of the signal.
9
- * @param isEqual - A custom equality function to determine if the new value is different
10
- * from the current value (default is `Object.is`).
11
- * @returns A signal object with `get` and `set` methods to manage the value.
3
+ * A strongly‑typed EventEmitter.
12
4
  */
13
- function createSignal(reactivityAdapter, initialValue, isEqual = Object.is) {
14
- let value = initialValue;
15
- const dependency = reactivityAdapter?.create();
16
- const isInReactiveScope = () => {
17
- if (!reactivityAdapter?.isInScope) return true;
18
- return reactivityAdapter.isInScope();
19
- };
20
- return {
21
- get() {
22
- if (dependency && isInReactiveScope()) dependency.depend();
23
- return value;
24
- },
25
- set(newValue) {
26
- if (isEqual(value, newValue)) return;
27
- value = newValue;
28
- if (dependency) dependency.notify();
5
+ var EventEmitter = class {
6
+ _maxListeners = 100;
7
+ /**
8
+ * We store a set of the listeners for each event.
9
+ */
10
+ _listenerStore = /* @__PURE__ */ new Map();
11
+ setMaxListeners(max) {
12
+ this._maxListeners = max;
13
+ return this;
14
+ }
15
+ /**
16
+ * Subscribe to an event with a listener function.
17
+ * @param eventName The event name (key of E).
18
+ * @param listener A function that receives the emitted arguments.
19
+ * @returns The emitter instance (for chaining).
20
+ */
21
+ on(eventName, listener) {
22
+ let listenersSet = this._listenerStore.get(eventName);
23
+ if (!listenersSet) {
24
+ listenersSet = /* @__PURE__ */ new Set();
25
+ this._listenerStore.set(eventName, listenersSet);
29
26
  }
30
- };
31
- }
27
+ listenersSet.add(listener);
28
+ if (listenersSet.size > this._maxListeners) console.warn(`Possible EventEmitter memory leak detected. ${listenersSet.size} ${String(eventName)} listeners added. Use emitter.setMaxListeners() to increase limit.`);
29
+ return this;
30
+ }
31
+ /**
32
+ * Subscribe to an event with a listener function.
33
+ * @param eventName The event name (key of E).
34
+ * @param listener A function that receives the emitted arguments.
35
+ * @returns The emitter instance (for chaining).
36
+ */
37
+ addListener(eventName, listener) {
38
+ return this.on(eventName, listener);
39
+ }
40
+ /**
41
+ * Subscribe to an event, handling it only once. Automatically removes
42
+ * the listener after it fires the first time.
43
+ * @param eventName The event name (key of E).
44
+ * @param listener A function that receives the emitted arguments.
45
+ * @returns The emitter instance (for chaining).
46
+ */
47
+ once(eventName, listener) {
48
+ const onceWrapper = ((...args) => {
49
+ listener(...args);
50
+ this.off(eventName, onceWrapper);
51
+ });
52
+ return this.on(eventName, onceWrapper);
53
+ }
54
+ /**
55
+ * Unsubscribe a previously subscribed listener.
56
+ * @param eventName The event name (key of E).
57
+ * @param listener The original function passed to `on` or `once`.
58
+ * @returns The emitter instance (for chaining).
59
+ */
60
+ off(eventName, listener) {
61
+ const listenersSet = this._listenerStore.get(eventName);
62
+ if (!listenersSet) return this;
63
+ listenersSet.delete(listener);
64
+ if (listenersSet.size === 0) this._listenerStore.delete(eventName);
65
+ return this;
66
+ }
67
+ /**
68
+ * Unsubscribe a previously subscribed listener.
69
+ * @param eventName The event name (key of E).
70
+ * @param listener The original function passed to `on` or `once`.
71
+ * @returns The emitter instance (for chaining).
72
+ */
73
+ removeListener(eventName, listener) {
74
+ return this.off(eventName, listener);
75
+ }
76
+ /**
77
+ * Emit (dispatch) an event with a variable number of arguments.
78
+ * @param eventName The event name (key of E).
79
+ * @param args The arguments to pass to subscribed listeners.
80
+ */
81
+ emit(eventName, ...args) {
82
+ this.listeners(eventName).forEach((listener) => {
83
+ listener(...args);
84
+ });
85
+ }
86
+ /**
87
+ * Returns the array of listener functions currently registered for a given event.
88
+ * @param eventName The event name (key of E).
89
+ * @returns An array of listener functions.
90
+ */
91
+ listeners(eventName) {
92
+ const listenersSet = this._listenerStore.get(eventName);
93
+ if (!listenersSet) return [];
94
+ return [...listenersSet.values()];
95
+ }
96
+ /**
97
+ * Returns the number of listeners for a given event.
98
+ * @param eventName The event name (key of E).
99
+ * @returns The number of listeners.
100
+ */
101
+ listenerCount(eventName) {
102
+ const listenersSet = this._listenerStore.get(eventName);
103
+ return listenersSet ? listenersSet.size : 0;
104
+ }
105
+ /**
106
+ * Removes all listeners for a given event, or all events if none is specified.
107
+ * @param eventName Optional. If omitted, clears all events’ listeners.
108
+ * @returns The emitter instance (for chaining).
109
+ */
110
+ removeAllListeners(eventName) {
111
+ if (eventName === void 0) {
112
+ for (const [eventName_, listenersSet] of this._listenerStore.entries()) for (const listener of listenersSet.values()) this.off(eventName_, listener);
113
+ this._listenerStore.clear();
114
+ } else {
115
+ const listenersSet = this._listenerStore.get(eventName);
116
+ if (listenersSet) {
117
+ for (const listener of listenersSet.values()) this.off(eventName, listener);
118
+ this._listenerStore.delete(eventName);
119
+ }
120
+ }
121
+ return this;
122
+ }
123
+ };
32
124
  //#endregion
33
- export { createSignal as default };
125
+ export { EventEmitter as default };
@@ -1,12 +1,33 @@
1
- //#region src/utils/randomId.ts
1
+ //#region src/utils/createSignal.ts
2
2
  /**
3
- * creates a random id
4
- * @returns a random string of 16 characters
5
- * @example
6
- * randomId() // '1234567890abcdef'
3
+ * Creates a reactive signal for managing state and triggering dependencies.
4
+ * The signal holds a value and provides methods to get and set the value,
5
+ * with optional equality checks and dependency tracking.
6
+ * @template T - The type of the value held by the signal.
7
+ * @param reactivityAdapter - An optional reactivity adapter for managing dependencies.
8
+ * @param initialValue - The initial value of the signal.
9
+ * @param isEqual - A custom equality function to determine if the new value is different
10
+ * from the current value (default is `Object.is`).
11
+ * @returns A signal object with `get` and `set` methods to manage the value.
7
12
  */
8
- function randomId() {
9
- return Array.from({ length: 16 }, () => "abcdefghijklmnopqrstuvwxyz0123456789"[Math.floor(Math.random() * 36)]).join("");
13
+ function createSignal(reactivityAdapter, initialValue, isEqual = Object.is) {
14
+ let value = initialValue;
15
+ const dependency = reactivityAdapter?.create();
16
+ const isInReactiveScope = () => {
17
+ if (!reactivityAdapter?.isInScope) return true;
18
+ return reactivityAdapter.isInScope();
19
+ };
20
+ return {
21
+ get() {
22
+ if (dependency && isInReactiveScope()) dependency.depend();
23
+ return value;
24
+ },
25
+ set(newValue) {
26
+ if (isEqual(value, newValue)) return;
27
+ value = newValue;
28
+ if (dependency) dependency.notify();
29
+ }
30
+ };
10
31
  }
11
32
  //#endregion
12
- exports.default = randomId;
33
+ exports.default = createSignal;
package/dist/index8.mjs CHANGED
@@ -1,12 +1,33 @@
1
- //#region src/utils/randomId.ts
1
+ //#region src/utils/createSignal.ts
2
2
  /**
3
- * creates a random id
4
- * @returns a random string of 16 characters
5
- * @example
6
- * randomId() // '1234567890abcdef'
3
+ * Creates a reactive signal for managing state and triggering dependencies.
4
+ * The signal holds a value and provides methods to get and set the value,
5
+ * with optional equality checks and dependency tracking.
6
+ * @template T - The type of the value held by the signal.
7
+ * @param reactivityAdapter - An optional reactivity adapter for managing dependencies.
8
+ * @param initialValue - The initial value of the signal.
9
+ * @param isEqual - A custom equality function to determine if the new value is different
10
+ * from the current value (default is `Object.is`).
11
+ * @returns A signal object with `get` and `set` methods to manage the value.
7
12
  */
8
- function randomId() {
9
- return Array.from({ length: 16 }, () => "abcdefghijklmnopqrstuvwxyz0123456789"[Math.floor(Math.random() * 36)]).join("");
13
+ function createSignal(reactivityAdapter, initialValue, isEqual = Object.is) {
14
+ let value = initialValue;
15
+ const dependency = reactivityAdapter?.create();
16
+ const isInReactiveScope = () => {
17
+ if (!reactivityAdapter?.isInScope) return true;
18
+ return reactivityAdapter.isInScope();
19
+ };
20
+ return {
21
+ get() {
22
+ if (dependency && isInReactiveScope()) dependency.depend();
23
+ return value;
24
+ },
25
+ set(newValue) {
26
+ if (isEqual(value, newValue)) return;
27
+ value = newValue;
28
+ if (dependency) dependency.notify();
29
+ }
30
+ };
10
31
  }
11
32
  //#endregion
12
- export { randomId as default };
33
+ export { createSignal as default };
@@ -1,11 +1,12 @@
1
- //#region src/createIndexProvider.ts
1
+ //#region src/utils/randomId.ts
2
2
  /**
3
- * Creates an IndexProvider based on the given definition.
4
- * @param definition - The definition of the IndexProvider.
5
- * @returns The created IndexProvider.
3
+ * creates a random id
4
+ * @returns a random string of 16 characters
5
+ * @example
6
+ * randomId() // '1234567890abcdef'
6
7
  */
7
- function createIndexProvider(definition) {
8
- return definition;
8
+ function randomId() {
9
+ return Array.from({ length: 16 }, () => "abcdefghijklmnopqrstuvwxyz0123456789"[Math.floor(Math.random() * 36)]).join("");
9
10
  }
10
11
  //#endregion
11
- exports.default = createIndexProvider;
12
+ exports.default = randomId;
package/dist/index9.mjs CHANGED
@@ -1,11 +1,12 @@
1
- //#region src/createIndexProvider.ts
1
+ //#region src/utils/randomId.ts
2
2
  /**
3
- * Creates an IndexProvider based on the given definition.
4
- * @param definition - The definition of the IndexProvider.
5
- * @returns The created IndexProvider.
3
+ * creates a random id
4
+ * @returns a random string of 16 characters
5
+ * @example
6
+ * randomId() // '1234567890abcdef'
6
7
  */
7
- function createIndexProvider(definition) {
8
- return definition;
8
+ function randomId() {
9
+ return Array.from({ length: 16 }, () => "abcdefghijklmnopqrstuvwxyz0123456789"[Math.floor(Math.random() * 36)]).join("");
9
10
  }
10
11
  //#endregion
11
- export { createIndexProvider as default };
12
+ export { randomId as default };
@@ -0,0 +1,60 @@
1
+ import { QueryOptions } from '../DataAdapter';
2
+ import { BaseItem } from '../Collection/types';
3
+ import { default as Selector } from '../types/Selector';
4
+ /**
5
+ * The items a write created, updated or removed, from the point of view of a store.
6
+ *
7
+ * `upserts` are the *current* state of every item that still exists; `deletes` are the ids of items
8
+ * that no longer do. A write that changes an item's id contributes to both. Callers are responsible
9
+ * for that split — an "affected items" list holding an item's state from before and after a write
10
+ * cannot be told apart from two unrelated items here.
11
+ */
12
+ export interface QueryChangeset<T extends BaseItem> {
13
+ upserts: T[];
14
+ deletes: any[];
15
+ }
16
+ /**
17
+ * Recomputes a query's result from its previous result and the change that was just written,
18
+ * without going back to the store.
19
+ *
20
+ * A store re-executing the query instead reads every item it holds (or every item an index points
21
+ * at) and filters, sorts and projects the lot — for a write that touched one row. This does the
22
+ * same job in the size of the write, which is what a query's result costs to keep up to date when
23
+ * the change that affects it is already in hand.
24
+ *
25
+ * Returns `null` when the previous result is not enough to answer, and the caller has to re-execute
26
+ * the query after all:
27
+ * - `limit` or `skip`: the result is a window onto a larger set, and an item leaving the window has
28
+ * to be replaced by one the previous result never contained.
29
+ * - `fields` together with `sort`: the previous items are projected, so the field the sort is keyed
30
+ * on may no longer be there to sort by.
31
+ * - a `null` selector, which matches nothing and is not worth a special case.
32
+ * @template T - The type of the items.
33
+ * @param previous - The query's previous result.
34
+ * @param selector - The query's selector.
35
+ * @param options - The query's options.
36
+ * @param changes - The items the write created, updated or removed.
37
+ * @returns The new result, or `null` when the query has to be re-executed.
38
+ */
39
+ export default function incrementalQueryUpdate<T extends BaseItem>(previous: T[], selector: Selector<T>, options: QueryOptions<T> | undefined, changes: QueryChangeset<T>): T[] | null;
40
+ /**
41
+ * Folds a change into a query's result, whatever the query's options.
42
+ *
43
+ * The unguarded version of `incrementalQueryUpdate`, for the places where the alternative is not a
44
+ * more accurate answer but a wrong one — layering a write that has not been confirmed yet on top of
45
+ * the last confirmed result, say. For a query returning everything it matches, this is exact. For a
46
+ * window onto a larger set it is the closest the window itself can get: an item that no longer
47
+ * belongs is dropped, one that does is placed, and the window is trimmed back to its length — but
48
+ * an item pulled in from beyond the window is not something the window knows about.
49
+ *
50
+ * What it never does is re-examine the items already in the result. They matched when the store
51
+ * produced them, they still match, and asking again is both wasteful and — for a projected result,
52
+ * whose items no longer carry the fields the selector names — wrong.
53
+ * @template T - The type of the items.
54
+ * @param previous - The query's previous result.
55
+ * @param selector - The query's selector.
56
+ * @param options - The query's options.
57
+ * @param changes - The items the write created, updated or removed.
58
+ * @returns The resulting items.
59
+ */
60
+ export declare function mergeChangesetIntoResult<T extends BaseItem>(previous: T[], selector: Selector<T>, options: QueryOptions<T> | undefined, changes: QueryChangeset<T>): T[];
@@ -0,0 +1,12 @@
1
+ import { QueryOptions } from '../DataAdapter';
2
+ import { BaseItem } from '../Collection/types';
3
+ /**
4
+ * Applies a query's field projection to a list of items, keeping the primary key unless the
5
+ * projection excludes it outright. Returns the items untouched when there is no projection, so a
6
+ * caller does not have to check for one first.
7
+ * @template T - The type of the items.
8
+ * @param items - The items to project.
9
+ * @param fields - The projection, or `undefined` for none.
10
+ * @returns The projected items.
11
+ */
12
+ export default function projectItems<T extends BaseItem>(items: T[], fields: QueryOptions<T>['fields']): T[];
@@ -0,0 +1,83 @@
1
+ import { BaseItem } from '../Collection/types';
2
+ /**
3
+ * The change between two consecutive results of the same query, expressed so it can be applied to
4
+ * the earlier result to obtain the later one.
5
+ *
6
+ * Indices in `added` and `moved` are positions in the *resulting* array and are always ascending,
7
+ * which is what makes applying them a matter of splicing in order. Removals and moves name items by
8
+ * id only: whoever applies the delta still holds the previous result and can look the item up
9
+ * there, so there is no reason to send it twice — the point of the whole exercise is that a change
10
+ * costs the size of the change, not the size of the result.
11
+ */
12
+ export interface QueryDelta<T extends BaseItem = BaseItem> {
13
+ /** Items that were not in the previous result, at their position in the new one. */
14
+ added: {
15
+ index: number;
16
+ item: T;
17
+ }[];
18
+ /** Items that were in the previous result and whose contents changed. */
19
+ changed: T[];
20
+ /** Ids of items that are no longer in the result. */
21
+ removed: any[];
22
+ /** Items that stayed, at their new position, because the order around them changed. */
23
+ moved: {
24
+ index: number;
25
+ id: any;
26
+ }[];
27
+ /** Length of the resulting array — lets a recipient verify it applied the delta to the result it was computed against. */
28
+ resultCount: number;
29
+ }
30
+ /**
31
+ * Checks whether a delta leaves the result it is applied to unchanged.
32
+ * @param delta - The delta to inspect.
33
+ * @returns `true` when applying the delta would be a no-op.
34
+ */
35
+ export declare function isEmptyQueryDelta(delta: QueryDelta<any>): boolean;
36
+ /**
37
+ * Calls a state-change callback, passing the delta only when there is one.
38
+ *
39
+ * A callback invoked as `callback(state, undefined)` has been handed two arguments, which is a
40
+ * different thing from being handed one — visible to anything that inspects arity, and to any test
41
+ * that asserts on the call.
42
+ * @template T - The type of the items.
43
+ * @param callback - The callback to invoke.
44
+ * @param state - The state to report.
45
+ * @param delta - The delta to report, if there is one.
46
+ */
47
+ export declare function callWithDelta<T extends BaseItem>(callback: (state: 'active' | 'complete' | 'error', delta?: QueryDelta<T>) => void, state: 'active' | 'complete' | 'error', delta?: QueryDelta<T>): void;
48
+ /**
49
+ * Checks whether a delta describes a change to the given result.
50
+ *
51
+ * A delta is only meaningful against the exact result it was computed from — it names positions in
52
+ * an array and items by id alone. Applying one to anything else produces a result that looks
53
+ * plausible and is wrong, and from then on every further delta compounds the error. This is the
54
+ * cheap structural check that catches that: every id the delta expects to find is there, every id
55
+ * it expects to be new is not, and the arithmetic on the length works out. It costs the size of the
56
+ * delta, not the size of the result.
57
+ * @template T - The type of the items.
58
+ * @param previous - The result the delta would be applied to.
59
+ * @param delta - The delta to check.
60
+ * @returns `true` when the delta can be applied.
61
+ */
62
+ export declare function canApplyQueryDelta<T extends BaseItem>(previous: T[], delta: QueryDelta<T>): boolean;
63
+ /**
64
+ * Computes the delta between two results of the same query.
65
+ *
66
+ * A fallback for the cases where the change that produced the new result is not available — a query
67
+ * that had to be re-executed in full, for instance. It costs a pass over both results, but it is
68
+ * paid once, on the side that has both of them, instead of shipping the entire new result to
69
+ * everyone who only needs to know what changed.
70
+ * @template T - The type of the items.
71
+ * @param previous - The result the delta should be relative to.
72
+ * @param next - The result the delta should produce.
73
+ * @returns The delta between the two results.
74
+ */
75
+ export declare function diffQueryResults<T extends BaseItem>(previous: T[], next: T[]): QueryDelta<T>;
76
+ /**
77
+ * Applies a delta to the result it was computed against.
78
+ * @template T - The type of the items.
79
+ * @param previous - The result the delta is relative to. Not modified.
80
+ * @param delta - The delta to apply.
81
+ * @returns The resulting items.
82
+ */
83
+ export declare function applyQueryDelta<T extends BaseItem>(previous: T[], delta: QueryDelta<T>): T[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaldb/core",
3
- "version": "2.0.0-beta.13",
3
+ "version": "2.0.0-beta.14",
4
4
  "description": "SignalDB is a client-side database that provides a simple MongoDB-like interface to the data with first-class typescript support to achieve an optimistic UI. Data persistence can be achieved by using storage providers that store the data through a JSON interface to places such as localStorage.",
5
5
  "scripts": {
6
6
  "build": "rimraf dist && vite build",