@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.
- package/dist/adaptors/react/composables.d.ts +1 -0
- package/dist/adaptors/react/composables.js +4 -0
- package/dist/adaptors/react/index.d.ts +1 -0
- package/dist/adaptors/react/index.js +1 -0
- package/dist/adaptors/vue/composables.d.ts +2 -0
- package/dist/adaptors/vue/composables.js +36 -0
- package/dist/adaptors/vue/index.d.ts +2 -0
- package/dist/adaptors/vue/index.js +2 -0
- package/dist/adaptors/vue/reactivity.d.ts +18 -0
- package/dist/adaptors/vue/reactivity.js +125 -0
- package/dist/cli/commands/syncModels.d.ts +132 -0
- package/dist/cli/commands/syncModels.js +1040 -0
- package/dist/cli/configFileLoader.d.ts +10 -0
- package/dist/cli/configFileLoader.js +85 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +14 -0
- package/dist/config.d.ts +52 -0
- package/dist/config.js +242 -0
- package/dist/core/eventReceivers.d.ts +179 -0
- package/dist/core/eventReceivers.js +210 -0
- package/dist/core/utils.d.ts +8 -0
- package/dist/core/utils.js +62 -0
- package/dist/filtering/localFiltering.d.ts +116 -0
- package/dist/filtering/localFiltering.js +834 -0
- package/dist/flavours/django/dates.d.ts +33 -0
- package/dist/flavours/django/dates.js +99 -0
- package/dist/flavours/django/errors.d.ts +138 -0
- package/dist/flavours/django/errors.js +187 -0
- package/dist/flavours/django/f.d.ts +6 -0
- package/dist/flavours/django/f.js +91 -0
- package/dist/flavours/django/files.d.ts +76 -0
- package/dist/flavours/django/files.js +338 -0
- package/dist/flavours/django/makeApiCall.d.ts +20 -0
- package/dist/flavours/django/makeApiCall.js +169 -0
- package/dist/flavours/django/manager.d.ts +197 -0
- package/dist/flavours/django/manager.js +222 -0
- package/dist/flavours/django/model.d.ts +112 -0
- package/dist/flavours/django/model.js +253 -0
- package/dist/flavours/django/operationFactory.d.ts +65 -0
- package/dist/flavours/django/operationFactory.js +216 -0
- package/dist/flavours/django/q.d.ts +70 -0
- package/dist/flavours/django/q.js +43 -0
- package/dist/flavours/django/queryExecutor.d.ts +131 -0
- package/dist/flavours/django/queryExecutor.js +468 -0
- package/dist/flavours/django/querySet.d.ts +412 -0
- package/dist/flavours/django/querySet.js +601 -0
- package/dist/flavours/django/tempPk.d.ts +19 -0
- package/dist/flavours/django/tempPk.js +48 -0
- package/dist/flavours/django/utils.d.ts +19 -0
- package/dist/flavours/django/utils.js +29 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +38 -0
- package/dist/react-entry.d.ts +2 -0
- package/dist/react-entry.js +2 -0
- package/dist/reactiveAdaptor.d.ts +24 -0
- package/dist/reactiveAdaptor.js +38 -0
- package/dist/setup.d.ts +15 -0
- package/dist/setup.js +22 -0
- package/dist/syncEngine/cache/cache.d.ts +75 -0
- package/dist/syncEngine/cache/cache.js +341 -0
- package/dist/syncEngine/metrics/metricOptCalcs.d.ts +79 -0
- package/dist/syncEngine/metrics/metricOptCalcs.js +284 -0
- package/dist/syncEngine/registries/metricRegistry.d.ts +53 -0
- package/dist/syncEngine/registries/metricRegistry.js +162 -0
- package/dist/syncEngine/registries/modelStoreRegistry.d.ts +11 -0
- package/dist/syncEngine/registries/modelStoreRegistry.js +56 -0
- package/dist/syncEngine/registries/querysetStoreRegistry.d.ts +55 -0
- package/dist/syncEngine/registries/querysetStoreRegistry.js +244 -0
- package/dist/syncEngine/stores/metricStore.d.ts +55 -0
- package/dist/syncEngine/stores/metricStore.js +222 -0
- package/dist/syncEngine/stores/modelStore.d.ts +40 -0
- package/dist/syncEngine/stores/modelStore.js +405 -0
- package/dist/syncEngine/stores/operation.d.ts +99 -0
- package/dist/syncEngine/stores/operation.js +224 -0
- package/dist/syncEngine/stores/operationEventHandlers.d.ts +8 -0
- package/dist/syncEngine/stores/operationEventHandlers.js +239 -0
- package/dist/syncEngine/stores/querysetStore.d.ts +32 -0
- package/dist/syncEngine/stores/querysetStore.js +200 -0
- package/dist/syncEngine/stores/reactivity.d.ts +3 -0
- package/dist/syncEngine/stores/reactivity.js +4 -0
- package/dist/syncEngine/stores/utils.d.ts +14 -0
- package/dist/syncEngine/stores/utils.js +32 -0
- package/dist/syncEngine/sync.d.ts +32 -0
- package/dist/syncEngine/sync.js +169 -0
- package/dist/vue-entry.d.ts +6 -0
- package/dist/vue-entry.js +2 -0
- package/license.md +116 -0
- package/package.json +123 -0
- 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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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,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
|
+
}
|
package/dist/setup.d.ts
ADDED
|
@@ -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
|
+
}
|