@chromahq/store 0.0.2

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.
Files changed (2) hide show
  1. package/README.md +557 -0
  2. package/package.json +64 -0
package/README.md ADDED
@@ -0,0 +1,557 @@
1
+ # @chromahq/store
2
+
3
+ > 🏪 **Simple, powerful state management** for Chrome extensions with automatic synchronization between service worker and UI contexts.
4
+
5
+ ## ✨ Features
6
+
7
+ - **🔄 Auto-Sync**: Service worker ↔ Popup ↔ Content scripts
8
+ - **💾 Auto-Persist**: All stores automatically persist to Chrome storage
9
+ - **🎯 Zero Config**: Smart context detection - no complex setup
10
+ - **🎨 Modern API**: Clean, fluent builder pattern
11
+ - **🔒 Type Safe**: Full TypeScript support with excellent DX
12
+ - **⚡ Fast**: Optimistic updates with background sync
13
+
14
+ ## 🚀 Quick Setup
15
+
16
+ ### Install
17
+
18
+ ```bash
19
+ npm install @chromahq/store @chromahq/core
20
+ ```
21
+
22
+ ## � Usage
23
+
24
+ ### 1. Define Your Slices
25
+
26
+ ```typescript
27
+ // src/slices/counter.ts
28
+ import type { StateCreator } from 'zustand';
29
+
30
+ export interface CounterSlice {
31
+ count: number;
32
+ increment: () => void;
33
+ decrement: () => void;
34
+ reset: () => void;
35
+ }
36
+
37
+ export const counterSlice: StateCreator<CounterSlice> = (set) => ({
38
+ count: 0,
39
+ increment: () => set((state) => ({ count: state.count + 1 })),
40
+ decrement: () => set((state) => ({ count: state.count - 1 })),
41
+ reset: () => set({ count: 0 }),
42
+ });
43
+
44
+ // Export combined type for TypeScript
45
+ export type RootState = CounterSlice; // Add more slices with &
46
+ ```
47
+
48
+ ### 2. Service Worker Setup
49
+
50
+ ```typescript
51
+ // src/service-worker.ts
52
+ import '@abraham/reflection';
53
+ import { StoreDefinition } from '@chromahq/store';
54
+ import { bootstrap } from '@chromahq/core';
55
+ import { counterSlice } from './slices/counter';
56
+
57
+ const store: StoreDefinition = {
58
+ name: 'app',
59
+ slices: [counterSlice],
60
+ // Persistence is automatic - no config needed!
61
+ };
62
+
63
+ bootstrap().withStore(store).create();
64
+ ```
65
+
66
+ ### 3. React UI Setup
67
+
68
+ ```typescript
69
+ // src/hooks/useAppStore.ts
70
+ import { RootState } from '../slices/counter';
71
+ import { useBridge } from '@chromahq/react';
72
+ import { CentralStore, createStore } from '@chromahq/store';
73
+ import { useEffect, useState } from 'react';
74
+
75
+ export function useAppStore() {
76
+ const { bridge } = useBridge();
77
+ const [store, setStore] = useState<CentralStore<RootState>>();
78
+ const [loading, setLoading] = useState(true);
79
+
80
+ useEffect(() => {
81
+ async function initStore() {
82
+ if (!bridge) return;
83
+
84
+ const store = await createStore<RootState>('app')
85
+ .withSlices(counterSlice)
86
+ .withBridge(bridge)
87
+ .create();
88
+
89
+ setStore(store);
90
+ setLoading(false);
91
+ }
92
+
93
+ initStore();
94
+ }, [bridge]);
95
+
96
+ return { store, loading };
97
+ }
98
+ ```
99
+
100
+ ```typescript
101
+ // src/components/Counter.tsx
102
+ import { useCentralStore } from '@chromahq/store';
103
+ import { useAppStore } from '../hooks/useAppStore';
104
+
105
+ export function Counter() {
106
+ const { store, loading } = useAppStore();
107
+
108
+ // Use the store with a selector
109
+ const count = useCentralStore(store!, (state) => state.count);
110
+ const increment = useCentralStore(store!, (state) => state.increment);
111
+ const decrement = useCentralStore(store!, (state) => state.decrement);
112
+
113
+ if (loading) return <div>Loading...</div>;
114
+
115
+ return (
116
+ <div>
117
+ <h2>Count: {count}</h2>
118
+ <button onClick={increment}>+</button>
119
+ <button onClick={decrement}>-</button>
120
+ </div>
121
+ );
122
+ }
123
+ ```
124
+
125
+ ## 🏗 Architecture
126
+
127
+ ### Service Worker (Source of Truth)
128
+
129
+ - **ServiceWorkerStore**: Real Zustand store with automatic Chrome storage persistence
130
+ - **Message Handlers**: Auto-registered for cross-context communication
131
+ - **State Authority**: The single source of truth for all application state
132
+
133
+ ### UI Contexts (React/Popup/Content Scripts)
134
+
135
+ - **BridgeStore**: Lightweight proxy that connects to service worker via bridge
136
+ - **Reactive Updates**: Automatically syncs with service worker state changes
137
+ - **Optimistic Updates**: Immediate UI feedback with background synchronization
138
+
139
+ ## 🎯 Key Benefits
140
+
141
+ ### Simple & Clean
142
+
143
+ - **No plugin system complexity** - just slices and bridges
144
+ - **Automatic persistence** - every store persists without configuration
145
+ - **Smart context detection** - creates the right store type automatically
146
+
147
+ ### Developer Experience
148
+
149
+ - **TypeScript first** - excellent type inference and safety
150
+ - **Familiar API** - built on Zustand, same patterns you know
151
+ - **Zero boilerplate** - minimal setup, maximum functionality
152
+
153
+ ### Performance
154
+
155
+ - **Optimistic updates** - UI responds immediately
156
+ - **Background sync** - service worker handles persistence
157
+ - **Efficient bridge** - only sends serializable data, executes functions locally
158
+
159
+ ## 📚 API Reference
160
+
161
+ ### Core Functions
162
+
163
+ ```typescript
164
+ // Create a store builder
165
+ createStore<T>(name?: string): StoreBuilder<T>
166
+
167
+ // StoreBuilder methods
168
+ .withSlices(...slices): StoreBuilder<T> // Add state slices
169
+ .withBridge(bridge): StoreBuilder<T> // Connect to service worker (UI only)
170
+ .create(): Promise<CentralStore<T>> // Create the store
171
+
172
+ // React hooks
173
+ useCentralStore<T, U>(store, selector): U // Subscribe to state
174
+ useCentralDispatch<T>(store): SetState<T> // Get state updater
175
+ ```
176
+
177
+ ### Types
178
+
179
+ ```typescript
180
+ // Store definition for service worker bootstrap
181
+ interface StoreDefinition {
182
+ name: string;
183
+ slices: StateCreator<any, [], [], any>[];
184
+ }
185
+
186
+ // Central store interface (both ServiceWorkerStore and BridgeStore)
187
+ interface CentralStore<T> {
188
+ getState(): T;
189
+ setState(partial: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: boolean): void;
190
+ subscribe(listener: (state: T, prevState: T) => void): () => void;
191
+ ready: Promise<void>;
192
+ }
193
+ ```
194
+
195
+ ## 🔧 Advanced Usage
196
+
197
+ ### Multiple Stores
198
+
199
+ ```typescript
200
+ // Service worker
201
+ const userStore: StoreDefinition = { name: 'user', slices: [userSlice] };
202
+ const settingsStore: StoreDefinition = { name: 'settings', slices: [settingsSlice] };
203
+
204
+ bootstrap().withStore(userStore).withStore(settingsStore).create();
205
+
206
+ // React
207
+ const userStore = await createStore<UserState>('user')
208
+ .withSlices(userSlice)
209
+ .withBridge(bridge)
210
+ .create();
211
+ const settingsStore = await createStore<SettingsState>('settings')
212
+ .withSlices(settingsSlice)
213
+ .withBridge(bridge)
214
+ .create();
215
+ ```
216
+
217
+ ### Context Providers
218
+
219
+ ```typescript
220
+ // Create typed hooks with context
221
+ import { createStoreHooks } from '@chromahq/store';
222
+
223
+ const { StoreProvider, useStore } = createStoreHooks<RootState>();
224
+
225
+ function App() {
226
+ const { store } = useAppStore();
227
+
228
+ return (
229
+ <StoreProvider store={store}>
230
+ <MyComponent />
231
+ </StoreProvider>
232
+ );
233
+ }
234
+
235
+ function MyComponent() {
236
+ // No need to pass store around - uses context
237
+ const count = useStore(state => state.count);
238
+ const increment = useStore(state => state.increment);
239
+
240
+ return <button onClick={increment}>{count}</button>;
241
+ }
242
+ ```
243
+
244
+ ---
245
+
246
+ **🎉 That's it!** You now have powerful, type-safe state management across your entire Chrome extension with automatic persistence and synchronization.
247
+ return (
248
+ <BridgeProvider>
249
+ <AppContent />
250
+ </BridgeProvider>
251
+ );
252
+ }
253
+
254
+ ````
255
+
256
+ ### 3. Use in Components
257
+
258
+ ```typescript
259
+ // src/popup/components/Counter.tsx
260
+ import React from 'react';
261
+ import { useStore } from '../../hooks/useAppStore';
262
+
263
+ export function Counter() {
264
+ // Select specific state (automatically typed!)
265
+ const count = useStore(state => state.count);
266
+ const { increment, decrement, reset } = useStore(state => ({
267
+ increment: state.increment,
268
+ decrement: state.decrement,
269
+ reset: state.reset
270
+ }));
271
+
272
+ return (
273
+ <div className="counter">
274
+ <h2>Counter: {count}</h2>
275
+ <div className="buttons">
276
+ <button onClick={increment}>+1</button>
277
+ <button onClick={decrement}>-1</button>
278
+ <button onClick={reset}>Reset</button>
279
+ </div>
280
+ </div>
281
+ );
282
+ }
283
+ ````
284
+
285
+ ```typescript
286
+ // src/popup/components/UserProfile.tsx
287
+ import React from 'react';
288
+ import { useStore } from '../../hooks/useAppStore';
289
+
290
+ export function UserProfile() {
291
+ const { user, isAuthenticated } = useStore(state => ({
292
+ user: state.user,
293
+ isAuthenticated: state.isAuthenticated
294
+ }));
295
+ const { login, logout } = useStore(state => ({
296
+ login: state.login,
297
+ logout: state.logout
298
+ }));
299
+
300
+ const handleLogin = () => {
301
+ login({
302
+ name: 'John Doe',
303
+ email: 'john@example.com',
304
+ id: '12345'
305
+ });
306
+ };
307
+
308
+ if (!isAuthenticated) {
309
+ return (
310
+ <div className="login">
311
+ <h3>Please log in</h3>
312
+ <button onClick={handleLogin}>Login</button>
313
+ </div>
314
+ );
315
+ }
316
+
317
+ return (
318
+ <div className="profile">
319
+ <h3>Welcome, {user.name}!</h3>
320
+ <p>Email: {user.email}</p>
321
+ <button onClick={logout}>Logout</button>
322
+ </div>
323
+ );
324
+ }
325
+ ```
326
+
327
+ ---
328
+
329
+ ## 🔄 How Sync Works
330
+
331
+ ### Automatic Synchronization
332
+
333
+ ```typescript
334
+ // Any change in service worker...
335
+ store.getState().increment(); // count: 0 → 1
336
+
337
+ // ...automatically appears in React components!
338
+ // No manual sync code needed ✨
339
+ ```
340
+
341
+ ### The Magic Behind The Scenes
342
+
343
+ 1. **Service Worker** creates real store with persistence
344
+ 2. **React Components** create bridge store with same name
345
+ 3. **@chromahq/core** bridge automatically syncs all changes
346
+ 4. **State updates** flow instantly between contexts
347
+ 5. **Persistence** ensures state survives browser restarts
348
+
349
+ ---
350
+
351
+ ## 🛠️ Advanced Usage
352
+
353
+ ### Custom Plugins
354
+
355
+ ```typescript
356
+ // src/app/stores/plugins.ts
357
+ import type { StorePlugin } from '@chromahq/store';
358
+
359
+ // Analytics plugin
360
+ export const analyticsPlugin: StorePlugin = {
361
+ name: 'analytics',
362
+ priority: 50,
363
+ async setup(store, config) {
364
+ store.subscribe((state, prevState) => {
365
+ // Track state changes
366
+ chrome.runtime.sendMessage({
367
+ type: 'ANALYTICS_EVENT',
368
+ data: { store: config.name, state },
369
+ });
370
+ });
371
+ },
372
+ };
373
+
374
+ // Logging plugin
375
+ export const loggingPlugin: StorePlugin = {
376
+ name: 'logging',
377
+ priority: 10, // Higher priority = runs first
378
+ async setup(store, config) {
379
+ console.log(`🏪 Store "${config.name}" initialized`);
380
+
381
+ store.subscribe((state, prevState) => {
382
+ console.log('State change:', { from: prevState, to: state });
383
+ });
384
+ },
385
+ };
386
+ ```
387
+
388
+ ### Enhanced Store Definition
389
+
390
+ ```typescript
391
+ // src/app/stores/app.store.ts
392
+ import { counterSlice, userSlice } from '../slices';
393
+ import { analyticsPlugin, loggingPlugin } from './plugins';
394
+ import type { StoreDefinition } from '@chromahq/store';
395
+
396
+ const store: StoreDefinition = {
397
+ name: 'app',
398
+ slices: [counterSlice, userSlice],
399
+ persistence: {
400
+ name: 'my-extension-state',
401
+ version: 2,
402
+ migrate: (state, version) => {
403
+ // Handle state migrations
404
+ if (version < 2) {
405
+ return { ...state, newField: 'default' };
406
+ }
407
+ return state;
408
+ },
409
+ },
410
+ plugins: [loggingPlugin, analyticsPlugin],
411
+ config: {
412
+ apiUrl: 'https://api.myservice.com',
413
+ debugMode: true,
414
+ },
415
+ };
416
+
417
+ export default store;
418
+ ```
419
+
420
+ ### Multiple Stores
421
+
422
+ ```typescript
423
+ // src/app/stores/user.store.ts
424
+ import { userSlice, authSlice } from '../slices';
425
+
426
+ export default {
427
+ name: 'user',
428
+ slices: [userSlice, authSlice],
429
+ persistence: { name: 'user-data' },
430
+ };
431
+
432
+ // src/app/stores/settings.store.ts
433
+ import { settingsSlice, themeSlice } from '../slices';
434
+
435
+ export default {
436
+ name: 'settings',
437
+ slices: [settingsSlice, themeSlice],
438
+ persistence: { name: 'user-settings' },
439
+ };
440
+
441
+ // src/app/stores/cache.store.ts
442
+ import { cacheSlice } from '../slices';
443
+ import { ttlPlugin } from './plugins';
444
+
445
+ export default {
446
+ name: 'cache',
447
+ slices: [cacheSlice],
448
+ plugins: [ttlPlugin],
449
+ };
450
+ ```
451
+
452
+ ---
453
+
454
+ ## 🎯 Best Practices
455
+
456
+ ### 1. **Store Organization**
457
+
458
+ ```typescript
459
+ // ✅ Good: Feature-based slices
460
+ const userSlice = (set, get) => ({
461
+ /* user logic */
462
+ });
463
+ const settingsSlice = (set, get) => ({
464
+ /* settings logic */
465
+ });
466
+
467
+ // ❌ Avoid: One giant slice
468
+ const everythingSlice = (set, get) => ({
469
+ /* 500 lines of code */
470
+ });
471
+ ```
472
+
473
+ ### 2. **State Selection**
474
+
475
+ ```typescript
476
+ // ✅ Good: Specific selectors
477
+ const count = useStore((state) => state.count);
478
+ const userName = useStore((state) => state.user?.name);
479
+
480
+ // ❌ Avoid: Selecting entire state
481
+ const everything = useStore((state) => state); // Causes unnecessary re-renders
482
+ ```
483
+
484
+ ### 3. **Store Names**
485
+
486
+ ```typescript
487
+ // ✅ Good: Descriptive and consistent
488
+ createStore('user-preferences'); // Service worker
489
+ createStore('user-preferences'); // React (same name!)
490
+
491
+ // ❌ Avoid: Generic or mismatched names
492
+ createStore('store'); // Service worker
493
+ createStore('data'); // React (different name!)
494
+ ```
495
+
496
+ ### 4. **Error Handling**
497
+
498
+ ```typescript
499
+ // ✅ Good: Handle connection states
500
+ function AppContent() {
501
+ const store = useAppStore();
502
+
503
+ if (!store) {
504
+ return <LoadingSpinner />;
505
+ }
506
+
507
+ return <MainApp store={store} />;
508
+ }
509
+
510
+ // ❌ Avoid: Assuming store is always ready
511
+ function App() {
512
+ const store = useAppStore();
513
+ return <StoreProvider store={store}>...</StoreProvider>; // Might be null!
514
+ }
515
+ ```
516
+
517
+ ---
518
+
519
+ ## 🚨 Troubleshooting
520
+
521
+ ### Store Not Syncing
522
+
523
+ - ✅ Ensure both stores use the **exact same name**
524
+ - ✅ Check that service worker store is created first
525
+ - ✅ Verify `@chromahq/core` bridge is initialized with `create()`
526
+
527
+ ### React Components Not Updating
528
+
529
+ - ✅ Use specific selectors: `state => state.count` not `state => state`
530
+ - ✅ Ensure components are wrapped in `<StoreProvider>`
531
+ - ✅ Check that bridge connection is established
532
+
533
+ ### State Not Persisting
534
+
535
+ - ✅ Add `.withPersistence({ name: 'unique-name' })` to service worker store
536
+ - ✅ Ensure service worker has storage permissions in manifest
537
+ - ✅ Check Chrome DevTools → Application → Storage → Local Storage
538
+
539
+ ### TypeScript Errors
540
+
541
+ - ✅ Define interfaces for your slices
542
+ - ✅ Use `createStoreHooks<YourStateType>()` for typed hooks
543
+ - ✅ Ensure same state shape in service worker and React
544
+
545
+ ---
546
+
547
+ ## 📦 Package Info
548
+
549
+ - **Main API**: `createStore()` with plugin system
550
+ - **React Integration**: `createStoreHooks()` for type-safe hooks
551
+ - **Auto-Sync**: Works with `@chromahq/core` bridge
552
+ - **Persistence**: Built-in Chrome storage support
553
+ - **TypeScript**: Full type safety throughout
554
+
555
+ ## 📄 License
556
+
557
+ MIT
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@chromahq/store",
3
+ "version": "0.0.2",
4
+ "description": "Centralized, persistent store for Chrome extensions using zustand, accessible from service workers and React, with chrome.storage.local persistence.",
5
+ "type": "module",
6
+ "main": "dist/index.cjs.js",
7
+ "module": "dist/index.es.js",
8
+ "types": "dist/index.d.ts",
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.es.js",
15
+ "require": "./dist/index.cjs.js",
16
+ "types": "./dist/index.d.ts"
17
+ },
18
+ "./autoRegister": {
19
+ "import": "./dist/autoRegister.js",
20
+ "require": "./dist/autoRegister.cjs.js",
21
+ "types": "./dist/autoRegister.d.ts"
22
+ }
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/chromaHQ/chroma.git",
30
+ "directory": "packages/store"
31
+ },
32
+ "keywords": [
33
+ "chrome-extension",
34
+ "browser-extension",
35
+ "store",
36
+ "zustand",
37
+ "react",
38
+ "service-worker",
39
+ "persistence",
40
+ "chrome-storage"
41
+ ],
42
+ "author": "Chroma Team",
43
+ "license": "MIT",
44
+ "homepage": "https://github.com/chromaHQ/chroma#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/chromaHQ/chroma/issues"
47
+ },
48
+ "peerDependencies": {
49
+ "@chromahq/core": ">=0.0.1",
50
+ "react": ">=18"
51
+ },
52
+ "dependencies": {
53
+ "zustand": "^5.0.7"
54
+ },
55
+ "devDependencies": {
56
+ "@types/chrome": "^0.0.326",
57
+ "@types/react": "^18.2.7",
58
+ "typescript": "^5.8.3"
59
+ },
60
+ "scripts": {
61
+ "build": "rollup -c rollup.config.mjs",
62
+ "dev": "rollup -c rollup.config.mjs --watch"
63
+ }
64
+ }