@statezero/core 0.1.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.
Files changed (89) hide show
  1. package/dist/adaptors/react/composables.d.ts +1 -0
  2. package/dist/adaptors/react/composables.js +4 -0
  3. package/dist/adaptors/react/index.d.ts +1 -0
  4. package/dist/adaptors/react/index.js +1 -0
  5. package/dist/adaptors/vue/composables.d.ts +2 -0
  6. package/dist/adaptors/vue/composables.js +36 -0
  7. package/dist/adaptors/vue/index.d.ts +2 -0
  8. package/dist/adaptors/vue/index.js +2 -0
  9. package/dist/adaptors/vue/reactivity.d.ts +18 -0
  10. package/dist/adaptors/vue/reactivity.js +125 -0
  11. package/dist/cli/commands/syncModels.d.ts +132 -0
  12. package/dist/cli/commands/syncModels.js +1040 -0
  13. package/dist/cli/configFileLoader.d.ts +10 -0
  14. package/dist/cli/configFileLoader.js +85 -0
  15. package/dist/cli/index.d.ts +2 -0
  16. package/dist/cli/index.js +14 -0
  17. package/dist/config.d.ts +52 -0
  18. package/dist/config.js +242 -0
  19. package/dist/core/eventReceivers.d.ts +179 -0
  20. package/dist/core/eventReceivers.js +210 -0
  21. package/dist/core/utils.d.ts +8 -0
  22. package/dist/core/utils.js +62 -0
  23. package/dist/filtering/localFiltering.d.ts +116 -0
  24. package/dist/filtering/localFiltering.js +834 -0
  25. package/dist/flavours/django/dates.d.ts +33 -0
  26. package/dist/flavours/django/dates.js +99 -0
  27. package/dist/flavours/django/errors.d.ts +138 -0
  28. package/dist/flavours/django/errors.js +187 -0
  29. package/dist/flavours/django/f.d.ts +6 -0
  30. package/dist/flavours/django/f.js +91 -0
  31. package/dist/flavours/django/files.d.ts +76 -0
  32. package/dist/flavours/django/files.js +338 -0
  33. package/dist/flavours/django/makeApiCall.d.ts +20 -0
  34. package/dist/flavours/django/makeApiCall.js +169 -0
  35. package/dist/flavours/django/manager.d.ts +197 -0
  36. package/dist/flavours/django/manager.js +222 -0
  37. package/dist/flavours/django/model.d.ts +112 -0
  38. package/dist/flavours/django/model.js +253 -0
  39. package/dist/flavours/django/operationFactory.d.ts +65 -0
  40. package/dist/flavours/django/operationFactory.js +216 -0
  41. package/dist/flavours/django/q.d.ts +70 -0
  42. package/dist/flavours/django/q.js +43 -0
  43. package/dist/flavours/django/queryExecutor.d.ts +131 -0
  44. package/dist/flavours/django/queryExecutor.js +468 -0
  45. package/dist/flavours/django/querySet.d.ts +412 -0
  46. package/dist/flavours/django/querySet.js +601 -0
  47. package/dist/flavours/django/tempPk.d.ts +19 -0
  48. package/dist/flavours/django/tempPk.js +48 -0
  49. package/dist/flavours/django/utils.d.ts +19 -0
  50. package/dist/flavours/django/utils.js +29 -0
  51. package/dist/index.d.ts +38 -0
  52. package/dist/index.js +38 -0
  53. package/dist/react-entry.d.ts +2 -0
  54. package/dist/react-entry.js +2 -0
  55. package/dist/reactiveAdaptor.d.ts +24 -0
  56. package/dist/reactiveAdaptor.js +38 -0
  57. package/dist/setup.d.ts +15 -0
  58. package/dist/setup.js +22 -0
  59. package/dist/syncEngine/cache/cache.d.ts +75 -0
  60. package/dist/syncEngine/cache/cache.js +341 -0
  61. package/dist/syncEngine/metrics/metricOptCalcs.d.ts +79 -0
  62. package/dist/syncEngine/metrics/metricOptCalcs.js +284 -0
  63. package/dist/syncEngine/registries/metricRegistry.d.ts +53 -0
  64. package/dist/syncEngine/registries/metricRegistry.js +162 -0
  65. package/dist/syncEngine/registries/modelStoreRegistry.d.ts +11 -0
  66. package/dist/syncEngine/registries/modelStoreRegistry.js +56 -0
  67. package/dist/syncEngine/registries/querysetStoreRegistry.d.ts +55 -0
  68. package/dist/syncEngine/registries/querysetStoreRegistry.js +244 -0
  69. package/dist/syncEngine/stores/metricStore.d.ts +55 -0
  70. package/dist/syncEngine/stores/metricStore.js +222 -0
  71. package/dist/syncEngine/stores/modelStore.d.ts +40 -0
  72. package/dist/syncEngine/stores/modelStore.js +405 -0
  73. package/dist/syncEngine/stores/operation.d.ts +99 -0
  74. package/dist/syncEngine/stores/operation.js +224 -0
  75. package/dist/syncEngine/stores/operationEventHandlers.d.ts +8 -0
  76. package/dist/syncEngine/stores/operationEventHandlers.js +239 -0
  77. package/dist/syncEngine/stores/querysetStore.d.ts +32 -0
  78. package/dist/syncEngine/stores/querysetStore.js +200 -0
  79. package/dist/syncEngine/stores/reactivity.d.ts +3 -0
  80. package/dist/syncEngine/stores/reactivity.js +4 -0
  81. package/dist/syncEngine/stores/utils.d.ts +14 -0
  82. package/dist/syncEngine/stores/utils.js +32 -0
  83. package/dist/syncEngine/sync.d.ts +32 -0
  84. package/dist/syncEngine/sync.js +169 -0
  85. package/dist/vue-entry.d.ts +6 -0
  86. package/dist/vue-entry.js +2 -0
  87. package/license.md +116 -0
  88. package/package.json +123 -0
  89. package/readme.md +222 -0
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Given a live reactive handle and a Promise, wire them together so
3
+ * the handle is also thenable.
4
+ *
5
+ * @template T
6
+ * @param {LiveQueryset} live
7
+ * @param {Promise<T>} promise
8
+ * @returns {LiveQueryset & Promise<T>}
9
+ */
10
+ export function makeLiveThenable(live, promise) {
11
+ live.isOptimistic = true;
12
+ live.then = promise.then.bind(promise);
13
+ live.catch = promise.catch.bind(promise);
14
+ return live;
15
+ }
16
+ /**
17
+ * Remove the thenable hooks and clear the optimistic flag in-place, so the object
18
+ * can be returned without causing a recursive await loop.
19
+ *
20
+ * @template T
21
+ * @param {T} live
22
+ * @returns {T}
23
+ */
24
+ export function breakThenable(live) {
25
+ delete live.isOptimistic;
26
+ delete live.then;
27
+ delete live.catch;
28
+ return live;
29
+ }
@@ -0,0 +1,38 @@
1
+ import { EventType } from "./core/eventReceivers.js";
2
+ import { PusherEventReceiver } from "./core/eventReceivers.js";
3
+ import { setEventReceiver } from "./core/eventReceivers.js";
4
+ import { getEventReceiver } from "./core/eventReceivers.js";
5
+ import { setNamespaceResolver } from "./core/eventReceivers.js";
6
+ import { setupStateZero } from "./setup.js";
7
+ import { FileObject } from "./flavours/django/files.js";
8
+ import { querysetStoreRegistry } from "./syncEngine/registries/querysetStoreRegistry.js";
9
+ import { modelStoreRegistry } from "./syncEngine/registries/modelStoreRegistry.js";
10
+ import { metricRegistry } from "./syncEngine/registries/metricRegistry.js";
11
+ import { syncManager } from "./syncEngine/sync.js";
12
+ import { Operation } from "./syncEngine/stores/operation.js";
13
+ import { operationRegistry } from "./syncEngine/stores/operation.js";
14
+ import { Q } from "./flavours/django/q.js";
15
+ import { StateZeroError } from "./flavours/django/errors.js";
16
+ import { ValidationError } from "./flavours/django/errors.js";
17
+ import { DoesNotExist } from "./flavours/django/errors.js";
18
+ import { PermissionDenied } from "./flavours/django/errors.js";
19
+ import { MultipleObjectsReturned } from "./flavours/django/errors.js";
20
+ import { ASTValidationError } from "./flavours/django/errors.js";
21
+ import { ConfigError } from "./flavours/django/errors.js";
22
+ import { parseStateZeroError } from "./flavours/django/errors.js";
23
+ import { QuerySet } from "./flavours/django/querySet.js";
24
+ import { Manager } from "./flavours/django/manager.js";
25
+ import { ResultTuple } from "./flavours/django/queryExecutor.js";
26
+ import { Model } from "./flavours/django/model.js";
27
+ import { setConfig } from "./config.js";
28
+ import { getConfig } from "./config.js";
29
+ import { setBackendConfig } from "./config.js";
30
+ import { initializeEventReceiver } from "./config.js";
31
+ import { configInstance } from "./config.js";
32
+ import { getModelClass } from "./config.js";
33
+ import { initEventHandler } from "./syncEngine/stores/operationEventHandlers.js";
34
+ import { cleanupEventHandler } from "./syncEngine/stores/operationEventHandlers.js";
35
+ import { setAdapters } from "./reactiveAdaptor.js";
36
+ import { wrapReactiveModel } from "./reactiveAdaptor.js";
37
+ import { wrapReactiveQuerySet } from "./reactiveAdaptor.js";
38
+ export { EventType, PusherEventReceiver, setEventReceiver, getEventReceiver, setNamespaceResolver, setupStateZero, FileObject, querysetStoreRegistry, modelStoreRegistry, metricRegistry, syncManager, Operation, operationRegistry, Q, StateZeroError, ValidationError, DoesNotExist, PermissionDenied, MultipleObjectsReturned, ASTValidationError, ConfigError, parseStateZeroError, QuerySet, Manager, ResultTuple, Model, setConfig, getConfig, setBackendConfig, initializeEventReceiver, configInstance, getModelClass, initEventHandler, cleanupEventHandler, setAdapters, wrapReactiveModel, wrapReactiveQuerySet };
package/dist/index.js ADDED
@@ -0,0 +1,38 @@
1
+ // Core event receivers
2
+ import { EventType, PusherEventReceiver, setEventReceiver, getEventReceiver, setNamespaceResolver, } from "./core/eventReceivers.js";
3
+ import { setupStateZero } from "./setup.js";
4
+ // Django flavor modules
5
+ import { Q } from "./flavours/django/q.js";
6
+ import { StateZeroError, ValidationError, DoesNotExist, PermissionDenied, MultipleObjectsReturned, ASTValidationError, ConfigError, parseStateZeroError, } from "./flavours/django/errors.js";
7
+ import { querysetStoreRegistry } from "./syncEngine/registries/querysetStoreRegistry.js";
8
+ import { modelStoreRegistry } from "./syncEngine/registries/modelStoreRegistry.js";
9
+ import { metricRegistry } from "./syncEngine/registries/metricRegistry.js";
10
+ import { QueryExecutor, ResultTuple } from "./flavours/django/queryExecutor.js";
11
+ import { Operation, operationRegistry } from "./syncEngine/stores/operation.js";
12
+ import { QuerySet } from "./flavours/django/querySet.js";
13
+ import { Manager } from "./flavours/django/manager.js";
14
+ import { Model } from "./flavours/django/model.js";
15
+ import { FileObject } from "./flavours/django/files.js";
16
+ // Configuration
17
+ import { setConfig, getConfig, setBackendConfig, initializeEventReceiver, configInstance, getModelClass, } from "./config.js";
18
+ import { setAdapters, wrapReactiveModel, wrapReactiveQuerySet, } from "./reactiveAdaptor.js";
19
+ import { syncManager } from "./syncEngine/sync.js";
20
+ import { initEventHandler, cleanupEventHandler, } from "./syncEngine/stores/operationEventHandlers.js";
21
+ // Explicitly export everything
22
+ export {
23
+ // Core event receivers
24
+ EventType, PusherEventReceiver, setEventReceiver, getEventReceiver, setNamespaceResolver,
25
+ // Setup
26
+ setupStateZero,
27
+ // Files
28
+ FileObject,
29
+ // Registry
30
+ querysetStoreRegistry, modelStoreRegistry, metricRegistry, syncManager,
31
+ // Operations
32
+ Operation, operationRegistry,
33
+ // Django flavor modules
34
+ Q, StateZeroError, ValidationError, DoesNotExist, PermissionDenied, MultipleObjectsReturned, ASTValidationError, ConfigError, parseStateZeroError, QuerySet, Manager, ResultTuple, Model,
35
+ // Configuration
36
+ setConfig, getConfig, setBackendConfig, initializeEventReceiver, configInstance, getModelClass,
37
+ // Reactivity framework integration
38
+ initEventHandler, cleanupEventHandler, setAdapters, wrapReactiveModel, wrapReactiveQuerySet, };
@@ -0,0 +1,2 @@
1
+ export { useQueryset };
2
+ import { useQueryset } from './adaptors/react/index.js';
@@ -0,0 +1,2 @@
1
+ import { useQueryset } from './adaptors/react/index.js';
2
+ export { useQueryset };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Set the global reactivity adapters.
3
+ * @param {Function} modelAdapterFn - A function that takes a model and returns its reactive version.
4
+ * @param {Function} querySetAdapterFn - A function that takes a queryset and returns its reactive version.
5
+ */
6
+ export function setAdapters(modelAdapterFn: Function, querySetAdapterFn: Function, metricAdaptorFn: any): void;
7
+ /**
8
+ * Wrap a model with the configured reactivity adapter.
9
+ * @param {Object} model - The model to wrap.
10
+ * @returns {Object} The reactive model.
11
+ */
12
+ export function wrapReactiveModel(model: Object): Object;
13
+ /**
14
+ * Wrap a queryset with the configured reactivity adapter.
15
+ * @param {Object} querySet - The queryset to wrap.
16
+ * @returns {Object} The reactive queryset.
17
+ */
18
+ export function wrapReactiveQuerySet(querySet: Object): Object;
19
+ /**
20
+ * Wrap a metric with the configured reactivity adaptor.
21
+ * @param {Object} LiveMetric - the metric to wrap.
22
+ * @returns {Object} The reactive metric
23
+ */
24
+ export function wrapReactiveMetric(liveMetric: any): Object;
@@ -0,0 +1,38 @@
1
+ // Global singleton for reactivity adapters
2
+ let modelAdapter = null;
3
+ let querySetAdapter = null;
4
+ let metricAdaptor = null;
5
+ /**
6
+ * Set the global reactivity adapters.
7
+ * @param {Function} modelAdapterFn - A function that takes a model and returns its reactive version.
8
+ * @param {Function} querySetAdapterFn - A function that takes a queryset and returns its reactive version.
9
+ */
10
+ export function setAdapters(modelAdapterFn, querySetAdapterFn, metricAdaptorFn) {
11
+ modelAdapter = modelAdapterFn;
12
+ querySetAdapter = querySetAdapterFn;
13
+ metricAdaptor = metricAdaptorFn;
14
+ }
15
+ /**
16
+ * Wrap a model with the configured reactivity adapter.
17
+ * @param {Object} model - The model to wrap.
18
+ * @returns {Object} The reactive model.
19
+ */
20
+ export function wrapReactiveModel(model) {
21
+ return modelAdapter ? modelAdapter(model) : model;
22
+ }
23
+ /**
24
+ * Wrap a queryset with the configured reactivity adapter.
25
+ * @param {Object} querySet - The queryset to wrap.
26
+ * @returns {Object} The reactive queryset.
27
+ */
28
+ export function wrapReactiveQuerySet(querySet) {
29
+ return querySetAdapter ? querySetAdapter(querySet) : querySet;
30
+ }
31
+ /**
32
+ * Wrap a metric with the configured reactivity adaptor.
33
+ * @param {Object} LiveMetric - the metric to wrap.
34
+ * @returns {Object} The reactive metric
35
+ */
36
+ export function wrapReactiveMetric(liveMetric) {
37
+ return metricAdaptor ? metricAdaptor(liveMetric) : liveMetric;
38
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Initialize StateZero with the provided configuration
3
+ *
4
+ * @param {Object} config - StateZero configuration object
5
+ * @param {Function} getModelClass - Model class getter function from model-registry.js
6
+ * @param {Object} adapters - Adapter objects for the framework
7
+ * @param {Function} adapters.ModelAdaptor - Model adapter
8
+ * @param {Function} adapters.QuerySetAdaptor - QuerySet adapter
9
+ * @param {Function} adapters.MetricAdaptor - Metric adapter
10
+ */
11
+ export function setupStateZero(config: Object, getModelClass: Function, adapters: {
12
+ ModelAdaptor: Function;
13
+ QuerySetAdaptor: Function;
14
+ MetricAdaptor: Function;
15
+ }): void;
package/dist/setup.js ADDED
@@ -0,0 +1,22 @@
1
+ import { configInstance } from "./config.js";
2
+ import { setAdapters } from "./reactiveAdaptor.js";
3
+ import { syncManager } from "./syncEngine/sync.js";
4
+ import { initEventHandler } from "./syncEngine/stores/operationEventHandlers.js";
5
+ /**
6
+ * Initialize StateZero with the provided configuration
7
+ *
8
+ * @param {Object} config - StateZero configuration object
9
+ * @param {Function} getModelClass - Model class getter function from model-registry.js
10
+ * @param {Object} adapters - Adapter objects for the framework
11
+ * @param {Function} adapters.ModelAdaptor - Model adapter
12
+ * @param {Function} adapters.QuerySetAdaptor - QuerySet adapter
13
+ * @param {Function} adapters.MetricAdaptor - Metric adapter
14
+ */
15
+ export function setupStateZero(config, getModelClass, adapters) {
16
+ // Initialize StateZero configuration
17
+ configInstance.setConfig(config);
18
+ configInstance.registerModelGetter(getModelClass);
19
+ setAdapters(adapters.ModelAdaptor, adapters.QuerySetAdaptor, adapters.MetricAdaptor);
20
+ initEventHandler();
21
+ syncManager.initialize();
22
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Simple IndexedDB key-value store with batched operations
3
+ */
4
+ export class IndexedDBStore {
5
+ constructor(dbName: any, options?: {});
6
+ dbName: any;
7
+ storeName: any;
8
+ batchDelay: any;
9
+ version: any;
10
+ resetOnErrors: any;
11
+ pendingOps: any[];
12
+ activeCommit: Promise<void> | null;
13
+ _destroyed: boolean;
14
+ dbPromise: Promise<any>;
15
+ /**
16
+ * Attempt to open the database, with automatic recovery if it fails
17
+ */
18
+ _openDatabaseWithRecovery(): Promise<any>;
19
+ /**
20
+ * Open the database
21
+ */
22
+ _openDatabase(): Promise<any>;
23
+ /**
24
+ * Delete the database
25
+ */
26
+ _deleteDatabase(): Promise<any>;
27
+ /**
28
+ * Retrieve a value by key
29
+ */
30
+ get(key: any): Promise<any>;
31
+ /**
32
+ * Store a value with the given key
33
+ */
34
+ set(key: any, value: any): Promise<any>;
35
+ /**
36
+ * Remove a key-value pair
37
+ */
38
+ delete(key: any): Promise<any>;
39
+ /**
40
+ * Delete the entire database
41
+ */
42
+ destroy(): Promise<void>;
43
+ /**
44
+ * Kick off a batch if none is running
45
+ */
46
+ _startCommitIfNeeded(): Promise<void>;
47
+ /**
48
+ * Add this method to the IndexedDBStore class
49
+ */
50
+ getAllKeys(): Promise<any>;
51
+ /**
52
+ * Get all entries from the store
53
+ * @returns {Promise<Array<[any, any]>>} Array of [key, value] pairs
54
+ */
55
+ getAll(): Promise<Array<[any, any]>>;
56
+ /**
57
+ * Execute all pending operations in one transaction
58
+ */
59
+ _executeBatch(): Promise<void>;
60
+ }
61
+ export class Cache {
62
+ constructor(dbName: any, options?: {}, onHydrated?: null);
63
+ store: IndexedDBStore;
64
+ localMap: Map<any, any>;
65
+ hydrate(): Promise<void>;
66
+ get(key: any): any;
67
+ set(key: any, value: any): void;
68
+ delete(key: any): void;
69
+ getAllKeys(): Promise<any>;
70
+ /**
71
+ * Clear all entries from the cache
72
+ * Clears the in-memory map and schedules deletion of all keys in IndexedDB
73
+ */
74
+ clear(): void;
75
+ }
@@ -0,0 +1,341 @@
1
+ const dbConnections = new Map();
2
+ /**
3
+ * Simple IndexedDB key-value store with batched operations
4
+ */
5
+ export class IndexedDBStore {
6
+ constructor(dbName, options = {}) {
7
+ this.dbName = dbName;
8
+ this.storeName = options.storeName || "keyval-store";
9
+ this.batchDelay = options.batchDelay || 50;
10
+ this.version = options.version || 5;
11
+ // List of error names or messages that should trigger database deletion
12
+ this.resetOnErrors = options.resetOnErrors || [
13
+ "VersionError",
14
+ "InvalidStateError",
15
+ "Failed to open database"
16
+ ];
17
+ // Queue of pending operations
18
+ this.pendingOps = [];
19
+ this.activeCommit = null;
20
+ // Whether destroy() has been called
21
+ this._destroyed = false;
22
+ // Open the database once
23
+ this.dbPromise = this._openDatabaseWithRecovery();
24
+ }
25
+ /**
26
+ * Attempt to open the database, with automatic recovery if it fails
27
+ */
28
+ async _openDatabaseWithRecovery() {
29
+ try {
30
+ return await this._openDatabase();
31
+ }
32
+ catch (error) {
33
+ // Check if this is an error type that should trigger reset
34
+ const shouldReset = this.resetOnErrors.some(errorPattern => error.name === errorPattern ||
35
+ error.message.includes(errorPattern));
36
+ if (shouldReset) {
37
+ console.warn(`[IndexedDBStore] Database error detected, attempting to reset database "${this.dbName}":`, error);
38
+ try {
39
+ // Delete the database
40
+ await this._deleteDatabase();
41
+ console.log(`[IndexedDBStore] Successfully deleted database "${this.dbName}", attempting to reopen...`);
42
+ // Try to open it again at the desired version
43
+ return await this._openDatabase();
44
+ }
45
+ catch (resetError) {
46
+ console.error(`[IndexedDBStore] Failed to recover database "${this.dbName}":`, resetError);
47
+ throw resetError;
48
+ }
49
+ }
50
+ else {
51
+ // Not a recoverable error
52
+ throw error;
53
+ }
54
+ }
55
+ }
56
+ /**
57
+ * Open the database
58
+ */
59
+ _openDatabase() {
60
+ return new Promise((resolve, reject) => {
61
+ const openRequest = indexedDB.open(this.dbName, this.version);
62
+ openRequest.onupgradeneeded = () => {
63
+ try {
64
+ // Create the object store if it doesn't exist
65
+ if (!openRequest.result.objectStoreNames.contains(this.storeName)) {
66
+ openRequest.result.createObjectStore(this.storeName);
67
+ }
68
+ }
69
+ catch (error) {
70
+ console.error(`[IndexedDBStore] Error during upgrade:`, error);
71
+ // Error during upgrade should reject the promise
72
+ reject(error);
73
+ }
74
+ };
75
+ openRequest.onsuccess = () => resolve(openRequest.result);
76
+ openRequest.onerror = () => reject(new Error(`Failed to open database: ${openRequest.error}`));
77
+ });
78
+ }
79
+ /**
80
+ * Delete the database
81
+ */
82
+ _deleteDatabase() {
83
+ return new Promise((resolve, reject) => {
84
+ const deleteRequest = indexedDB.deleteDatabase(this.dbName);
85
+ deleteRequest.onsuccess = () => resolve();
86
+ deleteRequest.onerror = () => reject(new Error(`Failed to delete database: ${deleteRequest.error}`));
87
+ });
88
+ }
89
+ /**
90
+ * Retrieve a value by key
91
+ */
92
+ async get(key) {
93
+ if (this._destroyed) {
94
+ throw new Error("Database has been destroyed");
95
+ }
96
+ // Schedule the get in the next batch
97
+ const getPromise = new Promise((resolve, reject) => {
98
+ this.pendingOps.push({ operation: "get", key, resolve, reject });
99
+ });
100
+ // Wait for that batch to run (or reject if destroyed)
101
+ await this._startCommitIfNeeded();
102
+ return getPromise;
103
+ }
104
+ /**
105
+ * Store a value with the given key
106
+ */
107
+ async set(key, value) {
108
+ if (this._destroyed) {
109
+ throw new Error("Database has been destroyed");
110
+ }
111
+ // Schedule the set in the next batch with a promise
112
+ const setPromise = new Promise((resolve, reject) => {
113
+ this.pendingOps.push({ operation: "set", key, value, resolve, reject });
114
+ });
115
+ // Wait for that batch to run (or reject if destroyed)
116
+ await this._startCommitIfNeeded();
117
+ return setPromise;
118
+ }
119
+ /**
120
+ * Remove a key-value pair
121
+ */
122
+ async delete(key) {
123
+ if (this._destroyed) {
124
+ throw new Error("Database has been destroyed");
125
+ }
126
+ // Schedule the delete in the next batch with a promise
127
+ const deletePromise = new Promise((resolve, reject) => {
128
+ this.pendingOps.push({ operation: "delete", key, resolve, reject });
129
+ });
130
+ // Wait for that batch to run (or reject if destroyed)
131
+ await this._startCommitIfNeeded();
132
+ return deletePromise;
133
+ }
134
+ /**
135
+ * Delete the entire database
136
+ */
137
+ async destroy() {
138
+ // Prevent any future actions
139
+ this._destroyed = true;
140
+ // Reject all pending operations
141
+ const error = new Error("Database has been destroyed");
142
+ for (const op of this.pendingOps) {
143
+ if (op.reject) {
144
+ op.reject(error);
145
+ }
146
+ }
147
+ this.pendingOps = [];
148
+ try {
149
+ // Close current connection if we have one
150
+ const db = await this.dbPromise;
151
+ db.close();
152
+ // Drop the whole database
153
+ await new Promise((resolve, reject) => {
154
+ const deleteRequest = indexedDB.deleteDatabase(this.dbName);
155
+ deleteRequest.onsuccess = () => resolve();
156
+ deleteRequest.onerror = () => reject(deleteRequest.error);
157
+ });
158
+ }
159
+ catch (error) {
160
+ // If database opening failed, we can safely ignore this
161
+ // as there's no active connection to close
162
+ }
163
+ }
164
+ /**
165
+ * Kick off a batch if none is running
166
+ */
167
+ _startCommitIfNeeded() {
168
+ if (this._destroyed) {
169
+ return Promise.reject(new Error("Database has been destroyed"));
170
+ }
171
+ if (!this.activeCommit) {
172
+ this.activeCommit = this._executeBatch();
173
+ }
174
+ return this.activeCommit;
175
+ }
176
+ /**
177
+ * Add this method to the IndexedDBStore class
178
+ */
179
+ async getAllKeys() {
180
+ if (this._destroyed) {
181
+ throw new Error("Database has been destroyed");
182
+ }
183
+ const db = await this.dbPromise;
184
+ const tx = db.transaction(this.storeName, "readonly");
185
+ const store = tx.objectStore(this.storeName);
186
+ return new Promise((resolve, reject) => {
187
+ const request = store.getAllKeys();
188
+ request.onsuccess = () => resolve(request.result);
189
+ request.onerror = () => reject(request.error);
190
+ });
191
+ }
192
+ /**
193
+ * Get all entries from the store
194
+ * @returns {Promise<Array<[any, any]>>} Array of [key, value] pairs
195
+ */
196
+ async getAll() {
197
+ if (this._destroyed) {
198
+ throw new Error("Database has been destroyed");
199
+ }
200
+ // Use a direct transaction for better performance with large datasets
201
+ const db = await this.dbPromise;
202
+ const tx = db.transaction(this.storeName, "readonly");
203
+ const store = tx.objectStore(this.storeName);
204
+ // Get all keys and values
205
+ const keysRequest = store.getAllKeys();
206
+ const valuesRequest = store.getAll();
207
+ // Wait for both requests to complete
208
+ const [keys, values] = await Promise.all([
209
+ new Promise((resolve, reject) => {
210
+ keysRequest.onsuccess = () => resolve(keysRequest.result);
211
+ keysRequest.onerror = () => reject(keysRequest.error);
212
+ }),
213
+ new Promise((resolve, reject) => {
214
+ valuesRequest.onsuccess = () => resolve(valuesRequest.result);
215
+ valuesRequest.onerror = () => reject(valuesRequest.error);
216
+ })
217
+ ]);
218
+ // Combine keys and values into entries
219
+ return keys.map((key, index) => [key, values[index]]);
220
+ }
221
+ /**
222
+ * Execute all pending operations in one transaction
223
+ */
224
+ async _executeBatch() {
225
+ // Brief pause so multiple ops can be coalesced
226
+ await new Promise((r) => setTimeout(r, this.batchDelay));
227
+ try {
228
+ // If we were destroyed in the meantime, abort and reject all pending operations
229
+ if (this._destroyed) {
230
+ const error = new Error("Database has been destroyed");
231
+ // Reject all pending operations
232
+ for (const op of this.pendingOps) {
233
+ if (op.reject) {
234
+ op.reject(error);
235
+ }
236
+ }
237
+ this.pendingOps = [];
238
+ return;
239
+ }
240
+ const db = await this.dbPromise;
241
+ const tx = db.transaction(this.storeName, "readwrite");
242
+ const store = tx.objectStore(this.storeName);
243
+ // Drain the queue
244
+ for (const op of this.pendingOps) {
245
+ switch (op.operation) {
246
+ case "get": {
247
+ const req = store.get(op.key);
248
+ req.onsuccess = () => op.resolve(req.result);
249
+ req.onerror = () => op.reject(req.error);
250
+ break;
251
+ }
252
+ case "set": {
253
+ const req = store.put(op.value, op.key);
254
+ req.onsuccess = () => op.resolve();
255
+ req.onerror = () => op.reject(req.error);
256
+ break;
257
+ }
258
+ case "delete": {
259
+ const req = store.delete(op.key);
260
+ req.onsuccess = () => op.resolve();
261
+ req.onerror = () => op.reject(req.error);
262
+ break;
263
+ }
264
+ }
265
+ }
266
+ this.pendingOps = [];
267
+ // Wait for the transaction to finish or fail
268
+ await new Promise((resolve, reject) => {
269
+ tx.oncomplete = () => resolve();
270
+ tx.onerror = () => reject(tx.error);
271
+ tx.onabort = (e) => reject(e.target.error);
272
+ });
273
+ }
274
+ catch (error) {
275
+ // If there's an error, make sure to reject all pending operations that have a reject function
276
+ for (const op of this.pendingOps) {
277
+ if (op.reject) {
278
+ op.reject(error);
279
+ }
280
+ }
281
+ this.pendingOps = [];
282
+ throw error; // Re-throw to propagate the error
283
+ }
284
+ finally {
285
+ // Allow a new batch to start
286
+ this.activeCommit = null;
287
+ }
288
+ }
289
+ }
290
+ export class Cache {
291
+ constructor(dbName, options = {}, onHydrated = null) {
292
+ this.store = new IndexedDBStore(dbName, options);
293
+ this.localMap = new Map();
294
+ // don't await - will hydrate during app setup
295
+ this.hydrate()
296
+ .then(result => {
297
+ if (typeof onHydrated === 'function') {
298
+ onHydrated(result);
299
+ }
300
+ })
301
+ .catch(err => {
302
+ console.error(`Cache hydration failed for "${dbName}":`, err);
303
+ });
304
+ }
305
+ async hydrate() {
306
+ this.localMap = new Map(await this.store.getAll());
307
+ }
308
+ get(key) {
309
+ return this.localMap.get(key);
310
+ }
311
+ set(key, value) {
312
+ this.localMap.set(key, value);
313
+ this.store.set(key, value);
314
+ }
315
+ delete(key) {
316
+ this.localMap.delete(key);
317
+ this.store.delete(key);
318
+ }
319
+ async getAllKeys() {
320
+ return await this.store.getAllKeys();
321
+ }
322
+ /**
323
+ * Clear all entries from the cache
324
+ * Clears the in-memory map and schedules deletion of all keys in IndexedDB
325
+ */
326
+ clear() {
327
+ // Clear the in-memory map
328
+ this.localMap.clear();
329
+ // Get all keys from the store and delete them
330
+ this.store.getAll()
331
+ .then(entries => {
332
+ // Delete each key from the store
333
+ entries.forEach(([key]) => {
334
+ this.store.delete(key);
335
+ });
336
+ })
337
+ .catch(err => {
338
+ console.error('Error clearing cache:', err);
339
+ });
340
+ }
341
+ }