@codebelt/classy-store 0.0.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/README.md +48 -0
- package/dist/index.cjs +234 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +215 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +215 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +226 -0
- package/dist/index.mjs.map +1 -0
- package/dist/snapshot-TbHIUjvP.cjs +594 -0
- package/dist/snapshot-TbHIUjvP.cjs.map +1 -0
- package/dist/snapshot-fVu34Cr6.mjs +546 -0
- package/dist/snapshot-fVu34Cr6.mjs.map +1 -0
- package/dist/utils/index.cjs +203 -0
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.d.cts +122 -0
- package/dist/utils/index.d.cts.map +1 -0
- package/dist/utils/index.d.mts +122 -0
- package/dist/utils/index.d.mts.map +1 -0
- package/dist/utils/index.mjs +202 -0
- package/dist/utils/index.mjs.map +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { a as subscribe, s as findGetterDescriptor, t as snapshot } from "../snapshot-fVu34Cr6.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/utils/persist.ts
|
|
4
|
+
/** Check if a value is a PropertyTransform descriptor (has `key` + `serialize`). */
|
|
5
|
+
function isTransform(entry) {
|
|
6
|
+
return typeof entry === "object" && entry !== null && "key" in entry && "serialize" in entry;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Build the normalized list of property keys and their optional transforms.
|
|
10
|
+
* If `properties` is not provided, defaults to all own enumerable data properties
|
|
11
|
+
* of the store (excluding getters and methods).
|
|
12
|
+
*/
|
|
13
|
+
function resolveProperties(proxyStore, properties) {
|
|
14
|
+
if (properties) return properties.map((entry) => {
|
|
15
|
+
if (isTransform(entry)) return {
|
|
16
|
+
key: entry.key,
|
|
17
|
+
transform: entry
|
|
18
|
+
};
|
|
19
|
+
return { key: entry };
|
|
20
|
+
});
|
|
21
|
+
const snap = snapshot(proxyStore);
|
|
22
|
+
const result = [];
|
|
23
|
+
for (const key of Object.keys(snap)) {
|
|
24
|
+
if (findGetterDescriptor(Object.getPrototypeOf(proxyStore), key)?.get) continue;
|
|
25
|
+
if (typeof proxyStore[key] === "function") continue;
|
|
26
|
+
result.push({ key });
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Detect if the given storage adapter is `globalThis.localStorage`.
|
|
32
|
+
*/
|
|
33
|
+
function isLocalStorage(storage) {
|
|
34
|
+
try {
|
|
35
|
+
return typeof globalThis !== "undefined" && typeof globalThis.localStorage !== "undefined" && storage === globalThis.localStorage;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get the default storage adapter (`localStorage`), or `undefined` if unavailable.
|
|
42
|
+
*/
|
|
43
|
+
function getDefaultStorage() {
|
|
44
|
+
try {
|
|
45
|
+
if (typeof globalThis !== "undefined" && typeof globalThis.localStorage !== "undefined") return globalThis.localStorage;
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Persist store state to a storage adapter.
|
|
50
|
+
*
|
|
51
|
+
* Subscribes to store mutations and writes the selected properties to storage
|
|
52
|
+
* (with optional per-property transforms, debouncing, and versioned envelopes).
|
|
53
|
+
* On init (or manual rehydrate), reads from storage and applies the state back
|
|
54
|
+
* to the store proxy.
|
|
55
|
+
*
|
|
56
|
+
* @param proxyStore - A reactive proxy created by `store()`.
|
|
57
|
+
* @param options - Persistence configuration.
|
|
58
|
+
* @returns A handle with lifecycle controls (unsubscribe, save, clear, rehydrate, hydrated).
|
|
59
|
+
*/
|
|
60
|
+
function persist(proxyStore, options) {
|
|
61
|
+
const { name, properties: propertiesOption, debounce: debounceMs = 0, version = 0, migrate, merge = "shallow", skipHydration = false, syncTabs: syncTabsOption } = options;
|
|
62
|
+
const maybeStorage = options.storage ?? getDefaultStorage();
|
|
63
|
+
if (!maybeStorage) throw new Error("@codebelt/classy-store: persist() requires a storage adapter. No localStorage found — provide a `storage` option.");
|
|
64
|
+
const storage = maybeStorage;
|
|
65
|
+
const resolvedProps = resolveProperties(proxyStore, propertiesOption);
|
|
66
|
+
const transformMap = /* @__PURE__ */ new Map();
|
|
67
|
+
for (const prop of resolvedProps) if (prop.transform) transformMap.set(prop.key, prop.transform);
|
|
68
|
+
const propKeys = resolvedProps.map((p) => p.key);
|
|
69
|
+
let disposed = false;
|
|
70
|
+
let debounceTimer = null;
|
|
71
|
+
let hydratedFlag = false;
|
|
72
|
+
let resolveHydrated;
|
|
73
|
+
let rejectHydrated;
|
|
74
|
+
const hydratedPromise = new Promise((resolve, reject) => {
|
|
75
|
+
resolveHydrated = resolve;
|
|
76
|
+
rejectHydrated = reject;
|
|
77
|
+
});
|
|
78
|
+
/** Serialize the current store state into a JSON string (versioned envelope). */
|
|
79
|
+
function serializeState() {
|
|
80
|
+
const snap = snapshot(proxyStore);
|
|
81
|
+
const state = {};
|
|
82
|
+
for (const key of propKeys) {
|
|
83
|
+
let value = snap[key];
|
|
84
|
+
const transform = transformMap.get(key);
|
|
85
|
+
if (transform) value = transform.serialize(value);
|
|
86
|
+
state[key] = value;
|
|
87
|
+
}
|
|
88
|
+
const envelope = {
|
|
89
|
+
version,
|
|
90
|
+
state
|
|
91
|
+
};
|
|
92
|
+
return JSON.stringify(envelope);
|
|
93
|
+
}
|
|
94
|
+
/** Write the current state to storage. */
|
|
95
|
+
async function writeToStorage() {
|
|
96
|
+
if (disposed) return;
|
|
97
|
+
const json = serializeState();
|
|
98
|
+
await storage.setItem(name, json);
|
|
99
|
+
}
|
|
100
|
+
/** Schedule a debounced write (or write immediately if debounce is 0). */
|
|
101
|
+
function scheduleWrite() {
|
|
102
|
+
if (disposed) return;
|
|
103
|
+
if (debounceMs <= 0) {
|
|
104
|
+
writeToStorage();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (debounceTimer !== null) clearTimeout(debounceTimer);
|
|
108
|
+
debounceTimer = setTimeout(() => {
|
|
109
|
+
debounceTimer = null;
|
|
110
|
+
writeToStorage();
|
|
111
|
+
}, debounceMs);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Parse a raw JSON string from storage, apply migration and transforms,
|
|
115
|
+
* and merge the result into the store proxy.
|
|
116
|
+
*/
|
|
117
|
+
function applyPersistedState(raw) {
|
|
118
|
+
let envelope;
|
|
119
|
+
try {
|
|
120
|
+
envelope = JSON.parse(raw);
|
|
121
|
+
} catch {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!envelope || typeof envelope !== "object" || typeof envelope.state !== "object") return;
|
|
125
|
+
let { state } = envelope;
|
|
126
|
+
if (migrate && envelope.version !== version) state = migrate(state, envelope.version);
|
|
127
|
+
for (const key of Object.keys(state)) {
|
|
128
|
+
const transform = transformMap.get(key);
|
|
129
|
+
if (transform) state[key] = transform.deserialize(state[key]);
|
|
130
|
+
}
|
|
131
|
+
const currentSnap = snapshot(proxyStore);
|
|
132
|
+
const currentState = {};
|
|
133
|
+
for (const key of propKeys) currentState[key] = currentSnap[key];
|
|
134
|
+
let merged;
|
|
135
|
+
if (typeof merge === "function") merged = merge(state, currentState);
|
|
136
|
+
else merged = {
|
|
137
|
+
...currentState,
|
|
138
|
+
...state
|
|
139
|
+
};
|
|
140
|
+
for (const key of propKeys) if (key in merged) proxyStore[key] = merged[key];
|
|
141
|
+
}
|
|
142
|
+
/** Read from storage and apply to the store. */
|
|
143
|
+
async function hydrateFromStorage() {
|
|
144
|
+
const raw = await storage.getItem(name);
|
|
145
|
+
if (raw !== null) applyPersistedState(raw);
|
|
146
|
+
}
|
|
147
|
+
const shouldSyncTabs = syncTabsOption !== void 0 ? syncTabsOption : isLocalStorage(storage);
|
|
148
|
+
/** Handler for `window.storage` events. */
|
|
149
|
+
function onStorageEvent(event) {
|
|
150
|
+
if (disposed) return;
|
|
151
|
+
if (event.key !== name) return;
|
|
152
|
+
if (event.newValue === null) return;
|
|
153
|
+
applyPersistedState(event.newValue);
|
|
154
|
+
}
|
|
155
|
+
if (shouldSyncTabs && typeof globalThis !== "undefined" && typeof globalThis.addEventListener === "function") globalThis.addEventListener("storage", onStorageEvent);
|
|
156
|
+
const unsubscribeFromStore = subscribe(proxyStore, scheduleWrite);
|
|
157
|
+
if (!skipHydration) hydrateFromStorage().then(() => {
|
|
158
|
+
hydratedFlag = true;
|
|
159
|
+
resolveHydrated();
|
|
160
|
+
}).catch((error) => {
|
|
161
|
+
hydratedFlag = true;
|
|
162
|
+
rejectHydrated(error);
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
get isHydrated() {
|
|
166
|
+
return hydratedFlag;
|
|
167
|
+
},
|
|
168
|
+
hydrated: hydratedPromise,
|
|
169
|
+
unsubscribe() {
|
|
170
|
+
if (disposed) return;
|
|
171
|
+
disposed = true;
|
|
172
|
+
if (debounceTimer !== null) {
|
|
173
|
+
clearTimeout(debounceTimer);
|
|
174
|
+
debounceTimer = null;
|
|
175
|
+
}
|
|
176
|
+
unsubscribeFromStore();
|
|
177
|
+
if (shouldSyncTabs && typeof globalThis !== "undefined" && typeof globalThis.removeEventListener === "function") globalThis.removeEventListener("storage", onStorageEvent);
|
|
178
|
+
},
|
|
179
|
+
async save() {
|
|
180
|
+
if (disposed) return;
|
|
181
|
+
if (debounceTimer !== null) {
|
|
182
|
+
clearTimeout(debounceTimer);
|
|
183
|
+
debounceTimer = null;
|
|
184
|
+
}
|
|
185
|
+
await writeToStorage();
|
|
186
|
+
},
|
|
187
|
+
async clear() {
|
|
188
|
+
await storage.removeItem(name);
|
|
189
|
+
},
|
|
190
|
+
async rehydrate() {
|
|
191
|
+
await hydrateFromStorage();
|
|
192
|
+
if (!hydratedFlag) {
|
|
193
|
+
hydratedFlag = true;
|
|
194
|
+
resolveHydrated();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
//#endregion
|
|
201
|
+
export { persist };
|
|
202
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/utils/persist.ts"],"sourcesContent":["import {subscribe} from '../core';\nimport {snapshot} from '../snapshot';\nimport {findGetterDescriptor} from '../utils';\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\n/**\n * Storage adapter interface. Compatible with `localStorage`, `sessionStorage`,\n * `AsyncStorage`, `localForage`, or any custom implementation.\n *\n * Methods may return synchronously or asynchronously — `persist` handles both.\n */\nexport type StorageAdapter = {\n getItem: (name: string) => string | null | Promise<string | null>;\n setItem: (name: string, value: string) => void | Promise<void>;\n removeItem: (name: string) => void | Promise<void>;\n};\n\n/**\n * Describes a per-property serialization transform.\n *\n * Use this when a property's value is not JSON-serializable (Date, ReactiveMap, etc.)\n * and needs custom conversion to/from a storable format.\n */\nexport type PropertyTransform<T extends object> = {\n /** The property key on the store class. */\n key: keyof T;\n\n /** Transform the value BEFORE saving to storage (after snapshot, before JSON.stringify). */\n serialize: (value: T[keyof T]) => unknown;\n\n /** Transform the value AFTER loading from storage (after JSON.parse, before applying to store). */\n deserialize: (stored: unknown) => T[keyof T];\n};\n\n/**\n * Options for `persist()`.\n */\nexport type PersistOptions<T extends object> = {\n /** Unique storage key. Required. */\n name: string;\n\n /**\n * Storage adapter. Defaults to `globalThis.localStorage`.\n * Any object with getItem/setItem/removeItem (sync or async).\n * Works with: localStorage, sessionStorage, AsyncStorage, localForage, etc.\n */\n storage?: StorageAdapter;\n\n /**\n * Which properties to persist.\n * Each entry is either a plain key (string) or a transform descriptor.\n * Defaults to all own enumerable data properties.\n *\n * **Getters and methods are always excluded.** Class getters (e.g., `get remaining()`)\n * are computed/derived values — they are not source-of-truth state. They recompute\n * automatically from the persisted data properties when accessed. Persisting a getter\n * result would be redundant and could produce stale values on restore.\n */\n properties?: Array<keyof T | PropertyTransform<T>>;\n\n /**\n * Debounce writes to storage (milliseconds).\n * Multiple rapid mutations coalesce into one write.\n * Default: 0 (write after every batched mutation).\n */\n debounce?: number;\n\n /**\n * Schema version number. Stored alongside the data.\n * When the stored version differs from this value, `migrate` is called.\n * Default: 0.\n */\n version?: number;\n\n /**\n * Migration function. Called when the stored version does not match `version`.\n * Receives the raw parsed state and the old version number.\n * Must return the state in the current shape.\n */\n migrate?: (\n persistedState: Record<string, unknown>,\n oldVersion: number,\n ) => Record<string, unknown>;\n\n /**\n * How to merge persisted state with current store state during hydration.\n *\n * - `'shallow'` (default): persisted values overwrite current values one key at a time.\n * Properties not in storage keep their current value.\n * - `'replace'`: same behavior as `'shallow'` for flat stores — only stored keys are assigned.\n * For nested objects, the entire object is replaced rather than merged.\n * - Custom function: receives `(persistedState, currentState)` and returns merged state.\n * Enables deep merge or any custom logic.\n *\n * This matters when the store adds new properties that don't exist in old\n * persisted data. `'shallow'` preserves the new defaults.\n */\n merge?:\n | 'shallow'\n | 'replace'\n | ((\n persisted: Record<string, unknown>,\n current: Record<string, unknown>,\n ) => Record<string, unknown>);\n\n /**\n * If true, do NOT hydrate automatically on init.\n * You must call `handle.rehydrate()` manually (e.g., in a `useEffect` for SSR).\n * Default: false.\n */\n skipHydration?: boolean;\n\n /**\n * Sync state across browser tabs via the `window.storage` event.\n * When another tab writes to the same storage key, this tab automatically\n * re-hydrates from the new value.\n *\n * Only works with `localStorage` (storage events don't fire for sessionStorage\n * or async adapters).\n *\n * Default: `true` when storage is `localStorage`, `false` otherwise.\n */\n syncTabs?: boolean;\n};\n\n/**\n * Handle returned by `persist()`. Provides control over the persist lifecycle.\n */\nexport type PersistHandle = {\n /** Stop persisting and clean up (unsubscribe + cancel pending debounce + remove storage event listener). */\n unsubscribe: () => void;\n\n /** Promise that resolves when initial hydration from storage is complete. */\n hydrated: Promise<void>;\n\n /** Whether hydration from storage has completed. */\n isHydrated: boolean;\n\n /** Manually trigger a write to storage (bypasses debounce). */\n save: () => Promise<void>;\n\n /** Clear this store's persisted data from storage. */\n clear: () => Promise<void>;\n\n /** Manually re-hydrate the store from storage. */\n rehydrate: () => Promise<void>;\n};\n\n// ── Storage envelope ─────────────────────────────────────────────────────────\n\n/** Internal shape of the JSON stored in storage. */\ntype PersistEnvelope = {\n version: number;\n state: Record<string, unknown>;\n};\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\n/** Check if a value is a PropertyTransform descriptor (has `key` + `serialize`). */\nfunction isTransform<T extends object>(\n entry: keyof T | PropertyTransform<T>,\n): entry is PropertyTransform<T> {\n return (\n typeof entry === 'object' &&\n entry !== null &&\n 'key' in entry &&\n 'serialize' in entry\n );\n}\n\n/**\n * Build the normalized list of property keys and their optional transforms.\n * If `properties` is not provided, defaults to all own enumerable data properties\n * of the store (excluding getters and methods).\n */\nfunction resolveProperties<T extends object>(\n proxyStore: T,\n properties?: Array<keyof T | PropertyTransform<T>>,\n): Array<{key: string; transform?: PropertyTransform<T>}> {\n if (properties) {\n return properties.map((entry) => {\n if (isTransform(entry)) {\n return {key: entry.key as string, transform: entry};\n }\n return {key: entry as string};\n });\n }\n\n // Default: all own enumerable keys that are not getters or methods.\n const snap = snapshot(proxyStore) as Record<string, unknown>;\n const result: Array<{key: string}> = [];\n for (const key of Object.keys(snap)) {\n // Skip getters (they live on the prototype, but snapshot installs them).\n // We check the original store's target for getter descriptors.\n if (findGetterDescriptor(Object.getPrototypeOf(proxyStore), key)?.get)\n continue;\n // Skip functions (methods).\n const value = (proxyStore as Record<string, unknown>)[key];\n if (typeof value === 'function') continue;\n result.push({key});\n }\n return result;\n}\n\n/**\n * Detect if the given storage adapter is `globalThis.localStorage`.\n */\nfunction isLocalStorage(storage: StorageAdapter): boolean {\n try {\n return (\n typeof globalThis !== 'undefined' &&\n typeof globalThis.localStorage !== 'undefined' &&\n storage === (globalThis.localStorage as unknown as StorageAdapter)\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Get the default storage adapter (`localStorage`), or `undefined` if unavailable.\n */\nfunction getDefaultStorage(): StorageAdapter | undefined {\n try {\n if (\n typeof globalThis !== 'undefined' &&\n typeof globalThis.localStorage !== 'undefined'\n ) {\n return globalThis.localStorage as unknown as StorageAdapter;\n }\n } catch {\n // SSR or restricted environment — no localStorage.\n }\n return undefined;\n}\n\n// ── Main implementation ──────────────────────────────────────────────────────\n\n/**\n * Persist store state to a storage adapter.\n *\n * Subscribes to store mutations and writes the selected properties to storage\n * (with optional per-property transforms, debouncing, and versioned envelopes).\n * On init (or manual rehydrate), reads from storage and applies the state back\n * to the store proxy.\n *\n * @param proxyStore - A reactive proxy created by `store()`.\n * @param options - Persistence configuration.\n * @returns A handle with lifecycle controls (unsubscribe, save, clear, rehydrate, hydrated).\n */\nexport function persist<T extends object>(\n proxyStore: T,\n options: PersistOptions<T>,\n): PersistHandle {\n const {\n name,\n properties: propertiesOption,\n debounce: debounceMs = 0,\n version = 0,\n migrate,\n merge = 'shallow',\n skipHydration = false,\n syncTabs: syncTabsOption,\n } = options;\n\n const maybeStorage = options.storage ?? getDefaultStorage();\n if (!maybeStorage) {\n throw new Error(\n '@codebelt/classy-store: persist() requires a storage adapter. ' +\n 'No localStorage found — provide a `storage` option.',\n );\n }\n const storage: StorageAdapter = maybeStorage;\n\n const resolvedProps = resolveProperties(proxyStore, propertiesOption);\n\n // Build a map of key → transform for fast lookup during save/restore.\n const transformMap = new Map<string, PropertyTransform<T>>();\n for (const prop of resolvedProps) {\n if (prop.transform) {\n transformMap.set(prop.key, prop.transform);\n }\n }\n\n const propKeys = resolvedProps.map((p) => p.key);\n\n // ── State ────────────────────────────────────────────────────────────────\n\n let disposed = false;\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n let hydratedFlag = false;\n\n // Hydration promise + resolver.\n let resolveHydrated: () => void;\n let rejectHydrated: (error: unknown) => void;\n const hydratedPromise = new Promise<void>((resolve, reject) => {\n resolveHydrated = resolve;\n rejectHydrated = reject;\n });\n\n // ── Save logic ───────────────────────────────────────────────────────────\n\n /** Serialize the current store state into a JSON string (versioned envelope). */\n function serializeState(): string {\n const snap = snapshot(proxyStore) as Record<string, unknown>;\n const state: Record<string, unknown> = {};\n\n for (const key of propKeys) {\n let value = snap[key];\n const transform = transformMap.get(key);\n if (transform) {\n value = transform.serialize(value as T[keyof T]);\n }\n state[key] = value;\n }\n\n const envelope: PersistEnvelope = {version, state};\n return JSON.stringify(envelope);\n }\n\n /** Write the current state to storage. */\n async function writeToStorage(): Promise<void> {\n if (disposed) return;\n const json = serializeState();\n await storage.setItem(name, json);\n }\n\n /** Schedule a debounced write (or write immediately if debounce is 0). */\n function scheduleWrite(): void {\n if (disposed) return;\n\n if (debounceMs <= 0) {\n void writeToStorage();\n return;\n }\n\n if (debounceTimer !== null) {\n clearTimeout(debounceTimer);\n }\n debounceTimer = setTimeout(() => {\n debounceTimer = null;\n void writeToStorage();\n }, debounceMs);\n }\n\n // ── Restore logic ────────────────────────────────────────────────────────\n\n /**\n * Parse a raw JSON string from storage, apply migration and transforms,\n * and merge the result into the store proxy.\n */\n function applyPersistedState(raw: string): void {\n let envelope: PersistEnvelope;\n try {\n envelope = JSON.parse(raw) as PersistEnvelope;\n } catch {\n // Corrupted data — skip.\n return;\n }\n\n if (\n !envelope ||\n typeof envelope !== 'object' ||\n typeof envelope.state !== 'object'\n ) {\n return;\n }\n\n let {state} = envelope;\n\n // Version migration.\n if (migrate && envelope.version !== version) {\n state = migrate(state, envelope.version);\n }\n\n // Per-property deserialize transforms.\n for (const key of Object.keys(state)) {\n const transform = transformMap.get(key);\n if (transform) {\n state[key] = transform.deserialize(state[key]);\n }\n }\n\n // Build current state for merge.\n const currentSnap = snapshot(proxyStore) as Record<string, unknown>;\n const currentState: Record<string, unknown> = {};\n for (const key of propKeys) {\n currentState[key] = currentSnap[key];\n }\n\n // Merge strategy.\n let merged: Record<string, unknown>;\n if (typeof merge === 'function') {\n merged = merge(state, currentState);\n } else {\n // Both 'shallow' and 'replace' assign persisted keys onto the store.\n // The difference is conceptual for nested objects, but at this level\n // both just assign the persisted value per key.\n merged = {...currentState, ...state};\n }\n\n // Apply to store proxy (goes through SET traps → reactivity).\n for (const key of propKeys) {\n if (key in merged) {\n (proxyStore as Record<string, unknown>)[key] = merged[key];\n }\n }\n }\n\n /** Read from storage and apply to the store. */\n async function hydrateFromStorage(): Promise<void> {\n const raw = await storage.getItem(name);\n if (raw !== null) {\n applyPersistedState(raw);\n }\n }\n\n // ── Cross-tab sync ───────────────────────────────────────────────────────\n\n const shouldSyncTabs =\n syncTabsOption !== undefined ? syncTabsOption : isLocalStorage(storage);\n\n /** Handler for `window.storage` events. */\n function onStorageEvent(event: StorageEvent): void {\n if (disposed) return;\n if (event.key !== name) return;\n if (event.newValue === null) return; // cleared\n applyPersistedState(event.newValue);\n }\n\n if (\n shouldSyncTabs &&\n typeof globalThis !== 'undefined' &&\n typeof globalThis.addEventListener === 'function'\n ) {\n globalThis.addEventListener('storage', onStorageEvent);\n }\n\n // ── Subscribe to store mutations ─────────────────────────────────────────\n\n const unsubscribeFromStore = subscribe(proxyStore, scheduleWrite);\n\n // ── Kick off initial hydration ───────────────────────────────────────────\n\n if (!skipHydration) {\n void hydrateFromStorage()\n .then(() => {\n hydratedFlag = true;\n resolveHydrated();\n })\n .catch((error) => {\n hydratedFlag = true;\n rejectHydrated(error);\n });\n } else {\n // When hydration is skipped, the promise is left pending until\n // the user calls handle.rehydrate() manually.\n }\n\n // ── Build handle ─────────────────────────────────────────────────────────\n\n const handle: PersistHandle = {\n get isHydrated() {\n return hydratedFlag;\n },\n\n hydrated: hydratedPromise,\n\n unsubscribe() {\n if (disposed) return;\n disposed = true;\n\n // Cancel pending debounce.\n if (debounceTimer !== null) {\n clearTimeout(debounceTimer);\n debounceTimer = null;\n }\n\n // Unsubscribe from store mutations.\n unsubscribeFromStore();\n\n // Remove cross-tab sync listener.\n if (\n shouldSyncTabs &&\n typeof globalThis !== 'undefined' &&\n typeof globalThis.removeEventListener === 'function'\n ) {\n globalThis.removeEventListener('storage', onStorageEvent);\n }\n },\n\n async save() {\n if (disposed) return;\n // Cancel pending debounce and write immediately.\n if (debounceTimer !== null) {\n clearTimeout(debounceTimer);\n debounceTimer = null;\n }\n await writeToStorage();\n },\n\n async clear() {\n await storage.removeItem(name);\n },\n\n async rehydrate() {\n await hydrateFromStorage();\n if (!hydratedFlag) {\n hydratedFlag = true;\n resolveHydrated();\n }\n },\n };\n\n return handle;\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codebelt/classy-store",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Class-based reactive state management for React — ES6 Proxy + immutable snapshots + useSyncExternalStore",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.mts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.mts",
|
|
13
|
+
"default": "./dist/index.mjs"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"./utils": {
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/utils/index.d.mts",
|
|
23
|
+
"default": "./dist/utils/index.mjs"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/utils/index.d.cts",
|
|
27
|
+
"default": "./dist/utils/index.cjs"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"---------- Dev --------------------------------------------------------": "",
|
|
36
|
+
"dev": "tsdown --watch",
|
|
37
|
+
"---------- Build ------------------------------------------------------": "",
|
|
38
|
+
"build": "tsdown",
|
|
39
|
+
"---------- Test --------------------------------------------------------": "",
|
|
40
|
+
"test": "bun test",
|
|
41
|
+
"---------- Lint --------------------------------------------------------": "",
|
|
42
|
+
"lint": "biome check",
|
|
43
|
+
"lint:fix": "biome check --write",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"---------- Release (Changesets) ----------------------------------------": "",
|
|
46
|
+
"changeset": "changeset",
|
|
47
|
+
"changeset:add": "changeset add",
|
|
48
|
+
"changeset:version": "changeset version",
|
|
49
|
+
"changeset:publish": "bun run build && changeset publish",
|
|
50
|
+
"changeset:status": "changeset status",
|
|
51
|
+
"prerelease:enter": "changeset pre enter",
|
|
52
|
+
"prerelease:exit": "changeset pre exit",
|
|
53
|
+
"---------- Docs (Docusaurus) ------------------------------------------": "",
|
|
54
|
+
"docs:dev": "cd website && bun run start",
|
|
55
|
+
"docs:build": "cd website && bun run build",
|
|
56
|
+
"docs:deploy": "cd website && bun run deploy",
|
|
57
|
+
"---------- CI / Helper ------------------------------------------------": "",
|
|
58
|
+
"checkall": "bun run lint:fix && bun run test && bun run typecheck",
|
|
59
|
+
"clean": "find . -name node_modules -o -name .next -o -name dist -o -name build | xargs rm -rf",
|
|
60
|
+
"deps": "bun update --latest",
|
|
61
|
+
"-----------------------------------------------------------------------": ""
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"proxy-compare": "3.0.1"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"react": ">=18.0.0"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@biomejs/biome": "2.3.15",
|
|
71
|
+
"@changesets/changelog-github": "^0.5.2",
|
|
72
|
+
"@changesets/cli": "^2.29.8",
|
|
73
|
+
"@happy-dom/global-registrator": "20.6.1",
|
|
74
|
+
"@types/bun": "1.3.9",
|
|
75
|
+
"@types/react": "19.2.14",
|
|
76
|
+
"@types/react-dom": "19.2.3",
|
|
77
|
+
"react": "19.2.4",
|
|
78
|
+
"react-dom": "19.2.4",
|
|
79
|
+
"tsdown": "0.20.3",
|
|
80
|
+
"typescript": "5.9.3"
|
|
81
|
+
},
|
|
82
|
+
"keywords": [
|
|
83
|
+
"classy-store",
|
|
84
|
+
"@codebelt/classy-store",
|
|
85
|
+
"classystore",
|
|
86
|
+
"state-management",
|
|
87
|
+
"reactive",
|
|
88
|
+
"proxy",
|
|
89
|
+
"class-based",
|
|
90
|
+
"react",
|
|
91
|
+
"hooks"
|
|
92
|
+
],
|
|
93
|
+
"license": "MIT"
|
|
94
|
+
}
|