@oddkin/state 0.0.0-stage → 1.0.0-beta.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/LICENSE +21 -0
- package/README.md +82 -2
- package/dist/context/context.d.ts +77 -0
- package/dist/context/context.d.ts.map +1 -0
- package/dist/context/context.js +335 -0
- package/dist/context/context.js.map +1 -0
- package/dist/context/index.d.ts +8 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +7 -0
- package/dist/context/index.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect/index.d.ts +33 -0
- package/dist/inspect/index.d.ts.map +1 -0
- package/dist/inspect/index.js +63 -0
- package/dist/inspect/index.js.map +1 -0
- package/dist/internal/dev.d.ts +6 -0
- package/dist/internal/dev.d.ts.map +1 -0
- package/dist/internal/dev.js +6 -0
- package/dist/internal/dev.js.map +1 -0
- package/dist/persist/index.d.ts +74 -0
- package/dist/persist/index.d.ts.map +1 -0
- package/dist/persist/index.js +228 -0
- package/dist/persist/index.js.map +1 -0
- package/dist/prod/context/context.js +222 -0
- package/dist/prod/context/index.js +8 -0
- package/dist/prod/index.js +7 -0
- package/dist/prod/inspect/index.js +45 -0
- package/dist/prod/internal/dev.js +4 -0
- package/dist/prod/persist/index.js +140 -0
- package/dist/prod/store/index.js +6 -0
- package/dist/prod/store/store.js +167 -0
- package/dist/store/index.d.ts +7 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +6 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/store.d.ts +83 -0
- package/dist/store/store.d.ts.map +1 -0
- package/dist/store/store.js +235 -0
- package/dist/store/store.js.map +1 -0
- package/package.json +50 -4
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@oddkin/state/inspect` — bounded inspection of stores for development tools and tests. DOM-free.
|
|
3
|
+
* A separate import: stores carry only counters and an observer hook, never a log.
|
|
4
|
+
* Contract: docs/architecture/core-contracts.md §7.
|
|
5
|
+
*/
|
|
6
|
+
import { DEV } from '../internal/dev.js';
|
|
7
|
+
import { OddkinError } from '@oddkin/core/diagnostics';
|
|
8
|
+
import { INTERNALS, liveCounts } from '../store/store.js';
|
|
9
|
+
/** Live stores (created and not disposed) and live `subscribe()` listeners, across all stores. */
|
|
10
|
+
export function storeStats() {
|
|
11
|
+
return liveCounts();
|
|
12
|
+
}
|
|
13
|
+
/** Record a store's updates and their actions, keeping the most recent `limit`. */
|
|
14
|
+
export function inspect(store, options) {
|
|
15
|
+
const internals = store[INTERNALS];
|
|
16
|
+
if (!internals) {
|
|
17
|
+
throw new OddkinError('state.inspect.unsupported', DEV
|
|
18
|
+
? 'inspect() needs a store created by createStore() from this copy of @oddkin/state.'
|
|
19
|
+
: '');
|
|
20
|
+
}
|
|
21
|
+
const limit = options?.limit ?? 50;
|
|
22
|
+
if (!Number.isInteger(limit) || limit < 0) {
|
|
23
|
+
throw new OddkinError('state.inspect.limit', DEV ? `inspect() limit must be a whole number of updates (0 or more), not ${limit}.` : '');
|
|
24
|
+
}
|
|
25
|
+
const updates = [];
|
|
26
|
+
let recorded = 0;
|
|
27
|
+
let stopped = false;
|
|
28
|
+
const stop = internals.observe({
|
|
29
|
+
update(record) {
|
|
30
|
+
recorded++;
|
|
31
|
+
if (limit === 0)
|
|
32
|
+
return;
|
|
33
|
+
if (updates.length === limit)
|
|
34
|
+
updates.shift();
|
|
35
|
+
updates.push({ previous: record.previous, next: record.next, actions: record.actions });
|
|
36
|
+
},
|
|
37
|
+
dispose() {
|
|
38
|
+
stopped = true;
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
store,
|
|
43
|
+
get updates() {
|
|
44
|
+
return [...updates];
|
|
45
|
+
},
|
|
46
|
+
get recorded() {
|
|
47
|
+
return recorded;
|
|
48
|
+
},
|
|
49
|
+
get subscribers() {
|
|
50
|
+
return internals.subscribers;
|
|
51
|
+
},
|
|
52
|
+
get stopped() {
|
|
53
|
+
return stopped || store.disposed;
|
|
54
|
+
},
|
|
55
|
+
dispose() {
|
|
56
|
+
if (stopped)
|
|
57
|
+
return;
|
|
58
|
+
stopped = true;
|
|
59
|
+
stop();
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/inspect/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAmC,MAAM,mBAAmB,CAAC;AA6B3F,kGAAkG;AAClG,MAAM,UAAU,UAAU;IACxB,OAAO,UAAU,EAAE,CAAC;AACtB,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,OAAO,CAAI,KAAe,EAAE,OAAwB;IAClE,MAAM,SAAS,GAAI,KAA8D,CAAC,SAAS,CAAC,CAAC;IAC7F,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,WAAW,CACnB,2BAA2B,EAC3B,GAAG;YACD,CAAC,CAAC,mFAAmF;YACrF,CAAC,CAAC,EAAE,CACP,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC;IACnC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,WAAW,CACnB,qBAAqB,EACrB,GAAG,CAAC,CAAC,CAAC,sEAAsE,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,CAC1F,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,GAAqB,EAAE,CAAC;IACrC,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC;QAC7B,MAAM,CAAC,MAAM;YACX,QAAQ,EAAE,CAAC;YACX,IAAI,KAAK,KAAK,CAAC;gBAAE,OAAO;YACxB,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK;gBAAE,OAAO,CAAC,KAAK,EAAE,CAAC;YAC9C,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;QAC1F,CAAC;QACD,OAAO;YACL,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC;KACF,CAAC,CAAC;IACH,OAAO;QACL,KAAK;QACL,IAAI,OAAO;YACT,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;QACtB,CAAC;QACD,IAAI,QAAQ;YACV,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,IAAI,WAAW;YACb,OAAO,SAAS,CAAC,WAAW,CAAC;QAC/B,CAAC;QACD,IAAI,OAAO;YACT,OAAO,OAAO,IAAI,KAAK,CAAC,QAAQ,CAAC;QACnC,CAAC;QACD,OAAO;YACL,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,IAAI,EAAE,CAAC;QACT,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Development-mode flag. The published production build (dist/prod, the default export condition)
|
|
3
|
+
* replaces it with false and removes dead branches; the development build keeps full messages.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEV: boolean;
|
|
6
|
+
//# sourceMappingURL=dev.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/internal/dev.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,GAAG,EAAE,OAAc,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/internal/dev.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,CAAC,MAAM,GAAG,GAAY,IAAI,CAAC"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { OddkinError } from '@oddkin/core/diagnostics';
|
|
2
|
+
import { type Readable } from '@oddkin/core/reactive';
|
|
3
|
+
import { type Store } from '../store/store.js';
|
|
4
|
+
/** The part of the Web Storage API persistence uses; `localStorage` and `sessionStorage` fit. */
|
|
5
|
+
export interface StorageLike {
|
|
6
|
+
getItem(key: string): string | null;
|
|
7
|
+
setItem(key: string, value: string): void;
|
|
8
|
+
removeItem(key: string): void;
|
|
9
|
+
}
|
|
10
|
+
/** What is stored under the key: the persisted data and the version of its shape. */
|
|
11
|
+
export interface PersistedEnvelope<P> {
|
|
12
|
+
readonly version: number;
|
|
13
|
+
readonly data: P;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* `empty`: nothing was stored. `restored`: stored data was migrated if needed, validated, and
|
|
17
|
+
* applied. `rejected`: stored data could not be read, migrated, or validated; the store kept its
|
|
18
|
+
* state and the next change overwrites the entry. `blocked`: the stored version is newer than this
|
|
19
|
+
* code; nothing is written until `clear()`. `unavailable`: there is no usable storage; the store
|
|
20
|
+
* works in memory only.
|
|
21
|
+
*/
|
|
22
|
+
export type PersistStatus = 'empty' | 'restored' | 'rejected' | 'blocked' | 'unavailable';
|
|
23
|
+
interface CommonOptions<P> {
|
|
24
|
+
/** Storage key. One persistence per key. */
|
|
25
|
+
key: string;
|
|
26
|
+
/** Version of the persisted shape: a whole number of 1 or more. */
|
|
27
|
+
version: number;
|
|
28
|
+
/** Storage, or a function returning it (default: `localStorage` when it can be reached). */
|
|
29
|
+
storage?: StorageLike | (() => StorageLike | null | undefined);
|
|
30
|
+
/** Migrations keyed by the version they produce: `{ 2: (v1Data) => v2Data }`. */
|
|
31
|
+
migrate?: Readonly<Record<number, (data: unknown) => unknown>>;
|
|
32
|
+
/** Accept restored data in the current version; return false (or throw) to reject it. */
|
|
33
|
+
validate?: (data: unknown) => boolean;
|
|
34
|
+
/** Decides whether a new selection must be written. Default `Object.is`. */
|
|
35
|
+
equals?: (previous: P, next: P) => boolean;
|
|
36
|
+
/** Default `JSON.stringify`. */
|
|
37
|
+
serialize?: (envelope: PersistedEnvelope<P>) => string;
|
|
38
|
+
/** Default `JSON.parse`. */
|
|
39
|
+
deserialize?: (text: string) => unknown;
|
|
40
|
+
/** Receives storage and data failures instead of the Oddkin error handler. */
|
|
41
|
+
onError?: (error: OddkinError) => void;
|
|
42
|
+
}
|
|
43
|
+
/** Persist the whole state; restoring replaces it unless `restore` merges. */
|
|
44
|
+
export interface PersistStateOptions<T> extends CommonOptions<T> {
|
|
45
|
+
select?: undefined;
|
|
46
|
+
restore?: (state: T, saved: T) => T;
|
|
47
|
+
}
|
|
48
|
+
/** Persist a selection of the state; `restore` puts it back into the current state. */
|
|
49
|
+
export interface PersistSelectionOptions<T, P> extends CommonOptions<P> {
|
|
50
|
+
select: (state: T) => P;
|
|
51
|
+
restore: (state: T, saved: P) => T;
|
|
52
|
+
}
|
|
53
|
+
export interface Persistence {
|
|
54
|
+
readonly key: string;
|
|
55
|
+
/** Outcome of the restore, then `empty` after `clear()`. */
|
|
56
|
+
readonly status: Readable<PersistStatus>;
|
|
57
|
+
readonly disposed: boolean;
|
|
58
|
+
/** Write the current selection now if it differs from what was last written or restored. */
|
|
59
|
+
flush(): void;
|
|
60
|
+
/** Remove the stored entry and allow writing again; the next change writes. */
|
|
61
|
+
clear(): void;
|
|
62
|
+
/** Write a pending change, then stop. Stored data stays. Idempotent. */
|
|
63
|
+
dispose(): void;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Persist a store under `options.key`. Restores synchronously before returning (migrating,
|
|
67
|
+
* validating, and applying stored data as one update named `persist/restore`), then writes the
|
|
68
|
+
* selection after changes, coalesced to one write per microtask. Storage failures are reported and
|
|
69
|
+
* never reach the store, its subscribers, or the caller of `update()`.
|
|
70
|
+
*/
|
|
71
|
+
export declare function persist<T>(store: Store<T>, options: PersistStateOptions<T>): Persistence;
|
|
72
|
+
export declare function persist<T, P>(store: Store<T>, options: PersistSelectionOptions<T, P>): Persistence;
|
|
73
|
+
export {};
|
|
74
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/persist/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,WAAW,EAAqB,MAAM,0BAA0B,CAAC;AAC1E,OAAO,EAAoB,KAAK,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACxE,OAAO,EAAU,KAAK,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAEvD,iGAAiG;AACjG,MAAM,WAAW,WAAW;IAC1B,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;CAC/B;AAED,qFAAqF;AACrF,MAAM,WAAW,iBAAiB,CAAC,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;CAClB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,aAAa,CAAC;AAE1F,UAAU,aAAa,CAAC,CAAC;IACvB,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAC;IAChB,4FAA4F;IAC5F,OAAO,CAAC,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAC/D,iFAAiF;IACjF,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC;IAC/D,yFAAyF;IACzF,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC;IACtC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC;IAC3C,gCAAgC;IAChC,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,iBAAiB,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC;IACvD,4BAA4B;IAC5B,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,8EAA8E;IAC9E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;CACxC;AAED,8EAA8E;AAC9E,MAAM,WAAW,mBAAmB,CAAC,CAAC,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC;IAC9D,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC;CACrC;AAED,uFAAuF;AACvF,MAAM,WAAW,uBAAuB,CAAC,CAAC,EAAE,CAAC,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC;IACrE,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC;IACxB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,aAAa,CAAC,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,4FAA4F;IAC5F,KAAK,IAAI,IAAI,CAAC;IACd,+EAA+E;IAC/E,KAAK,IAAI,IAAI,CAAC;IACd,wEAAwE;IACxE,OAAO,IAAI,IAAI,CAAC;CACjB;AAUD;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC;AAC1F,wBAAgB,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,uBAAuB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,WAAW,CAAC"}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@oddkin/state/persist` — an optional, versioned persistence adapter for one store
|
|
3
|
+
* (core-contracts.md §7.3). DOM-free: storage is reached only when `persist()` runs, and any Web
|
|
4
|
+
* Storage-like object works. Nothing is persisted unless it is selected explicitly or the whole
|
|
5
|
+
* store is opted in; no component property is ever persisted automatically.
|
|
6
|
+
*/
|
|
7
|
+
import { DEV } from '../internal/dev.js';
|
|
8
|
+
import { OddkinError, reportOddkinError } from '@oddkin/core/diagnostics';
|
|
9
|
+
import { computed, signal } from '@oddkin/core/reactive';
|
|
10
|
+
import { action } from '../store/store.js';
|
|
11
|
+
const NOTHING = Symbol('nothing written');
|
|
12
|
+
function isEnvelope(value) {
|
|
13
|
+
if (typeof value !== 'object' || value === null || !('data' in value))
|
|
14
|
+
return false;
|
|
15
|
+
const version = value.version;
|
|
16
|
+
return typeof version === 'number' && Number.isInteger(version) && version >= 1;
|
|
17
|
+
}
|
|
18
|
+
export function persist(store, options) {
|
|
19
|
+
const { key, version } = options;
|
|
20
|
+
if (typeof key !== 'string' || key === '' || !Number.isInteger(version) || version < 1) {
|
|
21
|
+
throw new OddkinError('state.persist.options', DEV
|
|
22
|
+
? `persist() needs a non-empty key and a whole-number version of 1 or more, not key ${JSON.stringify(key)} and version ${String(version)}.`
|
|
23
|
+
: '');
|
|
24
|
+
}
|
|
25
|
+
if (store.disposed) {
|
|
26
|
+
throw new OddkinError('state.store.disposed', DEV ? `persist() cannot follow a disposed store (key "${key}").` : '');
|
|
27
|
+
}
|
|
28
|
+
const select = (options.select ?? ((state) => state));
|
|
29
|
+
const restore = (options.restore ?? ((_state, saved) => saved));
|
|
30
|
+
const equals = (options.equals ?? Object.is);
|
|
31
|
+
const serialize = (options.serialize ?? JSON.stringify);
|
|
32
|
+
const deserialize = options.deserialize ?? JSON.parse;
|
|
33
|
+
const status = signal('empty');
|
|
34
|
+
const fail = (code, message, cause) => {
|
|
35
|
+
const error = new OddkinError(code, message, cause === undefined ? undefined : { cause });
|
|
36
|
+
if (!options.onError)
|
|
37
|
+
return reportOddkinError(error, { code, origin: key });
|
|
38
|
+
try {
|
|
39
|
+
options.onError(error);
|
|
40
|
+
}
|
|
41
|
+
catch (handlerError) {
|
|
42
|
+
reportOddkinError(handlerError, { code, origin: key });
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const reject = (code, message, cause) => {
|
|
46
|
+
status.value = 'rejected';
|
|
47
|
+
fail(code, message, cause);
|
|
48
|
+
};
|
|
49
|
+
let storage;
|
|
50
|
+
try {
|
|
51
|
+
const source = options.storage ?? (() => globalThis.localStorage);
|
|
52
|
+
storage = (typeof source === 'function' ? source() : source) ?? undefined;
|
|
53
|
+
if (!storage) {
|
|
54
|
+
fail('state.persist.unavailable', DEV
|
|
55
|
+
? `No storage for "${key}": localStorage does not exist here. Pass a storage option; the store keeps working in memory.`
|
|
56
|
+
: '');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
storage = undefined;
|
|
61
|
+
fail('state.persist.unavailable', DEV
|
|
62
|
+
? `Storage for "${key}" cannot be reached (for example, storage is disabled or the page is sandboxed); the store keeps working in memory.`
|
|
63
|
+
: '', error);
|
|
64
|
+
}
|
|
65
|
+
let text = null;
|
|
66
|
+
if (storage) {
|
|
67
|
+
try {
|
|
68
|
+
text = storage.getItem(key);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
storage = undefined;
|
|
72
|
+
fail('state.persist.read', DEV ? `Reading "${key}" from storage failed; the store keeps working in memory.` : '', error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!storage)
|
|
76
|
+
status.value = 'unavailable';
|
|
77
|
+
let blocked = false;
|
|
78
|
+
if (storage && text !== null) {
|
|
79
|
+
let envelope;
|
|
80
|
+
let parsed = true;
|
|
81
|
+
try {
|
|
82
|
+
envelope = deserialize(text);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
parsed = false;
|
|
86
|
+
reject('state.persist.parse', DEV ? `Stored data for "${key}" could not be parsed; it is ignored.` : '', error);
|
|
87
|
+
}
|
|
88
|
+
if (parsed && !isEnvelope(envelope)) {
|
|
89
|
+
reject('state.persist.parse', DEV
|
|
90
|
+
? `Stored data for "${key}" is not a { version, data } envelope with a whole-number version; it is ignored.`
|
|
91
|
+
: '');
|
|
92
|
+
}
|
|
93
|
+
else if (parsed && isEnvelope(envelope) && envelope.version > version) {
|
|
94
|
+
blocked = true;
|
|
95
|
+
status.value = 'blocked';
|
|
96
|
+
fail('state.persist.version', DEV
|
|
97
|
+
? `Stored data for "${key}" has version ${envelope.version}, newer than ${version}. It is not applied and not overwritten; call clear() to discard it.`
|
|
98
|
+
: '');
|
|
99
|
+
}
|
|
100
|
+
else if (parsed && isEnvelope(envelope)) {
|
|
101
|
+
let data = envelope.data;
|
|
102
|
+
let migrated = true;
|
|
103
|
+
for (let next = envelope.version + 1; next <= version && migrated; next++) {
|
|
104
|
+
const step = options.migrate?.[next];
|
|
105
|
+
if (!step) {
|
|
106
|
+
migrated = false;
|
|
107
|
+
reject('state.persist.migrate', DEV
|
|
108
|
+
? `Stored data for "${key}" has version ${envelope.version}, and there is no migration to version ${next}; it is ignored.`
|
|
109
|
+
: '');
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
data = step(data);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
migrated = false;
|
|
117
|
+
reject('state.persist.migrate', DEV ? `Migrating "${key}" to version ${next} failed; the stored data is ignored.` : '', error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (migrated) {
|
|
121
|
+
let valid = true;
|
|
122
|
+
let cause;
|
|
123
|
+
try {
|
|
124
|
+
valid = options.validate ? options.validate(data) : true;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
valid = false;
|
|
128
|
+
cause = error;
|
|
129
|
+
}
|
|
130
|
+
if (valid) {
|
|
131
|
+
try {
|
|
132
|
+
action('persist/restore', () => store.update((state) => restore(state, data)))();
|
|
133
|
+
status.value = 'restored';
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
valid = false;
|
|
137
|
+
cause = error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!valid) {
|
|
141
|
+
reject('state.persist.invalid', DEV
|
|
142
|
+
? `Stored data for "${key}" was rejected by validate() or restore(); the store keeps its state.`
|
|
143
|
+
: '', cause);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
let written = select(store.getSnapshot());
|
|
149
|
+
let pending = false;
|
|
150
|
+
let failing = false;
|
|
151
|
+
let disposed = false;
|
|
152
|
+
const write = () => {
|
|
153
|
+
pending = false;
|
|
154
|
+
if (!storage || blocked)
|
|
155
|
+
return;
|
|
156
|
+
const selection = select(store.getSnapshot());
|
|
157
|
+
if (written !== NOTHING && equals(written, selection))
|
|
158
|
+
return;
|
|
159
|
+
let serialized;
|
|
160
|
+
try {
|
|
161
|
+
serialized = serialize({ version, data: selection });
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (!failing)
|
|
165
|
+
fail('state.persist.serialize', DEV
|
|
166
|
+
? `The state for "${key}" could not be serialized (for example a cycle or a BigInt with JSON); it was not written.`
|
|
167
|
+
: '', error);
|
|
168
|
+
failing = true;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
storage.setItem(key, serialized);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (!failing)
|
|
176
|
+
fail('state.persist.write', DEV
|
|
177
|
+
? `Writing "${key}" to storage failed (for example the quota is exceeded); later changes try again.`
|
|
178
|
+
: '', error);
|
|
179
|
+
failing = true;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
failing = false;
|
|
183
|
+
written = selection;
|
|
184
|
+
};
|
|
185
|
+
const unsubscribe = store.subscribe(() => {
|
|
186
|
+
if (pending || disposed)
|
|
187
|
+
return;
|
|
188
|
+
pending = true;
|
|
189
|
+
queueMicrotask(() => {
|
|
190
|
+
if (pending && !disposed)
|
|
191
|
+
write();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
return {
|
|
195
|
+
key,
|
|
196
|
+
status: computed(() => status.value),
|
|
197
|
+
get disposed() {
|
|
198
|
+
return disposed;
|
|
199
|
+
},
|
|
200
|
+
flush() {
|
|
201
|
+
if (!disposed)
|
|
202
|
+
write();
|
|
203
|
+
},
|
|
204
|
+
clear() {
|
|
205
|
+
if (disposed || !storage)
|
|
206
|
+
return;
|
|
207
|
+
try {
|
|
208
|
+
storage.removeItem(key);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
fail('state.persist.write', DEV ? `Removing "${key}" from storage failed.` : '', error);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
blocked = false;
|
|
215
|
+
written = NOTHING;
|
|
216
|
+
status.value = 'empty';
|
|
217
|
+
},
|
|
218
|
+
dispose() {
|
|
219
|
+
if (disposed)
|
|
220
|
+
return;
|
|
221
|
+
if (pending)
|
|
222
|
+
write();
|
|
223
|
+
disposed = true;
|
|
224
|
+
unsubscribe();
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/persist/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { OddkinError, reportOddkinError } from "@oddkin/core/diagnostics";
|
|
2
|
+
import { READABLE, batch, computed, isReadable, signal } from "@oddkin/core/reactive";
|
|
3
|
+
function createContext(name) {
|
|
4
|
+
return Object.freeze({ name });
|
|
5
|
+
}
|
|
6
|
+
class ContextRequestEvent extends Event {
|
|
7
|
+
context;
|
|
8
|
+
/** The requesting element; `target` is retargeted across shadow roots. */
|
|
9
|
+
contextTarget;
|
|
10
|
+
callback;
|
|
11
|
+
/** True when the callback may be called again as the value changes. */
|
|
12
|
+
subscribe;
|
|
13
|
+
constructor(context, contextTarget, callback, subscribe = !1) {
|
|
14
|
+
super("context-request", { bubbles: !0, composed: !0 }), this.context = context, this.contextTarget = contextTarget, this.callback = callback, this.subscribe = subscribe;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
class ContextProviderEvent extends Event {
|
|
18
|
+
context;
|
|
19
|
+
contextTarget;
|
|
20
|
+
constructor(context, contextTarget) {
|
|
21
|
+
super("context-provider", { bubbles: !0, composed: !0 }), this.context = context, this.contextTarget = contextTarget;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const CONSUMER = /* @__PURE__ */ Symbol("oddkin.context.consumer"), ATTACH = /* @__PURE__ */ Symbol("oddkin.context.attach"), DETACH = /* @__PURE__ */ Symbol("oddkin.context.detach"), targetOf = (event) => event.contextTarget ?? event.composedPath()[0];
|
|
25
|
+
class ConsumerImpl {
|
|
26
|
+
[READABLE] = !0;
|
|
27
|
+
context;
|
|
28
|
+
host;
|
|
29
|
+
#fallback;
|
|
30
|
+
#required;
|
|
31
|
+
#source = signal(void 0);
|
|
32
|
+
#view;
|
|
33
|
+
#callback;
|
|
34
|
+
#local;
|
|
35
|
+
#unsubscribe;
|
|
36
|
+
#connection;
|
|
37
|
+
#answered = !1;
|
|
38
|
+
constructor(host, context, options) {
|
|
39
|
+
this.host = host, this.context = context, this.#fallback = options?.fallback, this.#required = options?.required ?? !1, this.#view = computed(() => {
|
|
40
|
+
const source = this.#source.value;
|
|
41
|
+
return source ? source.value : this.#fallback;
|
|
42
|
+
});
|
|
43
|
+
const callback = (value, unsubscribe) => this.#receive(value, unsubscribe);
|
|
44
|
+
callback[CONSUMER] = this, this.#callback = callback;
|
|
45
|
+
}
|
|
46
|
+
get value() {
|
|
47
|
+
return this.#view.value;
|
|
48
|
+
}
|
|
49
|
+
peek() {
|
|
50
|
+
return this.#view.peek();
|
|
51
|
+
}
|
|
52
|
+
subscribe = (onChange) => this.#view.subscribe(onChange);
|
|
53
|
+
get provided() {
|
|
54
|
+
return this.#source.value !== void 0;
|
|
55
|
+
}
|
|
56
|
+
connect(scope) {
|
|
57
|
+
if (scope.disposed)
|
|
58
|
+
return;
|
|
59
|
+
this.#disconnect();
|
|
60
|
+
const connection = { scope, waiting: void 0 };
|
|
61
|
+
this.#connection = connection, scope.add(() => {
|
|
62
|
+
this.#connection === connection && this.#disconnect();
|
|
63
|
+
}, "context consumer"), this.#request(), this.#required && this.#source.peek() === void 0 && reportOddkinError(new OddkinError("state.context.missing", ""), { code: "state.context.missing", origin: this.host.localName });
|
|
64
|
+
}
|
|
65
|
+
resolve() {
|
|
66
|
+
this.#request();
|
|
67
|
+
}
|
|
68
|
+
[ATTACH](view, unsubscribe) {
|
|
69
|
+
this.#answered = !0, this.#switch(unsubscribe), this.#source.value = view, this.#stopWaiting();
|
|
70
|
+
}
|
|
71
|
+
[DETACH](unsubscribe) {
|
|
72
|
+
unsubscribe === this.#unsubscribe && (this.#unsubscribe = void 0, this.#source.value = void 0, this.host.isConnected && this.#request());
|
|
73
|
+
}
|
|
74
|
+
#receive(value, unsubscribe) {
|
|
75
|
+
this.#answered = !0, batch(() => {
|
|
76
|
+
this.#switch(unsubscribe), this.#local ? this.#local.value = value : this.#local = signal(value), this.#source.value = this.#local;
|
|
77
|
+
}), this.#stopWaiting();
|
|
78
|
+
}
|
|
79
|
+
/** Adopt a provider's unsubscribe function, leaving the previous provider. */
|
|
80
|
+
#switch(unsubscribe) {
|
|
81
|
+
if (unsubscribe === this.#unsubscribe)
|
|
82
|
+
return;
|
|
83
|
+
const previous = this.#unsubscribe;
|
|
84
|
+
this.#unsubscribe = unsubscribe, previous?.();
|
|
85
|
+
}
|
|
86
|
+
#request() {
|
|
87
|
+
if (!this.#connection || (this.#answered = !1, this.host.dispatchEvent(new ContextRequestEvent(this.context, this.host, this.#callback, !0)), this.#answered || !this.#connection))
|
|
88
|
+
return;
|
|
89
|
+
const previous = this.#unsubscribe;
|
|
90
|
+
this.#unsubscribe = void 0, this.#source.value = void 0, previous?.(), this.#startWaiting();
|
|
91
|
+
}
|
|
92
|
+
#startWaiting() {
|
|
93
|
+
const connection = this.#connection;
|
|
94
|
+
!connection || connection.waiting || (connection.waiting = connection.scope.listen(this.host.ownerDocument, "context-provider", (event) => {
|
|
95
|
+
event.context === this.context && this.#request();
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
#stopWaiting() {
|
|
99
|
+
const connection = this.#connection, release = connection?.waiting;
|
|
100
|
+
!connection || !release || (connection.waiting = void 0, release());
|
|
101
|
+
}
|
|
102
|
+
#disconnect() {
|
|
103
|
+
const connection = this.#connection;
|
|
104
|
+
if (!connection)
|
|
105
|
+
return;
|
|
106
|
+
this.#connection = void 0, connection.waiting?.();
|
|
107
|
+
const previous = this.#unsubscribe;
|
|
108
|
+
this.#unsubscribe = void 0, this.#source.value = void 0, previous?.();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
class ProviderImpl {
|
|
112
|
+
context;
|
|
113
|
+
host;
|
|
114
|
+
#current;
|
|
115
|
+
#view;
|
|
116
|
+
#subscribers = /* @__PURE__ */ new Map();
|
|
117
|
+
#own;
|
|
118
|
+
#foreign;
|
|
119
|
+
#connection;
|
|
120
|
+
constructor(host, context, value) {
|
|
121
|
+
this.host = host, this.context = context, this.#current = signal(this.#readable(value)), this.#view = computed(() => this.#current.value.value);
|
|
122
|
+
}
|
|
123
|
+
get value() {
|
|
124
|
+
return this.#view.value;
|
|
125
|
+
}
|
|
126
|
+
set value(next) {
|
|
127
|
+
this.#current.value = this.#readable(next);
|
|
128
|
+
}
|
|
129
|
+
get consumers() {
|
|
130
|
+
return this.#subscribers.size;
|
|
131
|
+
}
|
|
132
|
+
connect(scope) {
|
|
133
|
+
if (scope.disposed)
|
|
134
|
+
return;
|
|
135
|
+
this.#disconnect();
|
|
136
|
+
const connection = {};
|
|
137
|
+
this.#connection = connection, scope.add(() => {
|
|
138
|
+
this.#connection === connection && this.#disconnect();
|
|
139
|
+
}, "context provider"), scope.listen(this.host, "context-request", (event) => {
|
|
140
|
+
this.#connection === connection && this.#onRequest(event);
|
|
141
|
+
}), scope.listen(this.host, "context-provider", (event) => {
|
|
142
|
+
this.#connection === connection && this.#onProvider(event);
|
|
143
|
+
}), this.host.dispatchEvent(new ContextProviderEvent(this.context, this.host));
|
|
144
|
+
}
|
|
145
|
+
#readable(value) {
|
|
146
|
+
return isReadable(value) ? value : (this.#own ? this.#own.value = value : this.#own = signal(value), this.#own);
|
|
147
|
+
}
|
|
148
|
+
#onRequest(event) {
|
|
149
|
+
if (event.context !== this.context)
|
|
150
|
+
return;
|
|
151
|
+
const target = targetOf(event);
|
|
152
|
+
if (target === this.host)
|
|
153
|
+
return;
|
|
154
|
+
event.stopImmediatePropagation();
|
|
155
|
+
const callback = event.callback;
|
|
156
|
+
if (!event.subscribe) {
|
|
157
|
+
callback(this.#view.peek());
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
let subscription = this.#subscribers.get(callback);
|
|
161
|
+
if (!subscription) {
|
|
162
|
+
const created = {
|
|
163
|
+
host: target,
|
|
164
|
+
consumer: callback[CONSUMER],
|
|
165
|
+
unsubscribe: () => this.#remove(callback, created)
|
|
166
|
+
};
|
|
167
|
+
subscription = created, this.#subscribers.set(callback, created);
|
|
168
|
+
}
|
|
169
|
+
subscription.consumer ? subscription.consumer[ATTACH](this.#view, subscription.unsubscribe) : (this.#watch(), callback(this.#view.peek(), subscription.unsubscribe));
|
|
170
|
+
}
|
|
171
|
+
/** A provider appeared below this one: consumers now closer to it move there. */
|
|
172
|
+
#onProvider(event) {
|
|
173
|
+
if (!(event.context !== this.context || targetOf(event) === this.host)) {
|
|
174
|
+
event.stopPropagation();
|
|
175
|
+
for (const [callback, subscription] of [...this.#subscribers])
|
|
176
|
+
this.#subscribers.get(callback) === subscription && subscription.host.dispatchEvent(new ContextRequestEvent(this.context, subscription.host, callback, !0));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** Other implementations' consumers receive values through their callbacks, each isolated. */
|
|
180
|
+
#watch() {
|
|
181
|
+
this.#foreign || (this.#foreign = this.#view.subscribe((value) => {
|
|
182
|
+
for (const [callback, subscription] of [...this.#subscribers])
|
|
183
|
+
if (!(subscription.consumer || this.#subscribers.get(callback) !== subscription))
|
|
184
|
+
try {
|
|
185
|
+
callback(value, subscription.unsubscribe);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
reportOddkinError(error, { code: "state.context.callback", origin: this.context.name });
|
|
188
|
+
}
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
#remove(callback, subscription) {
|
|
192
|
+
if (this.#subscribers.get(callback) === subscription && (this.#subscribers.delete(callback), !!this.#foreign)) {
|
|
193
|
+
for (const other of this.#subscribers.values())
|
|
194
|
+
if (!other.consumer)
|
|
195
|
+
return;
|
|
196
|
+
this.#foreign(), this.#foreign = void 0;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/** Stop providing: consumers look for the next provider up (or wait for one). */
|
|
200
|
+
#disconnect() {
|
|
201
|
+
if (!this.#connection)
|
|
202
|
+
return;
|
|
203
|
+
this.#connection = void 0, this.#foreign?.(), this.#foreign = void 0;
|
|
204
|
+
const subscriptions = [...this.#subscribers];
|
|
205
|
+
this.#subscribers.clear();
|
|
206
|
+
for (const [callback, subscription] of subscriptions)
|
|
207
|
+
subscription.consumer ? subscription.consumer[DETACH](subscription.unsubscribe) : subscription.host.isConnected && subscription.host.dispatchEvent(new ContextRequestEvent(this.context, subscription.host, callback, !0));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function provide(host, context, value) {
|
|
211
|
+
return new ProviderImpl(host, context, value);
|
|
212
|
+
}
|
|
213
|
+
function consume(host, context, options) {
|
|
214
|
+
return new ConsumerImpl(host, context, options);
|
|
215
|
+
}
|
|
216
|
+
export {
|
|
217
|
+
ContextProviderEvent,
|
|
218
|
+
ContextRequestEvent,
|
|
219
|
+
consume,
|
|
220
|
+
createContext,
|
|
221
|
+
provide
|
|
222
|
+
};
|