@hamak/ui-store 0.5.9 → 0.7.1
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/api/tokens/service-tokens.d.ts +8 -0
- package/dist/api/tokens/service-tokens.d.ts.map +1 -1
- package/dist/api/tokens/service-tokens.js +11 -0
- package/dist/api/types/store-types.d.ts +20 -12
- package/dist/api/types/store-types.d.ts.map +1 -1
- package/dist/impl/core/store-manager.d.ts +13 -1
- package/dist/impl/core/store-manager.d.ts.map +1 -1
- package/dist/impl/core/store-manager.js +17 -0
- package/dist/impl/index.d.ts +1 -0
- package/dist/impl/index.d.ts.map +1 -1
- package/dist/impl/index.js +1 -0
- package/dist/impl/persistence/index.d.ts +9 -0
- package/dist/impl/persistence/index.d.ts.map +1 -0
- package/dist/impl/persistence/index.js +8 -0
- package/dist/impl/persistence/persistence-filter.d.ts +11 -0
- package/dist/impl/persistence/persistence-filter.d.ts.map +1 -0
- package/dist/impl/persistence/persistence-filter.js +24 -0
- package/dist/impl/persistence/persistence-resolver.d.ts +9 -0
- package/dist/impl/persistence/persistence-resolver.d.ts.map +1 -0
- package/dist/impl/persistence/persistence-resolver.js +22 -0
- package/dist/impl/persistence/persistence-wiring.d.ts +29 -0
- package/dist/impl/persistence/persistence-wiring.d.ts.map +1 -0
- package/dist/impl/persistence/persistence-wiring.js +69 -0
- package/dist/impl/persistence/web-storage-persistence-provider.d.ts +46 -0
- package/dist/impl/persistence/web-storage-persistence-provider.d.ts.map +1 -0
- package/dist/impl/persistence/web-storage-persistence-provider.js +155 -0
- package/dist/impl/plugin/store-plugin-factory.d.ts +13 -1
- package/dist/impl/plugin/store-plugin-factory.d.ts.map +1 -1
- package/dist/impl/plugin/store-plugin-factory.js +56 -1
- package/package.json +1 -1
|
@@ -7,4 +7,12 @@ export declare const MIDDLEWARE_REGISTRY_TOKEN: unique symbol;
|
|
|
7
7
|
export declare const REDUCER_REGISTRY_TOKEN: unique symbol;
|
|
8
8
|
export declare const STORE_EXTENSIONS_TOKEN: unique symbol;
|
|
9
9
|
export declare const AUTOSAVE_REGISTRY_TOKEN: unique symbol;
|
|
10
|
+
export declare const PERSISTENCE_PROVIDER_TOKEN: unique symbol;
|
|
11
|
+
/**
|
|
12
|
+
* Global-registry alias for {@link PERSISTENCE_PROVIDER_TOKEN}. The unique
|
|
13
|
+
* symbol above only resolves for consumers that import it from this package;
|
|
14
|
+
* decoupled sibling plugins look the provider up via `Symbol.for(...)` instead
|
|
15
|
+
* (same convention as `Symbol.for('StoreManager')`, see #28 and #5).
|
|
16
|
+
*/
|
|
17
|
+
export declare const PERSISTENCE_PROVIDER_GLOBAL_TOKEN: unique symbol;
|
|
10
18
|
//# sourceMappingURL=service-tokens.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-tokens.d.ts","sourceRoot":"","sources":["../../../src/api/tokens/service-tokens.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,eAAO,MAAM,mBAAmB,eAAyB,CAAC;AAC1D,eAAO,MAAM,yBAAyB,eAA+B,CAAC;AACtE,eAAO,MAAM,sBAAsB,eAA4B,CAAC;AAChE,eAAO,MAAM,sBAAsB,eAA4B,CAAC;AAGhE,eAAO,MAAM,uBAAuB,eAA6B,CAAC"}
|
|
1
|
+
{"version":3,"file":"service-tokens.d.ts","sourceRoot":"","sources":["../../../src/api/tokens/service-tokens.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,eAAO,MAAM,mBAAmB,eAAyB,CAAC;AAC1D,eAAO,MAAM,yBAAyB,eAA+B,CAAC;AACtE,eAAO,MAAM,sBAAsB,eAA4B,CAAC;AAChE,eAAO,MAAM,sBAAsB,eAA4B,CAAC;AAGhE,eAAO,MAAM,uBAAuB,eAA6B,CAAC;AAKlE,eAAO,MAAM,0BAA0B,eAAgC,CAAC;AAExE;;;;;GAKG;AACH,eAAO,MAAM,iCAAiC,eAE7C,CAAC"}
|
|
@@ -8,3 +8,14 @@ export const REDUCER_REGISTRY_TOKEN = Symbol('ReducerRegistry');
|
|
|
8
8
|
export const STORE_EXTENSIONS_TOKEN = Symbol('StoreExtensions');
|
|
9
9
|
// Autosave tokens
|
|
10
10
|
export const AUTOSAVE_REGISTRY_TOKEN = Symbol('AutosaveRegistry');
|
|
11
|
+
// Persistence token — resolves the active IPersistenceProvider so consumers
|
|
12
|
+
// (or sibling plugins) can mount a custom provider instead of the default
|
|
13
|
+
// web-storage one. See amah/app-framework#30.
|
|
14
|
+
export const PERSISTENCE_PROVIDER_TOKEN = Symbol('PersistenceProvider');
|
|
15
|
+
/**
|
|
16
|
+
* Global-registry alias for {@link PERSISTENCE_PROVIDER_TOKEN}. The unique
|
|
17
|
+
* symbol above only resolves for consumers that import it from this package;
|
|
18
|
+
* decoupled sibling plugins look the provider up via `Symbol.for(...)` instead
|
|
19
|
+
* (same convention as `Symbol.for('StoreManager')`, see #28 and #5).
|
|
20
|
+
*/
|
|
21
|
+
export const PERSISTENCE_PROVIDER_GLOBAL_TOKEN = Symbol.for('@hamak/ui-store:PersistenceProvider');
|
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
* Store Type Definitions
|
|
3
3
|
*/
|
|
4
4
|
import type { Store, Action, StoreEnhancer } from 'redux';
|
|
5
|
+
/**
|
|
6
|
+
* State persistence configuration. Persistence is applied at the slice
|
|
7
|
+
* (top-level reducer key) level: a whitelisted slice is serialized to the
|
|
8
|
+
* configured web storage and rehydrated on boot. See amah/app-framework#30.
|
|
9
|
+
*/
|
|
10
|
+
export interface StorePersistenceConfig {
|
|
11
|
+
/** Enable state persistence */
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
/** Storage key the serialized slice is written under */
|
|
14
|
+
key?: string;
|
|
15
|
+
/** Storage type (indexedDB is not yet implemented and falls back to localStorage) */
|
|
16
|
+
storage?: 'localStorage' | 'sessionStorage' | 'indexedDB';
|
|
17
|
+
/** Slices to persist (top-level reducer keys); when set, only these are written */
|
|
18
|
+
whitelist?: string[];
|
|
19
|
+
/** Slices to never persist (top-level reducer keys); applied after the whitelist */
|
|
20
|
+
blacklist?: string[];
|
|
21
|
+
/** Debounce window (ms) for save-on-change; defaults to 250ms */
|
|
22
|
+
debounceMs?: number;
|
|
23
|
+
}
|
|
5
24
|
/**
|
|
6
25
|
* Store configuration
|
|
7
26
|
*/
|
|
@@ -13,18 +32,7 @@ export interface StoreConfig {
|
|
|
13
32
|
/** Additional store enhancers */
|
|
14
33
|
enhancers?: StoreEnhancer[];
|
|
15
34
|
/** Persistence configuration */
|
|
16
|
-
persistence?:
|
|
17
|
-
/** Enable state persistence */
|
|
18
|
-
enabled: boolean;
|
|
19
|
-
/** Storage key prefix */
|
|
20
|
-
key?: string;
|
|
21
|
-
/** Storage type */
|
|
22
|
-
storage?: 'localStorage' | 'sessionStorage' | 'indexedDB';
|
|
23
|
-
/** Keys to persist (whitelist) */
|
|
24
|
-
whitelist?: string[];
|
|
25
|
-
/** Keys to ignore (blacklist) */
|
|
26
|
-
blacklist?: string[];
|
|
27
|
-
};
|
|
35
|
+
persistence?: StorePersistenceConfig;
|
|
28
36
|
}
|
|
29
37
|
/**
|
|
30
38
|
* Root state shape (can be extended by applications)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store-types.d.ts","sourceRoot":"","sources":["../../../src/api/types/store-types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAE1D
|
|
1
|
+
{"version":3,"file":"store-types.d.ts","sourceRoot":"","sources":["../../../src/api/types/store-types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAE1D;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,+BAA+B;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,OAAO,CAAC,EAAE,cAAc,GAAG,gBAAgB,GAAG,WAAW,CAAC;IAC1D,mFAAmF;IACnF,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,wCAAwC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,8BAA8B;IAC9B,cAAc,CAAC,EAAE,GAAG,CAAC;IAErB,iCAAiC;IACjC,SAAS,CAAC,EAAE,aAAa,EAAE,CAAC;IAE5B,gCAAgC;IAChC,WAAW,CAAC,EAAE,sBAAsB,CAAC;CACtC;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS,CAAC,CAAC,GAAG,GAAG,CAAE,SAAQ,MAAM,CAAC,MAAM,CAAC;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,CAAC,CAAC;IACZ,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,KAAK,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC"}
|
|
@@ -3,21 +3,33 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { type Store, type Reducer } from '@reduxjs/toolkit';
|
|
5
5
|
import type { IStoreManager, StoreConfig, RootState, AppAction } from '../../api/index.js';
|
|
6
|
+
import type { IPersistenceProvider } from '../../spi/index.js';
|
|
6
7
|
import { MiddlewareRegistry } from './middleware-registry.js';
|
|
7
8
|
import { ReducerRegistry } from './reducer-registry.js';
|
|
9
|
+
/**
|
|
10
|
+
* Internal store config. Extends the public {@link StoreConfig} with the
|
|
11
|
+
* resolved persistence provider, which the plugin factory injects after
|
|
12
|
+
* resolving it via DI (see amah/app-framework#30). Kept out of the public
|
|
13
|
+
* type so consumers configure persistence declaratively, not by passing
|
|
14
|
+
* provider instances.
|
|
15
|
+
*/
|
|
16
|
+
export interface StoreManagerConfig extends StoreConfig {
|
|
17
|
+
persistenceProvider?: IPersistenceProvider;
|
|
18
|
+
}
|
|
8
19
|
export declare class StoreManager implements IStoreManager {
|
|
9
20
|
private store;
|
|
10
21
|
private middlewareRegistry;
|
|
11
22
|
private reducerRegistry;
|
|
12
23
|
private initialized;
|
|
13
24
|
private fileSystemAdapter;
|
|
25
|
+
private persistenceUnsubscribe;
|
|
14
26
|
constructor();
|
|
15
27
|
getMiddlewareRegistry(): MiddlewareRegistry;
|
|
16
28
|
getReducerRegistry(): ReducerRegistry;
|
|
17
29
|
setFileSystemAdapter(adapter: any): void;
|
|
18
30
|
getFileSystemAdapter(): any;
|
|
19
31
|
isInitialized(): boolean;
|
|
20
|
-
initialize(config?:
|
|
32
|
+
initialize(config?: StoreManagerConfig): Store<RootState, AppAction>;
|
|
21
33
|
getStore(): Store<RootState, AppAction>;
|
|
22
34
|
dispatch<A extends AppAction>(action: A): A;
|
|
23
35
|
getState<S = RootState>(): S;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store-manager.d.ts","sourceRoot":"","sources":["../../../src/impl/core/store-manager.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAkB,KAAK,KAAK,EAAE,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAClF,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAErD,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,iBAAiB,CAAoB;;
|
|
1
|
+
{"version":3,"file":"store-manager.d.ts","sourceRoot":"","sources":["../../../src/impl/core/store-manager.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAkB,KAAK,KAAK,EAAE,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAClF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAEtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAErD;;;;;;GAMG;AACH,MAAM,WAAW,kBAAmB,SAAQ,WAAW;IACrD,mBAAmB,CAAC,EAAE,oBAAoB,CAAC;CAC5C;AAED,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,sBAAsB,CAA6B;;IAY3D,qBAAqB;IAIrB,kBAAkB;IAIlB,oBAAoB,CAAC,OAAO,EAAE,GAAG;IAIjC,oBAAoB;IAIpB,aAAa,IAAI,OAAO;IAIxB,UAAU,CAAC,MAAM,GAAE,kBAAuB,GAAG,KAAK,CAAC,SAAS,EAAE,SAAS,CAAC;IAiExE,QAAQ,IAAI,KAAK,CAAC,SAAS,EAAE,SAAS,CAAC;IAOvC,QAAQ,CAAC,CAAC,SAAS,SAAS,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC;IAI3C,QAAQ,CAAC,CAAC,GAAG,SAAS,KAAK,CAAC;IAI5B,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAI3C,cAAc,CAAC,WAAW,EAAE,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,GAAG,IAAI;IAIhE,OAAO,IAAI,IAAI;CAShB"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Store Manager Implementation
|
|
3
3
|
*/
|
|
4
4
|
import { configureStore } from '@reduxjs/toolkit';
|
|
5
|
+
import { attachPersistenceSave } from '../persistence/index.js';
|
|
5
6
|
import { MiddlewareRegistry } from './middleware-registry.js';
|
|
6
7
|
import { ReducerRegistry } from './reducer-registry.js';
|
|
7
8
|
export class StoreManager {
|
|
@@ -36,6 +37,12 @@ export class StoreManager {
|
|
|
36
37
|
writable: true,
|
|
37
38
|
value: null
|
|
38
39
|
});
|
|
40
|
+
Object.defineProperty(this, "persistenceUnsubscribe", {
|
|
41
|
+
enumerable: true,
|
|
42
|
+
configurable: true,
|
|
43
|
+
writable: true,
|
|
44
|
+
value: null
|
|
45
|
+
});
|
|
39
46
|
this.middlewareRegistry = new MiddlewareRegistry();
|
|
40
47
|
this.reducerRegistry = new ReducerRegistry((rootReducer) => {
|
|
41
48
|
// Hot replacement callback
|
|
@@ -92,6 +99,12 @@ export class StoreManager {
|
|
|
92
99
|
} : false,
|
|
93
100
|
});
|
|
94
101
|
this.initialized = true;
|
|
102
|
+
// Persist the whitelisted slice on change when persistence is enabled and a
|
|
103
|
+
// provider was injected (rehydration is handled upstream by the plugin via
|
|
104
|
+
// preloadedState, since load() is async). See amah/app-framework#30.
|
|
105
|
+
if (config.persistence?.enabled && config.persistenceProvider) {
|
|
106
|
+
this.persistenceUnsubscribe = attachPersistenceSave(this.store, config.persistenceProvider, config.persistence);
|
|
107
|
+
}
|
|
95
108
|
console.log('[StoreManager] Store initialized with:', {
|
|
96
109
|
reducers: this.reducerRegistry.getAllRegistrations().map(r => r.key),
|
|
97
110
|
middleware: this.middlewareRegistry.getAllRegistrations().map(m => ({
|
|
@@ -122,6 +135,10 @@ export class StoreManager {
|
|
|
122
135
|
this.getStore().replaceReducer(nextReducer);
|
|
123
136
|
}
|
|
124
137
|
destroy() {
|
|
138
|
+
if (this.persistenceUnsubscribe) {
|
|
139
|
+
this.persistenceUnsubscribe();
|
|
140
|
+
this.persistenceUnsubscribe = null;
|
|
141
|
+
}
|
|
125
142
|
this.store = null;
|
|
126
143
|
this.initialized = false;
|
|
127
144
|
console.log('[StoreManager] Store destroyed');
|
package/dist/impl/index.d.ts
CHANGED
package/dist/impl/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/impl/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AAEvB,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC;AACzB,cAAc,MAAM,CAAC;AACrB,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/impl/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AAEvB,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC;AACzB,cAAc,MAAM,CAAC;AACrB,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC"}
|
package/dist/impl/index.js
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State persistence implementations and wiring.
|
|
3
|
+
* See amah/app-framework#30.
|
|
4
|
+
*/
|
|
5
|
+
export * from './web-storage-persistence-provider.js';
|
|
6
|
+
export * from './persistence-filter.js';
|
|
7
|
+
export * from './persistence-resolver.js';
|
|
8
|
+
export * from './persistence-wiring.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/impl/persistence/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,oCAAoC,CAAC;AACnD,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State persistence implementations and wiring.
|
|
3
|
+
* See amah/app-framework#30.
|
|
4
|
+
*/
|
|
5
|
+
export * from './web-storage-persistence-provider.js';
|
|
6
|
+
export * from './persistence-filter.js';
|
|
7
|
+
export * from './persistence-resolver.js';
|
|
8
|
+
export * from './persistence-wiring.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice-level whitelist/blacklist filtering for persisted state.
|
|
3
|
+
*
|
|
4
|
+
* Persistence operates at the top-level reducer key ("slice") granularity:
|
|
5
|
+
* a whitelist keeps only the named slices; a blacklist drops named slices.
|
|
6
|
+
* The whitelist is applied first, then the blacklist. See amah/app-framework#30.
|
|
7
|
+
*/
|
|
8
|
+
import type { StorePersistenceConfig } from '../../api/index.js';
|
|
9
|
+
export type SliceSelection = Pick<StorePersistenceConfig, 'whitelist' | 'blacklist'>;
|
|
10
|
+
export declare function pickPersistableState(state: Record<string, unknown> | null | undefined, selection: SliceSelection): Record<string, unknown>;
|
|
11
|
+
//# sourceMappingURL=persistence-filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistence-filter.d.ts","sourceRoot":"","sources":["../../../src/impl/persistence/persistence-filter.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAExD,MAAM,MAAM,cAAc,GAAG,IAAI,CAC/B,sBAAsB,EACtB,WAAW,GAAG,WAAW,CAC1B,CAAC;AAEF,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,EACjD,SAAS,EAAE,cAAc,GACxB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgBzB"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice-level whitelist/blacklist filtering for persisted state.
|
|
3
|
+
*
|
|
4
|
+
* Persistence operates at the top-level reducer key ("slice") granularity:
|
|
5
|
+
* a whitelist keeps only the named slices; a blacklist drops named slices.
|
|
6
|
+
* The whitelist is applied first, then the blacklist. See amah/app-framework#30.
|
|
7
|
+
*/
|
|
8
|
+
export function pickPersistableState(state, selection) {
|
|
9
|
+
if (!state || typeof state !== 'object')
|
|
10
|
+
return {};
|
|
11
|
+
const { whitelist, blacklist } = selection;
|
|
12
|
+
const result = {};
|
|
13
|
+
for (const key of Object.keys(state)) {
|
|
14
|
+
// A set whitelist constrains to its members (an empty whitelist persists
|
|
15
|
+
// nothing); an unset whitelist imposes no constraint. The blacklist is
|
|
16
|
+
// then applied on top.
|
|
17
|
+
if (whitelist && !whitelist.includes(key))
|
|
18
|
+
continue;
|
|
19
|
+
if (blacklist && blacklist.includes(key))
|
|
20
|
+
continue;
|
|
21
|
+
result[key] = state[key];
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a concrete `IPersistenceProvider` from a `StorePersistenceConfig`.
|
|
3
|
+
* Used to build the default provider when a consumer enables persistence but
|
|
4
|
+
* does not mount a custom one. See amah/app-framework#30.
|
|
5
|
+
*/
|
|
6
|
+
import type { StorePersistenceConfig } from '../../api/index.js';
|
|
7
|
+
import type { IPersistenceProvider } from '../../spi/index.js';
|
|
8
|
+
export declare function resolvePersistenceProvider(persistence: StorePersistenceConfig): IPersistenceProvider;
|
|
9
|
+
//# sourceMappingURL=persistence-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistence-resolver.d.ts","sourceRoot":"","sources":["../../../src/impl/persistence/persistence-resolver.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAMtD,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,sBAAsB,GAClC,oBAAoB,CAiBtB"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a concrete `IPersistenceProvider` from a `StorePersistenceConfig`.
|
|
3
|
+
* Used to build the default provider when a consumer enables persistence but
|
|
4
|
+
* does not mount a custom one. See amah/app-framework#30.
|
|
5
|
+
*/
|
|
6
|
+
import { createLocalStoragePersistenceProvider, createSessionStoragePersistenceProvider, } from './web-storage-persistence-provider.js';
|
|
7
|
+
export function resolvePersistenceProvider(persistence) {
|
|
8
|
+
const storage = persistence.storage ?? 'localStorage';
|
|
9
|
+
switch (storage) {
|
|
10
|
+
case 'sessionStorage':
|
|
11
|
+
return createSessionStoragePersistenceProvider();
|
|
12
|
+
case 'indexedDB':
|
|
13
|
+
// Not yet implemented — fall back to localStorage so enabling it is not a
|
|
14
|
+
// silent no-op. Tracked as a follow-up in #30.
|
|
15
|
+
console.warn("[persistence] storage 'indexedDB' is not implemented yet; " +
|
|
16
|
+
'falling back to localStorage.');
|
|
17
|
+
return createLocalStoragePersistenceProvider();
|
|
18
|
+
case 'localStorage':
|
|
19
|
+
default:
|
|
20
|
+
return createLocalStoragePersistenceProvider();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load/save wiring that connects a Redux store to an `IPersistenceProvider`.
|
|
3
|
+
*
|
|
4
|
+
* - `loadPersistedState` rehydrates the whitelisted slice on boot (async,
|
|
5
|
+
* called before store creation so the result can seed `preloadedState`).
|
|
6
|
+
* - `attachPersistenceSave` subscribes to the store and writes the whitelisted
|
|
7
|
+
* slice back, debounced, on every change. See amah/app-framework#30.
|
|
8
|
+
*/
|
|
9
|
+
import type { Store } from 'redux';
|
|
10
|
+
import type { StorePersistenceConfig } from '../../api/index.js';
|
|
11
|
+
import type { IPersistenceProvider } from '../../spi/index.js';
|
|
12
|
+
/** Default storage key when `persistence.key` is not set. */
|
|
13
|
+
export declare const DEFAULT_PERSISTENCE_KEY = "state";
|
|
14
|
+
/** Default debounce window for save-on-change. */
|
|
15
|
+
export declare const DEFAULT_PERSISTENCE_DEBOUNCE_MS = 250;
|
|
16
|
+
export declare function persistenceKey(persistence: StorePersistenceConfig): string;
|
|
17
|
+
/**
|
|
18
|
+
* Load the persisted slice and re-apply the whitelist/blacklist defensively
|
|
19
|
+
* (in case the config tightened since the data was written). Returns
|
|
20
|
+
* `undefined` when nothing is persisted, so callers can spread it into
|
|
21
|
+
* `preloadedState` without overwriting reducer defaults.
|
|
22
|
+
*/
|
|
23
|
+
export declare function loadPersistedState(provider: IPersistenceProvider, persistence: StorePersistenceConfig): Promise<Record<string, unknown> | undefined>;
|
|
24
|
+
/**
|
|
25
|
+
* Subscribe to the store and persist the whitelisted slice on change,
|
|
26
|
+
* debounced. Returns an unsubscribe function that also cancels a pending save.
|
|
27
|
+
*/
|
|
28
|
+
export declare function attachPersistenceSave(store: Pick<Store, 'getState' | 'subscribe'>, provider: IPersistenceProvider, persistence: StorePersistenceConfig): () => void;
|
|
29
|
+
//# sourceMappingURL=persistence-wiring.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistence-wiring.d.ts","sourceRoot":"","sources":["../../../src/impl/persistence/persistence-wiring.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAGtD,6DAA6D;AAC7D,eAAO,MAAM,uBAAuB,UAAU,CAAC;AAE/C,kDAAkD;AAClD,eAAO,MAAM,+BAA+B,MAAM,CAAC;AAEnD,wBAAgB,cAAc,CAAC,WAAW,EAAE,sBAAsB,GAAG,MAAM,CAE1E;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,oBAAoB,EAC9B,WAAW,EAAE,sBAAsB,GAClC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CAQ9C;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,WAAW,CAAC,EAC5C,QAAQ,EAAE,oBAAoB,EAC9B,WAAW,EAAE,sBAAsB,GAClC,MAAM,IAAI,CAyCZ"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load/save wiring that connects a Redux store to an `IPersistenceProvider`.
|
|
3
|
+
*
|
|
4
|
+
* - `loadPersistedState` rehydrates the whitelisted slice on boot (async,
|
|
5
|
+
* called before store creation so the result can seed `preloadedState`).
|
|
6
|
+
* - `attachPersistenceSave` subscribes to the store and writes the whitelisted
|
|
7
|
+
* slice back, debounced, on every change. See amah/app-framework#30.
|
|
8
|
+
*/
|
|
9
|
+
import { pickPersistableState } from './persistence-filter.js';
|
|
10
|
+
/** Default storage key when `persistence.key` is not set. */
|
|
11
|
+
export const DEFAULT_PERSISTENCE_KEY = 'state';
|
|
12
|
+
/** Default debounce window for save-on-change. */
|
|
13
|
+
export const DEFAULT_PERSISTENCE_DEBOUNCE_MS = 250;
|
|
14
|
+
export function persistenceKey(persistence) {
|
|
15
|
+
return persistence.key ?? DEFAULT_PERSISTENCE_KEY;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Load the persisted slice and re-apply the whitelist/blacklist defensively
|
|
19
|
+
* (in case the config tightened since the data was written). Returns
|
|
20
|
+
* `undefined` when nothing is persisted, so callers can spread it into
|
|
21
|
+
* `preloadedState` without overwriting reducer defaults.
|
|
22
|
+
*/
|
|
23
|
+
export async function loadPersistedState(provider, persistence) {
|
|
24
|
+
const loaded = await provider.load(persistenceKey(persistence));
|
|
25
|
+
if (loaded == null || typeof loaded !== 'object')
|
|
26
|
+
return undefined;
|
|
27
|
+
const filtered = pickPersistableState(loaded, persistence);
|
|
28
|
+
return Object.keys(filtered).length > 0 ? filtered : undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Subscribe to the store and persist the whitelisted slice on change,
|
|
32
|
+
* debounced. Returns an unsubscribe function that also cancels a pending save.
|
|
33
|
+
*/
|
|
34
|
+
export function attachPersistenceSave(store, provider, persistence) {
|
|
35
|
+
const key = persistenceKey(persistence);
|
|
36
|
+
const debounceMs = persistence.debounceMs ?? DEFAULT_PERSISTENCE_DEBOUNCE_MS;
|
|
37
|
+
let timer = null;
|
|
38
|
+
// Seed the baseline with the current slice so a change that leaves the
|
|
39
|
+
// persisted slice untouched (e.g. a non-whitelisted slice updated) never
|
|
40
|
+
// triggers a redundant write — including right after rehydration.
|
|
41
|
+
let lastSerialized = JSON.stringify(pickPersistableState(store.getState(), persistence));
|
|
42
|
+
const flush = () => {
|
|
43
|
+
timer = null;
|
|
44
|
+
const slice = pickPersistableState(store.getState(), persistence);
|
|
45
|
+
const serialized = JSON.stringify(slice);
|
|
46
|
+
// Skip redundant writes when the persisted slice did not actually change.
|
|
47
|
+
if (serialized === lastSerialized)
|
|
48
|
+
return;
|
|
49
|
+
lastSerialized = serialized;
|
|
50
|
+
void provider.save(key, slice);
|
|
51
|
+
};
|
|
52
|
+
const unsubscribe = store.subscribe(() => {
|
|
53
|
+
if (timer !== null)
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
if (debounceMs <= 0) {
|
|
56
|
+
flush();
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
timer = setTimeout(flush, debounceMs);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return () => {
|
|
63
|
+
if (timer !== null) {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
timer = null;
|
|
66
|
+
}
|
|
67
|
+
unsubscribe();
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Storage Persistence Provider
|
|
3
|
+
*
|
|
4
|
+
* Concrete `IPersistenceProvider` backed by a Web Storage area
|
|
5
|
+
* (`localStorage` / `sessionStorage`). When the backing storage is
|
|
6
|
+
* unavailable — SSR, private-mode quota errors, disabled storage — it
|
|
7
|
+
* degrades gracefully to an in-memory map so callers never throw. This mirrors
|
|
8
|
+
* the `createLocalStorageActiveProjectStorage` fallback pattern used
|
|
9
|
+
* downstream. See amah/app-framework#30.
|
|
10
|
+
*/
|
|
11
|
+
import type { IPersistenceProvider } from '../../spi/index.js';
|
|
12
|
+
/** Minimal structural subset of the Web Storage API we depend on. */
|
|
13
|
+
export interface WebStorageLike {
|
|
14
|
+
getItem(key: string): string | null;
|
|
15
|
+
setItem(key: string, value: string): void;
|
|
16
|
+
removeItem(key: string): void;
|
|
17
|
+
key(index: number): string | null;
|
|
18
|
+
readonly length: number;
|
|
19
|
+
}
|
|
20
|
+
/** Default namespace every persisted key is prefixed with. */
|
|
21
|
+
export declare const DEFAULT_PERSISTENCE_NAMESPACE = "@hamak/ui-store";
|
|
22
|
+
export declare class WebStoragePersistenceProvider implements IPersistenceProvider {
|
|
23
|
+
private readonly storage;
|
|
24
|
+
private readonly namespace;
|
|
25
|
+
private readonly memory;
|
|
26
|
+
private readonly available;
|
|
27
|
+
/**
|
|
28
|
+
* @param storage Backing web storage, or `null` to run in-memory only.
|
|
29
|
+
* @param namespace Key prefix; `clear()` only removes keys under this prefix
|
|
30
|
+
* so it never wipes unrelated app state.
|
|
31
|
+
*/
|
|
32
|
+
constructor(storage: WebStorageLike | null, namespace?: string);
|
|
33
|
+
isAvailable(): boolean;
|
|
34
|
+
save(key: string, state: unknown): Promise<void>;
|
|
35
|
+
load(key: string): Promise<any | null>;
|
|
36
|
+
remove(key: string): Promise<void>;
|
|
37
|
+
clear(): Promise<void>;
|
|
38
|
+
private fullKey;
|
|
39
|
+
/** Verify the storage area is usable with a write/remove round-trip. */
|
|
40
|
+
private static probe;
|
|
41
|
+
}
|
|
42
|
+
/** Provider backed by `window.localStorage` (in-memory fallback when absent). */
|
|
43
|
+
export declare function createLocalStoragePersistenceProvider(namespace?: string): WebStoragePersistenceProvider;
|
|
44
|
+
/** Provider backed by `window.sessionStorage` (in-memory fallback when absent). */
|
|
45
|
+
export declare function createSessionStoragePersistenceProvider(namespace?: string): WebStoragePersistenceProvider;
|
|
46
|
+
//# sourceMappingURL=web-storage-persistence-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-storage-persistence-provider.d.ts","sourceRoot":"","sources":["../../../src/impl/persistence/web-storage-persistence-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAEtD,qEAAqE;AACrE,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACpC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,8DAA8D;AAC9D,eAAO,MAAM,6BAA6B,oBAAoB,CAAC;AAI/D,qBAAa,6BAA8B,YAAW,oBAAoB;IAUtE,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAV5B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IACpD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAU;IAEpC;;;;OAIG;gBAEgB,OAAO,EAAE,cAAc,GAAG,IAAI,EAC9B,SAAS,GAAE,MAAsC;IAKpE,WAAW,IAAI,OAAO;IAIhB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAchD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC;IAsBtC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYlC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB5B,OAAO,CAAC,OAAO;IAIf,wEAAwE;IACxE,OAAO,CAAC,MAAM,CAAC,KAAK;CAUrB;AAcD,iFAAiF;AACjF,wBAAgB,qCAAqC,CACnD,SAAS,CAAC,EAAE,MAAM,GACjB,6BAA6B,CAK/B;AAED,mFAAmF;AACnF,wBAAgB,uCAAuC,CACrD,SAAS,CAAC,EAAE,MAAM,GACjB,6BAA6B,CAK/B"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Storage Persistence Provider
|
|
3
|
+
*
|
|
4
|
+
* Concrete `IPersistenceProvider` backed by a Web Storage area
|
|
5
|
+
* (`localStorage` / `sessionStorage`). When the backing storage is
|
|
6
|
+
* unavailable — SSR, private-mode quota errors, disabled storage — it
|
|
7
|
+
* degrades gracefully to an in-memory map so callers never throw. This mirrors
|
|
8
|
+
* the `createLocalStorageActiveProjectStorage` fallback pattern used
|
|
9
|
+
* downstream. See amah/app-framework#30.
|
|
10
|
+
*/
|
|
11
|
+
/** Default namespace every persisted key is prefixed with. */
|
|
12
|
+
export const DEFAULT_PERSISTENCE_NAMESPACE = '@hamak/ui-store';
|
|
13
|
+
const PROBE_KEY = '@hamak/ui-store:__persist_probe__';
|
|
14
|
+
export class WebStoragePersistenceProvider {
|
|
15
|
+
/**
|
|
16
|
+
* @param storage Backing web storage, or `null` to run in-memory only.
|
|
17
|
+
* @param namespace Key prefix; `clear()` only removes keys under this prefix
|
|
18
|
+
* so it never wipes unrelated app state.
|
|
19
|
+
*/
|
|
20
|
+
constructor(storage, namespace = DEFAULT_PERSISTENCE_NAMESPACE) {
|
|
21
|
+
Object.defineProperty(this, "storage", {
|
|
22
|
+
enumerable: true,
|
|
23
|
+
configurable: true,
|
|
24
|
+
writable: true,
|
|
25
|
+
value: storage
|
|
26
|
+
});
|
|
27
|
+
Object.defineProperty(this, "namespace", {
|
|
28
|
+
enumerable: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
writable: true,
|
|
31
|
+
value: namespace
|
|
32
|
+
});
|
|
33
|
+
Object.defineProperty(this, "memory", {
|
|
34
|
+
enumerable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
writable: true,
|
|
37
|
+
value: new Map()
|
|
38
|
+
});
|
|
39
|
+
Object.defineProperty(this, "available", {
|
|
40
|
+
enumerable: true,
|
|
41
|
+
configurable: true,
|
|
42
|
+
writable: true,
|
|
43
|
+
value: void 0
|
|
44
|
+
});
|
|
45
|
+
this.available = WebStoragePersistenceProvider.probe(storage);
|
|
46
|
+
}
|
|
47
|
+
isAvailable() {
|
|
48
|
+
return this.available;
|
|
49
|
+
}
|
|
50
|
+
async save(key, state) {
|
|
51
|
+
const serialized = JSON.stringify(state);
|
|
52
|
+
const fullKey = this.fullKey(key);
|
|
53
|
+
if (this.available && this.storage) {
|
|
54
|
+
try {
|
|
55
|
+
this.storage.setItem(fullKey, serialized);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
console.warn('[persistence] save failed, falling back to memory:', e);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
this.memory.set(fullKey, serialized);
|
|
63
|
+
}
|
|
64
|
+
async load(key) {
|
|
65
|
+
const fullKey = this.fullKey(key);
|
|
66
|
+
let raw = null;
|
|
67
|
+
if (this.available && this.storage) {
|
|
68
|
+
try {
|
|
69
|
+
raw = this.storage.getItem(fullKey);
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
console.warn('[persistence] load failed:', e);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (raw == null) {
|
|
76
|
+
raw = this.memory.get(fullKey) ?? null;
|
|
77
|
+
}
|
|
78
|
+
if (raw == null)
|
|
79
|
+
return null;
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(raw);
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
console.warn('[persistence] could not parse persisted state:', e);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async remove(key) {
|
|
89
|
+
const fullKey = this.fullKey(key);
|
|
90
|
+
this.memory.delete(fullKey);
|
|
91
|
+
if (this.available && this.storage) {
|
|
92
|
+
try {
|
|
93
|
+
this.storage.removeItem(fullKey);
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
console.warn('[persistence] remove failed:', e);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async clear() {
|
|
101
|
+
// Only remove keys under our namespace — never the whole storage area.
|
|
102
|
+
for (const memKey of [...this.memory.keys()]) {
|
|
103
|
+
if (memKey.startsWith(this.namespace))
|
|
104
|
+
this.memory.delete(memKey);
|
|
105
|
+
}
|
|
106
|
+
if (this.available && this.storage) {
|
|
107
|
+
try {
|
|
108
|
+
const toRemove = [];
|
|
109
|
+
for (let i = 0; i < this.storage.length; i++) {
|
|
110
|
+
const k = this.storage.key(i);
|
|
111
|
+
if (k && k.startsWith(this.namespace))
|
|
112
|
+
toRemove.push(k);
|
|
113
|
+
}
|
|
114
|
+
toRemove.forEach((k) => this.storage.removeItem(k));
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
console.warn('[persistence] clear failed:', e);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
fullKey(key) {
|
|
122
|
+
return `${this.namespace}:${key}`;
|
|
123
|
+
}
|
|
124
|
+
/** Verify the storage area is usable with a write/remove round-trip. */
|
|
125
|
+
static probe(storage) {
|
|
126
|
+
if (!storage)
|
|
127
|
+
return false;
|
|
128
|
+
try {
|
|
129
|
+
storage.setItem(PROBE_KEY, '1');
|
|
130
|
+
storage.removeItem(PROBE_KEY);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function getWindowStorage(kind) {
|
|
139
|
+
try {
|
|
140
|
+
const g = globalThis;
|
|
141
|
+
return g[kind] ?? null;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Accessing storage can throw in some sandboxed environments.
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Provider backed by `window.localStorage` (in-memory fallback when absent). */
|
|
149
|
+
export function createLocalStoragePersistenceProvider(namespace) {
|
|
150
|
+
return new WebStoragePersistenceProvider(getWindowStorage('localStorage'), namespace);
|
|
151
|
+
}
|
|
152
|
+
/** Provider backed by `window.sessionStorage` (in-memory fallback when absent). */
|
|
153
|
+
export function createSessionStoragePersistenceProvider(namespace) {
|
|
154
|
+
return new WebStoragePersistenceProvider(getWindowStorage('sessionStorage'), namespace);
|
|
155
|
+
}
|
|
@@ -3,13 +3,25 @@
|
|
|
3
3
|
* Creates a microkernel plugin for Redux store management
|
|
4
4
|
*/
|
|
5
5
|
import type { PluginModule } from '@hamak/microkernel-spi';
|
|
6
|
-
import { type StorePluginExtensions } from '../../api/index.js';
|
|
6
|
+
import { type StorePluginExtensions, type StorePersistenceConfig } from '../../api/index.js';
|
|
7
|
+
import type { IPersistenceProvider } from '../../spi/index.js';
|
|
7
8
|
export declare const FILESYSTEM_ADAPTER_TOKEN = "FILESYSTEM_ADAPTER";
|
|
8
9
|
export interface StorePluginConfig extends StorePluginExtensions {
|
|
9
10
|
/** Enable Redux DevTools integration */
|
|
10
11
|
devTools?: boolean;
|
|
11
12
|
/** Enable logger middleware in development */
|
|
12
13
|
logger?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* State persistence configuration. When `enabled`, the whitelisted slice is
|
|
16
|
+
* rehydrated on boot and saved (debounced) on change. See #30.
|
|
17
|
+
*/
|
|
18
|
+
persistence?: StorePersistenceConfig;
|
|
19
|
+
/**
|
|
20
|
+
* Custom persistence provider to mount instead of the default web-storage
|
|
21
|
+
* one. Takes precedence over any DI-registered provider and over the
|
|
22
|
+
* `persistence.storage` default. See #30.
|
|
23
|
+
*/
|
|
24
|
+
persistenceProvider?: IPersistenceProvider;
|
|
13
25
|
}
|
|
14
26
|
export declare function createStorePlugin(config?: StorePluginConfig): PluginModule;
|
|
15
27
|
//# sourceMappingURL=store-plugin-factory.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store-plugin-factory.d.ts","sourceRoot":"","sources":["../../../src/impl/plugin/store-plugin-factory.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,
|
|
1
|
+
{"version":3,"file":"store-plugin-factory.d.ts","sourceRoot":"","sources":["../../../src/impl/plugin/store-plugin-factory.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAQL,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC5B,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AActD,eAAO,MAAM,wBAAwB,uBAAuB,CAAC;AAE7D,MAAM,WAAW,iBAAkB,SAAQ,qBAAqB;IAC9D,wCAAwC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,8CAA8C;IAC9C,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,WAAW,CAAC,EAAE,sBAAsB,CAAC;IAErC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,oBAAoB,CAAC;CAC5C;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,GAAE,iBAAsB,GAC7B,YAAY,CAmMd"}
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Store Plugin Factory
|
|
3
3
|
* Creates a microkernel plugin for Redux store management
|
|
4
4
|
*/
|
|
5
|
-
import { STORE_MANAGER_TOKEN, MIDDLEWARE_REGISTRY_TOKEN, REDUCER_REGISTRY_TOKEN, STORE_EXTENSIONS_TOKEN, } from '../../api/index.js';
|
|
5
|
+
import { STORE_MANAGER_TOKEN, MIDDLEWARE_REGISTRY_TOKEN, REDUCER_REGISTRY_TOKEN, STORE_EXTENSIONS_TOKEN, PERSISTENCE_PROVIDER_TOKEN, PERSISTENCE_PROVIDER_GLOBAL_TOKEN, } from '../../api/index.js';
|
|
6
6
|
import { StoreManager } from '../core/store-manager.js';
|
|
7
7
|
import { createEventBridgeMiddleware, createLoggerMiddleware, } from '../middleware/index.js';
|
|
8
8
|
import { applyStoreExtensions, createStoreExtensionsCollector, } from '../extensions/store-extensions.js';
|
|
9
9
|
import { createFileSystemAdapter } from '../fs/core/fs-adapter.js';
|
|
10
|
+
import { resolvePersistenceProvider, loadPersistedState } from '../persistence/index.js';
|
|
10
11
|
// DI token for filesystem adapter
|
|
11
12
|
export const FILESYSTEM_ADAPTER_TOKEN = 'FILESYSTEM_ADAPTER';
|
|
12
13
|
export function createStorePlugin(config = {}) {
|
|
@@ -14,6 +15,8 @@ export function createStorePlugin(config = {}) {
|
|
|
14
15
|
const middlewareRegistry = storeManager.getMiddlewareRegistry();
|
|
15
16
|
const reducerRegistry = storeManager.getReducerRegistry();
|
|
16
17
|
const extensionsCollector = createStoreExtensionsCollector();
|
|
18
|
+
// Resolved once in initialize(), reused in activate() for rehydrate + save.
|
|
19
|
+
let persistenceProvider;
|
|
17
20
|
// Create filesystem adapter with 'fs' slice name
|
|
18
21
|
const fileSystemAdapter = createFileSystemAdapter('fs');
|
|
19
22
|
const registerDefaultMiddleware = (hooks) => {
|
|
@@ -95,15 +98,67 @@ export function createStorePlugin(config = {}) {
|
|
|
95
98
|
provide: FILESYSTEM_ADAPTER_TOKEN,
|
|
96
99
|
useValue: fileSystemAdapter,
|
|
97
100
|
});
|
|
101
|
+
// Back-compat aliases (global symbol registry). The unique-symbol tokens
|
|
102
|
+
// above only resolve for consumers that import them from
|
|
103
|
+
// `@hamak/ui-store-api`. Several sibling plugins (ui-navigation, auth,
|
|
104
|
+
// notification, event-channel) deliberately stay decoupled and look the
|
|
105
|
+
// store services up via `Symbol.for(...)` keys instead. Register the same
|
|
106
|
+
// instances under those global keys so those lookups succeed instead of
|
|
107
|
+
// silently no-opping. See amah/app-framework#28 and #5.
|
|
108
|
+
ctx.provide({
|
|
109
|
+
provide: Symbol.for('StoreManager'),
|
|
110
|
+
useValue: storeManager,
|
|
111
|
+
});
|
|
112
|
+
ctx.provide({
|
|
113
|
+
provide: Symbol.for('@hamak/ui-store:StoreExtensionsRegistry'),
|
|
114
|
+
useValue: extensionsCollector,
|
|
115
|
+
});
|
|
116
|
+
// Resolve the persistence provider (Option B, #30). Precedence:
|
|
117
|
+
// 1. explicit mount-time provider (config.persistenceProvider)
|
|
118
|
+
// 2. a provider another plugin already registered under the token
|
|
119
|
+
// 3. a default built from `persistence.storage`
|
|
120
|
+
// The resolved instance is re-provided under both the unique and global
|
|
121
|
+
// tokens so sibling plugins can look it up the same way they do the store.
|
|
122
|
+
persistenceProvider = config.persistenceProvider;
|
|
123
|
+
if (!persistenceProvider) {
|
|
124
|
+
try {
|
|
125
|
+
persistenceProvider = ctx.resolve(PERSISTENCE_PROVIDER_TOKEN);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// No provider registered yet — fall through to the default.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!persistenceProvider && config.persistence?.enabled) {
|
|
132
|
+
persistenceProvider = resolvePersistenceProvider(config.persistence);
|
|
133
|
+
}
|
|
134
|
+
if (persistenceProvider) {
|
|
135
|
+
ctx.provide({
|
|
136
|
+
provide: PERSISTENCE_PROVIDER_TOKEN,
|
|
137
|
+
useValue: persistenceProvider,
|
|
138
|
+
});
|
|
139
|
+
ctx.provide({
|
|
140
|
+
provide: PERSISTENCE_PROVIDER_GLOBAL_TOKEN,
|
|
141
|
+
useValue: persistenceProvider,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
98
144
|
registerDefaultMiddleware(ctx.hooks);
|
|
99
145
|
registerConfigExtensions();
|
|
100
146
|
console.log('[ui-store] Plugin initialized with filesystem support');
|
|
101
147
|
},
|
|
102
148
|
async activate(ctx) {
|
|
103
149
|
applyStoreExtensions(extensionsCollector, middlewareRegistry, reducerRegistry);
|
|
150
|
+
// Rehydrate the persisted slice before store creation (load() is async,
|
|
151
|
+
// but preloadedState must be supplied synchronously). See #30.
|
|
152
|
+
let preloadedState;
|
|
153
|
+
if (persistenceProvider && config.persistence?.enabled) {
|
|
154
|
+
preloadedState = await loadPersistedState(persistenceProvider, config.persistence);
|
|
155
|
+
}
|
|
104
156
|
// Initialize the store after all plugins have registered middleware/reducers
|
|
105
157
|
const store = storeManager.initialize({
|
|
106
158
|
devTools: config.devTools,
|
|
159
|
+
preloadedState,
|
|
160
|
+
persistence: config.persistence,
|
|
161
|
+
persistenceProvider,
|
|
107
162
|
});
|
|
108
163
|
// Bridge Redux state changes to microkernel events
|
|
109
164
|
store.subscribe(() => {
|