@chromahq/store 0.0.3 → 0.0.5
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/index.cjs.js +405 -0
- package/dist/index.d.ts +129 -0
- package/dist/index.es.js +378 -0
- package/package.json +1 -1
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React = require('react');
|
|
4
|
+
var vanilla = require('zustand/vanilla');
|
|
5
|
+
|
|
6
|
+
function _interopNamespaceDefault(e) {
|
|
7
|
+
var n = Object.create(null);
|
|
8
|
+
if (e) {
|
|
9
|
+
Object.keys(e).forEach(function (k) {
|
|
10
|
+
if (k !== 'default') {
|
|
11
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
12
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () { return e[k]; }
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
n.default = e;
|
|
20
|
+
return Object.freeze(n);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
|
|
24
|
+
|
|
25
|
+
function chromeStoragePersist(options) {
|
|
26
|
+
return (config) => (set, get, store) => {
|
|
27
|
+
const key = options.name;
|
|
28
|
+
let isInitialized = false;
|
|
29
|
+
let persistenceSetup = false;
|
|
30
|
+
const initialState = config(set, get, store);
|
|
31
|
+
const loadPersistedState = async () => {
|
|
32
|
+
try {
|
|
33
|
+
if (!chrome?.storage?.local) {
|
|
34
|
+
console.warn(`Chrome storage not available for "${key}", using memory only`);
|
|
35
|
+
isInitialized = true;
|
|
36
|
+
setupPersistence();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const result = await new Promise((resolve, reject) => {
|
|
40
|
+
chrome.storage.local.get([key], (result2) => {
|
|
41
|
+
if (chrome.runtime.lastError) {
|
|
42
|
+
reject(chrome.runtime.lastError);
|
|
43
|
+
} else {
|
|
44
|
+
resolve(result2);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
if (result[key]) {
|
|
49
|
+
const mergedState = { ...initialState, ...result[key] };
|
|
50
|
+
set(mergedState);
|
|
51
|
+
} else {
|
|
52
|
+
await persistState(initialState);
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(`Failed to load persisted state for "${key}":`, error);
|
|
56
|
+
} finally {
|
|
57
|
+
isInitialized = true;
|
|
58
|
+
setupPersistence();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const persistState = async (state) => {
|
|
62
|
+
if (!chrome?.storage?.local) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
chrome.storage.local.set({ [key]: state }, () => {
|
|
67
|
+
if (chrome.runtime.lastError) {
|
|
68
|
+
console.error(`Failed to persist state for "${key}":`, chrome.runtime.lastError);
|
|
69
|
+
}
|
|
70
|
+
resolve();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
const setupPersistence = () => {
|
|
75
|
+
if (persistenceSetup) return;
|
|
76
|
+
persistenceSetup = true;
|
|
77
|
+
store.subscribe((state) => {
|
|
78
|
+
if (!isInitialized) return;
|
|
79
|
+
persistState(state);
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
loadPersistedState();
|
|
83
|
+
return initialState;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function useCentralStore(store, selector) {
|
|
88
|
+
return React.useSyncExternalStore(
|
|
89
|
+
store.subscribe,
|
|
90
|
+
() => selector(store.getState()),
|
|
91
|
+
() => selector(store.getState())
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
function useCentralDispatch(store) {
|
|
95
|
+
return store.setState;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class BridgeStore {
|
|
99
|
+
constructor(bridge, initialState, storeName = "default") {
|
|
100
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
101
|
+
this.currentState = null;
|
|
102
|
+
this.previousState = null;
|
|
103
|
+
this.ready = false;
|
|
104
|
+
this.initialize = async () => {
|
|
105
|
+
try {
|
|
106
|
+
console.log("Initializing bridge store:", this.storeName, this.bridge);
|
|
107
|
+
const state = await this.bridge.send(`store:${this.storeName}:getState`);
|
|
108
|
+
this.previousState = this.currentState;
|
|
109
|
+
this.currentState = state;
|
|
110
|
+
this.notifyListeners();
|
|
111
|
+
this.ready = true;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.warn("Failed to initialize bridge store:", error);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
this.notifyListeners = () => {
|
|
117
|
+
if (!this.listeners) {
|
|
118
|
+
console.warn("BridgeStore: listeners not initialized");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (this.currentState && this.previousState) {
|
|
122
|
+
this.listeners.forEach((listener) => listener(this.currentState, this.previousState));
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
this.getState = () => {
|
|
126
|
+
return this.currentState;
|
|
127
|
+
};
|
|
128
|
+
this.subscribe = (listener) => {
|
|
129
|
+
if (!this.listeners) {
|
|
130
|
+
console.error("BridgeStore: Cannot subscribe, listeners not initialized");
|
|
131
|
+
return () => {
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
this.listeners.add(listener);
|
|
135
|
+
if (this.currentState && this.previousState) {
|
|
136
|
+
listener(this.currentState, this.previousState);
|
|
137
|
+
}
|
|
138
|
+
return () => {
|
|
139
|
+
if (this.listeners) {
|
|
140
|
+
this.listeners.delete(listener);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
// Additional StoreApi methods
|
|
145
|
+
this.destroy = () => {
|
|
146
|
+
if (this.listeners) {
|
|
147
|
+
this.listeners.clear();
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
this.getInitialState = () => {
|
|
151
|
+
return this.getState();
|
|
152
|
+
};
|
|
153
|
+
this.bridge = bridge;
|
|
154
|
+
this.currentState = initialState || null;
|
|
155
|
+
this.previousState = initialState || null;
|
|
156
|
+
this.storeName = storeName;
|
|
157
|
+
this.setupStateSync();
|
|
158
|
+
this.initialize();
|
|
159
|
+
}
|
|
160
|
+
setupStateSync() {
|
|
161
|
+
if (this.bridge.on) {
|
|
162
|
+
this.bridge.on(`store:${this.storeName}:stateChanged`, (newState) => {
|
|
163
|
+
this.previousState = this.currentState;
|
|
164
|
+
this.currentState = newState;
|
|
165
|
+
this.notifyListeners();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
setState(partial, replace) {
|
|
170
|
+
let actualUpdate;
|
|
171
|
+
if (typeof partial === "function") {
|
|
172
|
+
if (this.currentState === null) {
|
|
173
|
+
console.warn("BridgeStore: Cannot execute function update, state not initialized");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
actualUpdate = partial(this.currentState);
|
|
177
|
+
} else {
|
|
178
|
+
actualUpdate = partial;
|
|
179
|
+
}
|
|
180
|
+
const payload = { partial: actualUpdate, replace };
|
|
181
|
+
this.bridge.send(`store:${this.storeName}:setState`, payload).catch((error) => {
|
|
182
|
+
console.error("Failed to update state via bridge:", error);
|
|
183
|
+
});
|
|
184
|
+
if (this.currentState) {
|
|
185
|
+
this.previousState = this.currentState;
|
|
186
|
+
if (replace) {
|
|
187
|
+
this.currentState = actualUpdate;
|
|
188
|
+
} else {
|
|
189
|
+
this.currentState = { ...this.currentState, ...actualUpdate };
|
|
190
|
+
}
|
|
191
|
+
this.notifyListeners();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function createBridgeStore(bridge, initialState, storeName = "default") {
|
|
196
|
+
return new BridgeStore(bridge, initialState, storeName);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
class StoreBuilder {
|
|
200
|
+
constructor(name = "default") {
|
|
201
|
+
this.config = {
|
|
202
|
+
name,
|
|
203
|
+
slices: []
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Add state slices to the store
|
|
208
|
+
*/
|
|
209
|
+
withSlices(...slices) {
|
|
210
|
+
this.config.slices = [...this.config.slices, ...slices];
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Attach a bridge for cross-context communication
|
|
215
|
+
*/
|
|
216
|
+
withBridge(bridge) {
|
|
217
|
+
this.config.bridge = bridge;
|
|
218
|
+
return this;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Create the store
|
|
222
|
+
*/
|
|
223
|
+
async create() {
|
|
224
|
+
if (this.config.slices.length === 0) {
|
|
225
|
+
throw new Error("Store must have at least one slice. Use withSlices() to add state.");
|
|
226
|
+
}
|
|
227
|
+
return await this.createBaseStore();
|
|
228
|
+
}
|
|
229
|
+
async createBaseStore() {
|
|
230
|
+
const bridge = this.config.bridge || globalThis.bridge;
|
|
231
|
+
if (bridge) {
|
|
232
|
+
createBridgeStore(bridge, void 0, this.config.name);
|
|
233
|
+
}
|
|
234
|
+
return this.createServiceWorkerStore();
|
|
235
|
+
}
|
|
236
|
+
createServiceWorkerStore() {
|
|
237
|
+
const creator = (set, get, store2) => {
|
|
238
|
+
let state = {};
|
|
239
|
+
for (const slice of this.config.slices) {
|
|
240
|
+
const sliceState = slice(set, get, store2);
|
|
241
|
+
state = { ...state, ...sliceState };
|
|
242
|
+
}
|
|
243
|
+
return state;
|
|
244
|
+
};
|
|
245
|
+
const persistOptions = { name: this.config.name };
|
|
246
|
+
const persistedCreator = chromeStoragePersist(persistOptions)(creator);
|
|
247
|
+
const store = vanilla.createStore(persistedCreator);
|
|
248
|
+
const centralStore = store;
|
|
249
|
+
return centralStore;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function createStore(name) {
|
|
253
|
+
return new StoreBuilder(name);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function useStoreActions(store) {
|
|
257
|
+
return React.useMemo(
|
|
258
|
+
() => ({
|
|
259
|
+
// Update state with partial data
|
|
260
|
+
update: (partial) => {
|
|
261
|
+
store.setState((state) => ({ ...state, ...partial }));
|
|
262
|
+
},
|
|
263
|
+
// Update state with a function
|
|
264
|
+
updateWith: (updater) => {
|
|
265
|
+
store.setState((state) => ({ ...state, ...updater(state) }));
|
|
266
|
+
},
|
|
267
|
+
// Replace entire state
|
|
268
|
+
replace: (newState) => {
|
|
269
|
+
store.setState(newState, true);
|
|
270
|
+
},
|
|
271
|
+
// Direct access to setState
|
|
272
|
+
setState: store.setState.bind(store)
|
|
273
|
+
}),
|
|
274
|
+
[store]
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
function createStoreHooks() {
|
|
278
|
+
const StoreContext = React.createContext(null);
|
|
279
|
+
function StoreProvider({ store, children }) {
|
|
280
|
+
const storeRef = React.useRef(store);
|
|
281
|
+
return React__namespace.createElement(StoreContext.Provider, { value: storeRef.current }, children);
|
|
282
|
+
}
|
|
283
|
+
function useStore(selector) {
|
|
284
|
+
const store = React.useContext(StoreContext);
|
|
285
|
+
if (!store) throw new Error("useStore must be used within a StoreProvider");
|
|
286
|
+
return useCentralStore(store, selector);
|
|
287
|
+
}
|
|
288
|
+
function useStoreInstance() {
|
|
289
|
+
const store = React.useContext(StoreContext);
|
|
290
|
+
if (!store) throw new Error("useStoreInstance must be used within a StoreProvider");
|
|
291
|
+
return store;
|
|
292
|
+
}
|
|
293
|
+
function useActions() {
|
|
294
|
+
const store = useStoreInstance();
|
|
295
|
+
return useStoreActions(store);
|
|
296
|
+
}
|
|
297
|
+
function createActionHook(actionsFactory) {
|
|
298
|
+
return function useCustomActions() {
|
|
299
|
+
const store = useStoreInstance();
|
|
300
|
+
const baseActions = useStoreActions(store);
|
|
301
|
+
return React.useMemo(() => actionsFactory(baseActions), [baseActions]);
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
StoreProvider,
|
|
306
|
+
useStore,
|
|
307
|
+
useStoreInstance,
|
|
308
|
+
useActions,
|
|
309
|
+
createActionHook
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function autoRegisterStoreHandlers(store) {
|
|
314
|
+
if (!store) {
|
|
315
|
+
throw new Error("autoRegisterStoreHandlers: store parameter is required");
|
|
316
|
+
}
|
|
317
|
+
class AutoGetStoreStateMessage {
|
|
318
|
+
handle() {
|
|
319
|
+
if (!store) {
|
|
320
|
+
throw new Error("Store instance not available");
|
|
321
|
+
}
|
|
322
|
+
return store.getState();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
class AutoSetStoreStateMessage {
|
|
326
|
+
handle(...args) {
|
|
327
|
+
if (!store) {
|
|
328
|
+
throw new Error("Store instance not available");
|
|
329
|
+
}
|
|
330
|
+
let partial;
|
|
331
|
+
let replace = false;
|
|
332
|
+
if (args.length === 1 && args[0] && typeof args[0] === "object") {
|
|
333
|
+
const payload = args[0];
|
|
334
|
+
if ("partial" in payload && "replace" in payload) {
|
|
335
|
+
({ partial, replace } = payload);
|
|
336
|
+
} else {
|
|
337
|
+
partial = payload;
|
|
338
|
+
replace = false;
|
|
339
|
+
}
|
|
340
|
+
} else if (args.length >= 2) {
|
|
341
|
+
partial = args[0];
|
|
342
|
+
replace = args[1] || false;
|
|
343
|
+
} else if (args.length === 1) {
|
|
344
|
+
partial = args[0];
|
|
345
|
+
replace = false;
|
|
346
|
+
} else {
|
|
347
|
+
return store.getState();
|
|
348
|
+
}
|
|
349
|
+
if (partial === void 0) {
|
|
350
|
+
return store.getState();
|
|
351
|
+
}
|
|
352
|
+
if (replace) {
|
|
353
|
+
store.setState(partial, true);
|
|
354
|
+
} else {
|
|
355
|
+
store.setState(partial);
|
|
356
|
+
}
|
|
357
|
+
return store.getState();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
class AutoSubscribeToStoreMessage {
|
|
361
|
+
handle() {
|
|
362
|
+
if (!store) {
|
|
363
|
+
throw new Error("Store instance not available");
|
|
364
|
+
}
|
|
365
|
+
store.subscribe((state, prevState) => {
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
GetStoreStateMessage: AutoGetStoreStateMessage,
|
|
371
|
+
SetStoreStateMessage: AutoSetStoreStateMessage,
|
|
372
|
+
SubscribeToStoreMessage: AutoSubscribeToStoreMessage
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function init(storeDefinition) {
|
|
377
|
+
try {
|
|
378
|
+
let builder = createStore(storeDefinition.name);
|
|
379
|
+
if (storeDefinition.slices) {
|
|
380
|
+
builder = builder.withSlices(...storeDefinition.slices);
|
|
381
|
+
}
|
|
382
|
+
const store = await builder.create();
|
|
383
|
+
return {
|
|
384
|
+
store,
|
|
385
|
+
classes: autoRegisterStoreHandlers(store)
|
|
386
|
+
};
|
|
387
|
+
} catch (error) {
|
|
388
|
+
console.error(`Failed to initialize store "${storeDefinition.name}":`, error);
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (typeof globalThis !== "undefined") {
|
|
393
|
+
globalThis.__CHROMA__ = globalThis.__CHROMA__ || {};
|
|
394
|
+
globalThis.__CHROMA__.initStores = init;
|
|
395
|
+
globalThis.initStores = init;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
exports.BridgeStore = BridgeStore;
|
|
399
|
+
exports.StoreBuilder = StoreBuilder;
|
|
400
|
+
exports.chromeStoragePersist = chromeStoragePersist;
|
|
401
|
+
exports.createBridgeStore = createBridgeStore;
|
|
402
|
+
exports.createStore = createStore;
|
|
403
|
+
exports.createStoreHooks = createStoreHooks;
|
|
404
|
+
exports.useCentralDispatch = useCentralDispatch;
|
|
405
|
+
exports.useCentralStore = useCentralStore;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { StoreApi, StateCreator } from 'zustand';
|
|
2
|
+
import { StateCreator as StateCreator$1 } from 'zustand/vanilla';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
type PersistOptions = {
|
|
7
|
+
name: string;
|
|
8
|
+
version?: number;
|
|
9
|
+
migrate?: (state: any, version: number) => any;
|
|
10
|
+
};
|
|
11
|
+
interface StoreDefinition {
|
|
12
|
+
name: string;
|
|
13
|
+
slices?: StateCreator<any, [], [], any>[];
|
|
14
|
+
persistence?: PersistOptions;
|
|
15
|
+
config?: Record<string, any>;
|
|
16
|
+
}
|
|
17
|
+
type ExtractSliceState<T> = T extends StateCreator<infer State, any, any, any> ? State : never;
|
|
18
|
+
type SliceCreator<T> = StateCreator<T, [], [], T>;
|
|
19
|
+
interface StoreConfig<T> {
|
|
20
|
+
slices: readonly StateCreator<any, [], [], any>[];
|
|
21
|
+
persist?: PersistOptions;
|
|
22
|
+
}
|
|
23
|
+
type MergeSlices<Slices extends readonly StateCreator<any, [], [], any>[]> = Slices extends readonly [infer First, ...infer Rest] ? First extends StateCreator<any, [], [], infer FirstState> ? Rest extends readonly StateCreator<any, [], [], any>[] ? FirstState & MergeSlices<Rest> : FirstState : {} : {};
|
|
24
|
+
interface CentralStore<T> extends StoreApi<T> {
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare function chromeStoragePersist<S>(options: PersistOptions): (config: StateCreator<S>) => StateCreator<S>;
|
|
28
|
+
|
|
29
|
+
declare function useCentralStore<T, U = T>(store: CentralStore<T>, selector: (state: T) => U): U;
|
|
30
|
+
declare function useCentralDispatch<T>(store: CentralStore<T>): {
|
|
31
|
+
(partial: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: false): void;
|
|
32
|
+
(state: T | ((state: T) => T), replace: true): void;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
interface Bridge {
|
|
36
|
+
send: <Req = unknown, Res = unknown>(key: string, payload?: Req) => Promise<Res>;
|
|
37
|
+
isConnected: boolean;
|
|
38
|
+
}
|
|
39
|
+
interface BridgeWithEvents extends Bridge {
|
|
40
|
+
on?: (key: string, handler: (payload: any) => void) => void;
|
|
41
|
+
}
|
|
42
|
+
interface BridgeWithHandlers extends Bridge {
|
|
43
|
+
register: (key: string, handler: (payload?: any) => any) => void;
|
|
44
|
+
broadcast: (key: string, payload: any) => void;
|
|
45
|
+
on?: (key: string, handler: (payload: any) => void) => void;
|
|
46
|
+
}
|
|
47
|
+
declare class BridgeStore<T> implements CentralStore<T> {
|
|
48
|
+
private bridge;
|
|
49
|
+
private listeners;
|
|
50
|
+
private currentState;
|
|
51
|
+
private previousState;
|
|
52
|
+
private storeName;
|
|
53
|
+
private ready;
|
|
54
|
+
constructor(bridge: BridgeWithEvents, initialState?: T, storeName?: string);
|
|
55
|
+
initialize: () => Promise<void>;
|
|
56
|
+
private setupStateSync;
|
|
57
|
+
private notifyListeners;
|
|
58
|
+
getState: () => T;
|
|
59
|
+
setState(partial: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: false): void;
|
|
60
|
+
setState(state: T | ((state: T) => T), replace: true): void;
|
|
61
|
+
subscribe: (listener: (state: T, prevState: T) => void) => (() => void);
|
|
62
|
+
destroy: () => void;
|
|
63
|
+
getInitialState: () => T;
|
|
64
|
+
}
|
|
65
|
+
declare function createBridgeStore<T>(bridge: BridgeWithEvents, initialState?: T, storeName?: string): CentralStore<T>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Core store builder with fluent API
|
|
69
|
+
*/
|
|
70
|
+
declare class StoreBuilder<T = any> {
|
|
71
|
+
private config;
|
|
72
|
+
constructor(name?: string);
|
|
73
|
+
/**
|
|
74
|
+
* Add state slices to the store
|
|
75
|
+
*/
|
|
76
|
+
withSlices(...slices: StateCreator$1<any, [], [], any>[]): this;
|
|
77
|
+
/**
|
|
78
|
+
* Attach a bridge for cross-context communication
|
|
79
|
+
*/
|
|
80
|
+
withBridge(bridge?: BridgeWithEvents): this;
|
|
81
|
+
/**
|
|
82
|
+
* Create the store
|
|
83
|
+
*/
|
|
84
|
+
create(): Promise<CentralStore<T>>;
|
|
85
|
+
private createBaseStore;
|
|
86
|
+
private createServiceWorkerStore;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create a new store builder
|
|
90
|
+
*/
|
|
91
|
+
declare function createStore<T = any>(name?: string): StoreBuilder<T>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Store actions helper (inlined from actions.ts)
|
|
95
|
+
*/
|
|
96
|
+
declare function useStoreActions<T extends Record<string, any>>(store: CentralStore<T>): {
|
|
97
|
+
update: (partial: Partial<T>) => void;
|
|
98
|
+
updateWith: (updater: (state: T) => Partial<T>) => void;
|
|
99
|
+
replace: (newState: T) => void;
|
|
100
|
+
setState: {
|
|
101
|
+
(partial: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: false): void;
|
|
102
|
+
(state: T | ((state: T) => T), replace: true): void;
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Create a complete store hook factory with typed providers and action creators
|
|
107
|
+
* This prevents confusion by providing only the hooks you need, not global ones
|
|
108
|
+
*/
|
|
109
|
+
declare function createStoreHooks<T extends Record<string, any>>(): {
|
|
110
|
+
StoreProvider: ({ store, children }: {
|
|
111
|
+
store: CentralStore<T>;
|
|
112
|
+
children: ReactNode;
|
|
113
|
+
}) => React.FunctionComponentElement<React.ProviderProps<CentralStore<T> | null>>;
|
|
114
|
+
useStore: <U>(selector: (state: T) => U) => U;
|
|
115
|
+
useStoreInstance: () => CentralStore<T>;
|
|
116
|
+
useActions: () => {
|
|
117
|
+
update: (partial: Partial<T>) => void;
|
|
118
|
+
updateWith: (updater: (state: T) => Partial<T>) => void;
|
|
119
|
+
replace: (newState: T) => void;
|
|
120
|
+
setState: {
|
|
121
|
+
(partial: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: false): void;
|
|
122
|
+
(state: T | ((state: T) => T), replace: true): void;
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
createActionHook: <ActionMap extends Record<string, (...args: any[]) => void>>(actionsFactory: (actions: ReturnType<typeof useStoreActions<T>>) => ActionMap) => () => ActionMap;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export { BridgeStore, StoreBuilder, chromeStoragePersist, createBridgeStore, createStore, createStoreHooks, useCentralDispatch, useCentralStore };
|
|
129
|
+
export type { Bridge, BridgeWithEvents, BridgeWithHandlers, CentralStore, ExtractSliceState, MergeSlices, PersistOptions, SliceCreator, StoreConfig, StoreDefinition };
|
package/dist/index.es.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useSyncExternalStore, createContext, useMemo, useContext, useRef } from 'react';
|
|
3
|
+
import { createStore as createStore$1 } from 'zustand/vanilla';
|
|
4
|
+
|
|
5
|
+
function chromeStoragePersist(options) {
|
|
6
|
+
return (config) => (set, get, store) => {
|
|
7
|
+
const key = options.name;
|
|
8
|
+
let isInitialized = false;
|
|
9
|
+
let persistenceSetup = false;
|
|
10
|
+
const initialState = config(set, get, store);
|
|
11
|
+
const loadPersistedState = async () => {
|
|
12
|
+
try {
|
|
13
|
+
if (!chrome?.storage?.local) {
|
|
14
|
+
console.warn(`Chrome storage not available for "${key}", using memory only`);
|
|
15
|
+
isInitialized = true;
|
|
16
|
+
setupPersistence();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const result = await new Promise((resolve, reject) => {
|
|
20
|
+
chrome.storage.local.get([key], (result2) => {
|
|
21
|
+
if (chrome.runtime.lastError) {
|
|
22
|
+
reject(chrome.runtime.lastError);
|
|
23
|
+
} else {
|
|
24
|
+
resolve(result2);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
if (result[key]) {
|
|
29
|
+
const mergedState = { ...initialState, ...result[key] };
|
|
30
|
+
set(mergedState);
|
|
31
|
+
} else {
|
|
32
|
+
await persistState(initialState);
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error(`Failed to load persisted state for "${key}":`, error);
|
|
36
|
+
} finally {
|
|
37
|
+
isInitialized = true;
|
|
38
|
+
setupPersistence();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const persistState = async (state) => {
|
|
42
|
+
if (!chrome?.storage?.local) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
chrome.storage.local.set({ [key]: state }, () => {
|
|
47
|
+
if (chrome.runtime.lastError) {
|
|
48
|
+
console.error(`Failed to persist state for "${key}":`, chrome.runtime.lastError);
|
|
49
|
+
}
|
|
50
|
+
resolve();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
const setupPersistence = () => {
|
|
55
|
+
if (persistenceSetup) return;
|
|
56
|
+
persistenceSetup = true;
|
|
57
|
+
store.subscribe((state) => {
|
|
58
|
+
if (!isInitialized) return;
|
|
59
|
+
persistState(state);
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
loadPersistedState();
|
|
63
|
+
return initialState;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function useCentralStore(store, selector) {
|
|
68
|
+
return useSyncExternalStore(
|
|
69
|
+
store.subscribe,
|
|
70
|
+
() => selector(store.getState()),
|
|
71
|
+
() => selector(store.getState())
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
function useCentralDispatch(store) {
|
|
75
|
+
return store.setState;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class BridgeStore {
|
|
79
|
+
constructor(bridge, initialState, storeName = "default") {
|
|
80
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
81
|
+
this.currentState = null;
|
|
82
|
+
this.previousState = null;
|
|
83
|
+
this.ready = false;
|
|
84
|
+
this.initialize = async () => {
|
|
85
|
+
try {
|
|
86
|
+
console.log("Initializing bridge store:", this.storeName, this.bridge);
|
|
87
|
+
const state = await this.bridge.send(`store:${this.storeName}:getState`);
|
|
88
|
+
this.previousState = this.currentState;
|
|
89
|
+
this.currentState = state;
|
|
90
|
+
this.notifyListeners();
|
|
91
|
+
this.ready = true;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.warn("Failed to initialize bridge store:", error);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
this.notifyListeners = () => {
|
|
97
|
+
if (!this.listeners) {
|
|
98
|
+
console.warn("BridgeStore: listeners not initialized");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (this.currentState && this.previousState) {
|
|
102
|
+
this.listeners.forEach((listener) => listener(this.currentState, this.previousState));
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
this.getState = () => {
|
|
106
|
+
return this.currentState;
|
|
107
|
+
};
|
|
108
|
+
this.subscribe = (listener) => {
|
|
109
|
+
if (!this.listeners) {
|
|
110
|
+
console.error("BridgeStore: Cannot subscribe, listeners not initialized");
|
|
111
|
+
return () => {
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
this.listeners.add(listener);
|
|
115
|
+
if (this.currentState && this.previousState) {
|
|
116
|
+
listener(this.currentState, this.previousState);
|
|
117
|
+
}
|
|
118
|
+
return () => {
|
|
119
|
+
if (this.listeners) {
|
|
120
|
+
this.listeners.delete(listener);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
// Additional StoreApi methods
|
|
125
|
+
this.destroy = () => {
|
|
126
|
+
if (this.listeners) {
|
|
127
|
+
this.listeners.clear();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
this.getInitialState = () => {
|
|
131
|
+
return this.getState();
|
|
132
|
+
};
|
|
133
|
+
this.bridge = bridge;
|
|
134
|
+
this.currentState = initialState || null;
|
|
135
|
+
this.previousState = initialState || null;
|
|
136
|
+
this.storeName = storeName;
|
|
137
|
+
this.setupStateSync();
|
|
138
|
+
this.initialize();
|
|
139
|
+
}
|
|
140
|
+
setupStateSync() {
|
|
141
|
+
if (this.bridge.on) {
|
|
142
|
+
this.bridge.on(`store:${this.storeName}:stateChanged`, (newState) => {
|
|
143
|
+
this.previousState = this.currentState;
|
|
144
|
+
this.currentState = newState;
|
|
145
|
+
this.notifyListeners();
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
setState(partial, replace) {
|
|
150
|
+
let actualUpdate;
|
|
151
|
+
if (typeof partial === "function") {
|
|
152
|
+
if (this.currentState === null) {
|
|
153
|
+
console.warn("BridgeStore: Cannot execute function update, state not initialized");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
actualUpdate = partial(this.currentState);
|
|
157
|
+
} else {
|
|
158
|
+
actualUpdate = partial;
|
|
159
|
+
}
|
|
160
|
+
const payload = { partial: actualUpdate, replace };
|
|
161
|
+
this.bridge.send(`store:${this.storeName}:setState`, payload).catch((error) => {
|
|
162
|
+
console.error("Failed to update state via bridge:", error);
|
|
163
|
+
});
|
|
164
|
+
if (this.currentState) {
|
|
165
|
+
this.previousState = this.currentState;
|
|
166
|
+
if (replace) {
|
|
167
|
+
this.currentState = actualUpdate;
|
|
168
|
+
} else {
|
|
169
|
+
this.currentState = { ...this.currentState, ...actualUpdate };
|
|
170
|
+
}
|
|
171
|
+
this.notifyListeners();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function createBridgeStore(bridge, initialState, storeName = "default") {
|
|
176
|
+
return new BridgeStore(bridge, initialState, storeName);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
class StoreBuilder {
|
|
180
|
+
constructor(name = "default") {
|
|
181
|
+
this.config = {
|
|
182
|
+
name,
|
|
183
|
+
slices: []
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Add state slices to the store
|
|
188
|
+
*/
|
|
189
|
+
withSlices(...slices) {
|
|
190
|
+
this.config.slices = [...this.config.slices, ...slices];
|
|
191
|
+
return this;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Attach a bridge for cross-context communication
|
|
195
|
+
*/
|
|
196
|
+
withBridge(bridge) {
|
|
197
|
+
this.config.bridge = bridge;
|
|
198
|
+
return this;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Create the store
|
|
202
|
+
*/
|
|
203
|
+
async create() {
|
|
204
|
+
if (this.config.slices.length === 0) {
|
|
205
|
+
throw new Error("Store must have at least one slice. Use withSlices() to add state.");
|
|
206
|
+
}
|
|
207
|
+
return await this.createBaseStore();
|
|
208
|
+
}
|
|
209
|
+
async createBaseStore() {
|
|
210
|
+
const bridge = this.config.bridge || globalThis.bridge;
|
|
211
|
+
if (bridge) {
|
|
212
|
+
createBridgeStore(bridge, void 0, this.config.name);
|
|
213
|
+
}
|
|
214
|
+
return this.createServiceWorkerStore();
|
|
215
|
+
}
|
|
216
|
+
createServiceWorkerStore() {
|
|
217
|
+
const creator = (set, get, store2) => {
|
|
218
|
+
let state = {};
|
|
219
|
+
for (const slice of this.config.slices) {
|
|
220
|
+
const sliceState = slice(set, get, store2);
|
|
221
|
+
state = { ...state, ...sliceState };
|
|
222
|
+
}
|
|
223
|
+
return state;
|
|
224
|
+
};
|
|
225
|
+
const persistOptions = { name: this.config.name };
|
|
226
|
+
const persistedCreator = chromeStoragePersist(persistOptions)(creator);
|
|
227
|
+
const store = createStore$1(persistedCreator);
|
|
228
|
+
const centralStore = store;
|
|
229
|
+
return centralStore;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function createStore(name) {
|
|
233
|
+
return new StoreBuilder(name);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function useStoreActions(store) {
|
|
237
|
+
return useMemo(
|
|
238
|
+
() => ({
|
|
239
|
+
// Update state with partial data
|
|
240
|
+
update: (partial) => {
|
|
241
|
+
store.setState((state) => ({ ...state, ...partial }));
|
|
242
|
+
},
|
|
243
|
+
// Update state with a function
|
|
244
|
+
updateWith: (updater) => {
|
|
245
|
+
store.setState((state) => ({ ...state, ...updater(state) }));
|
|
246
|
+
},
|
|
247
|
+
// Replace entire state
|
|
248
|
+
replace: (newState) => {
|
|
249
|
+
store.setState(newState, true);
|
|
250
|
+
},
|
|
251
|
+
// Direct access to setState
|
|
252
|
+
setState: store.setState.bind(store)
|
|
253
|
+
}),
|
|
254
|
+
[store]
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
function createStoreHooks() {
|
|
258
|
+
const StoreContext = createContext(null);
|
|
259
|
+
function StoreProvider({ store, children }) {
|
|
260
|
+
const storeRef = useRef(store);
|
|
261
|
+
return React.createElement(StoreContext.Provider, { value: storeRef.current }, children);
|
|
262
|
+
}
|
|
263
|
+
function useStore(selector) {
|
|
264
|
+
const store = useContext(StoreContext);
|
|
265
|
+
if (!store) throw new Error("useStore must be used within a StoreProvider");
|
|
266
|
+
return useCentralStore(store, selector);
|
|
267
|
+
}
|
|
268
|
+
function useStoreInstance() {
|
|
269
|
+
const store = useContext(StoreContext);
|
|
270
|
+
if (!store) throw new Error("useStoreInstance must be used within a StoreProvider");
|
|
271
|
+
return store;
|
|
272
|
+
}
|
|
273
|
+
function useActions() {
|
|
274
|
+
const store = useStoreInstance();
|
|
275
|
+
return useStoreActions(store);
|
|
276
|
+
}
|
|
277
|
+
function createActionHook(actionsFactory) {
|
|
278
|
+
return function useCustomActions() {
|
|
279
|
+
const store = useStoreInstance();
|
|
280
|
+
const baseActions = useStoreActions(store);
|
|
281
|
+
return useMemo(() => actionsFactory(baseActions), [baseActions]);
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
StoreProvider,
|
|
286
|
+
useStore,
|
|
287
|
+
useStoreInstance,
|
|
288
|
+
useActions,
|
|
289
|
+
createActionHook
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function autoRegisterStoreHandlers(store) {
|
|
294
|
+
if (!store) {
|
|
295
|
+
throw new Error("autoRegisterStoreHandlers: store parameter is required");
|
|
296
|
+
}
|
|
297
|
+
class AutoGetStoreStateMessage {
|
|
298
|
+
handle() {
|
|
299
|
+
if (!store) {
|
|
300
|
+
throw new Error("Store instance not available");
|
|
301
|
+
}
|
|
302
|
+
return store.getState();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
class AutoSetStoreStateMessage {
|
|
306
|
+
handle(...args) {
|
|
307
|
+
if (!store) {
|
|
308
|
+
throw new Error("Store instance not available");
|
|
309
|
+
}
|
|
310
|
+
let partial;
|
|
311
|
+
let replace = false;
|
|
312
|
+
if (args.length === 1 && args[0] && typeof args[0] === "object") {
|
|
313
|
+
const payload = args[0];
|
|
314
|
+
if ("partial" in payload && "replace" in payload) {
|
|
315
|
+
({ partial, replace } = payload);
|
|
316
|
+
} else {
|
|
317
|
+
partial = payload;
|
|
318
|
+
replace = false;
|
|
319
|
+
}
|
|
320
|
+
} else if (args.length >= 2) {
|
|
321
|
+
partial = args[0];
|
|
322
|
+
replace = args[1] || false;
|
|
323
|
+
} else if (args.length === 1) {
|
|
324
|
+
partial = args[0];
|
|
325
|
+
replace = false;
|
|
326
|
+
} else {
|
|
327
|
+
return store.getState();
|
|
328
|
+
}
|
|
329
|
+
if (partial === void 0) {
|
|
330
|
+
return store.getState();
|
|
331
|
+
}
|
|
332
|
+
if (replace) {
|
|
333
|
+
store.setState(partial, true);
|
|
334
|
+
} else {
|
|
335
|
+
store.setState(partial);
|
|
336
|
+
}
|
|
337
|
+
return store.getState();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
class AutoSubscribeToStoreMessage {
|
|
341
|
+
handle() {
|
|
342
|
+
if (!store) {
|
|
343
|
+
throw new Error("Store instance not available");
|
|
344
|
+
}
|
|
345
|
+
store.subscribe((state, prevState) => {
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
GetStoreStateMessage: AutoGetStoreStateMessage,
|
|
351
|
+
SetStoreStateMessage: AutoSetStoreStateMessage,
|
|
352
|
+
SubscribeToStoreMessage: AutoSubscribeToStoreMessage
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function init(storeDefinition) {
|
|
357
|
+
try {
|
|
358
|
+
let builder = createStore(storeDefinition.name);
|
|
359
|
+
if (storeDefinition.slices) {
|
|
360
|
+
builder = builder.withSlices(...storeDefinition.slices);
|
|
361
|
+
}
|
|
362
|
+
const store = await builder.create();
|
|
363
|
+
return {
|
|
364
|
+
store,
|
|
365
|
+
classes: autoRegisterStoreHandlers(store)
|
|
366
|
+
};
|
|
367
|
+
} catch (error) {
|
|
368
|
+
console.error(`Failed to initialize store "${storeDefinition.name}":`, error);
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (typeof globalThis !== "undefined") {
|
|
373
|
+
globalThis.__CHROMA__ = globalThis.__CHROMA__ || {};
|
|
374
|
+
globalThis.__CHROMA__.initStores = init;
|
|
375
|
+
globalThis.initStores = init;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export { BridgeStore, StoreBuilder, chromeStoragePersist, createBridgeStore, createStore, createStoreHooks, useCentralDispatch, useCentralStore };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chromahq/store",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Centralized, persistent store for Chrome extensions using zustand, accessible from service workers and React, with chrome.storage.local persistence.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs.js",
|