crann 1.0.34 → 1.0.36

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 +434 -45
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,87 +1,476 @@
1
+ Crann: Effortless State Synchronization for Web Extensions
2
+
1
3
  ![crann_logo](img/crann_logo_smaller.png)
2
4
 
3
5
  `npm i crann`
4
6
 
5
- Crann synchronizes state in Web Extensions, with full Typescript support.
7
+ ## Table of Contents
8
+
9
+ - [Core Features](#core-features)
10
+ - [Quick Start](#quick-start-a-simple-synchronization-example)
11
+ - [Core Usage](#getting-started-core-usage)
12
+ - [Advanced Features](#advanced-features)
13
+ - [Complex Types](#handling-complex-types)
14
+ - [Partitioned State](#understanding-partitioned-state)
15
+ - [Persistence](#state-persistence-options)
16
+ - [Advanced API](#advanced-api-functions)
17
+ - [React Integration](#react-integration)
18
+ - [What Was The Problem?](#what-was-the-problem)
19
+ - [Why Is This Better?](#why-is-this-better-how-crann-simplifies-synchronization)
20
+
21
+ ##State Synchronization for Web Extensions
22
+
23
+ Crann synchronizes state across all parts of your Web Extension with full TypeScript support, eliminating the need for complex manual message passing. Focus on your extension's features, not the plumbing.
24
+
25
+ **Core Features:**
6
26
 
7
27
  - Minimal size (< 5kb)
8
- - Syncs state between any context you might want - Content Scripts, Devtools, Sidepanels, Popup etc.
9
- - Removes the need for a tangled web of message passing
10
- - React to state! Better coding patterns.
11
- - Optionally persist any value to storage (local or session) via config.
28
+ - Syncs state between any context (Content Scripts, Service Worker, Devtools, Sidepanels, Popup, etc.)
29
+ - Eliminates manual `chrome.runtime.sendMessage` / `onMessage` boilerplate
30
+ - Reactive state updates via subscriptions (`subscribe`)
31
+ - Optional state persistence (`Persistence.Local` / `Persistence.Session`)
32
+ - Strong TypeScript inference and support for type safety
12
33
 
13
- ### First, create a Crann instance
34
+ ### Quick Start: A Simple Synchronization Example
14
35
 
15
- Crann needs a service worker to coordinate state and access
36
+ Let's see how easy it is. Imagine we want a toggle in the popup to control whether a border is applied to the current web page by a content script.
37
+
38
+ **1. Define the state in your Service Worker:**
16
39
 
17
40
  ```typescript
18
- // service worker environment
19
- import { create } from 'crann'
41
+ // service-worker.ts
42
+ import { create } from "crann";
20
43
 
21
- // Create an state with some defaults
22
44
  const crann = create({
23
- active: {default: false} // types are inferred from provided default
24
- trees: {default: 0}
25
- name: {default: '', partition: Partition.Instance} // partitioned state will be different for each connected context, so everyone connected except the service worker
45
+ isBorderEnabled: { default: false }, // Single shared state item
46
+ });
47
+
48
+ console.log("Crann hub initialized.");
49
+ // Keep the service worker alive if needed (e.g., using chrome.runtime.connect)
50
+ // Crann itself doesn't automatically keep the SW alive.
51
+ ```
52
+
53
+ **2. Control the state from your Popup:**
54
+
55
+ ```typescript
56
+ // popup.ts
57
+ import { connect } from "crann";
58
+
59
+ const { set, get } = connect(); // Connect to the Crann hub
60
+
61
+ const toggleButton = document.getElementById("toggleBorder");
62
+
63
+ // Set initial button state
64
+ const currentState = get();
65
+ toggleButton.textContent = currentState.isBorderEnabled
66
+ ? "Disable Border"
67
+ : "Enable Border";
68
+
69
+ // Add click listener to update state
70
+ toggleButton.addEventListener("click", () => {
71
+ const newState = !get().isBorderEnabled; // Get current state before setting
72
+ set({ isBorderEnabled: newState });
73
+ // Update button text immediately (or subscribe to changes)
74
+ toggleButton.textContent = newState ? "Disable Border" : "Enable Border";
26
75
  });
76
+ ```
77
+
78
+ **3. React to the state in your Content Script:**
79
+
80
+ ```typescript
81
+ // content-script.ts
82
+ import { connect } from "crann";
83
+
84
+ const { subscribe } = connect(); // Connect to the Crann hub
85
+
86
+ console.log("Content script connected to Crann.");
87
+
88
+ // Subscribe to changes in 'isBorderEnabled'
89
+ subscribe(
90
+ (state) => {
91
+ console.log("Border state changed:", state.isBorderEnabled);
92
+ document.body.style.border = state.isBorderEnabled ? "5px solid green" : "";
93
+ },
94
+ ["isBorderEnabled"]
95
+ ); // Optional: Only trigger for specific key changes
96
+
97
+ // Apply initial state
98
+ const initialState = connect().get(); // Can call connect() again or store result
99
+ document.body.style.border = initialState.isBorderEnabled
100
+ ? "5px solid green"
101
+ : "";
102
+ ```
103
+
104
+ **Notice:** We achieved synchronization between the popup and content script _without writing any `chrome.runtime.sendMessage` or `chrome.runtime.onMessage` code!_ Crann handled the communication behind the scenes.
105
+
106
+ ## Getting Started: Core Usage
107
+
108
+ ### Step 1: Create the State Hub (Service Worker)
109
+
110
+ The service worker is where you initialize your shared state. Here's a more detailed example showing how to define different types of state:
27
111
 
28
- // Get the state whenever you like.
29
- const {active, trees} = crann.get() // Not passing in a key will only get state that is service to all, aka no partitioned state
30
- const {active, trees, name} = crann.get('instancekey') // Passing in a key will return the service state AND the partitioned state for that instance
112
+ ```typescript
113
+ // service-worker.ts
114
+ import { create, Partition, Persistence } from "crann";
31
115
 
32
- // subscribe to listen for state changes
33
- crann.subscribe( (state, changes, key) => {
34
- const { active, trees, name } = state;
35
- console.log('Service state: active: ', active, ', trees: ', trees);
36
- console.log(`Instance state for ${key}: `, name);
37
- // or just look at changes.
116
+ const crann = create({
117
+ // Basic state with default value
118
+ active: { default: false },
38
119
 
39
- // key will be the id of the context that made the state update, or null if the update came from the service worker (here)
120
+ // State that's unique to each context
121
+ name: {
122
+ default: "",
123
+ partition: Partition.Instance,
124
+ },
125
+
126
+ // State that persists between sessions
127
+ timesUsed: {
128
+ default: 0,
129
+ persistence: Persistence.Local,
130
+ },
131
+
132
+ // State that resets when the browser closes
133
+ sessionStart: {
134
+ default: new Date(),
135
+ persistence: Persistence.Session,
136
+ },
40
137
  });
41
138
 
42
- // Set the state (either for an instance or for the service state)
43
- crann.set({active: true}) // Will notify all connected contexts that active is now true.
44
- crann.set({name: 'ContentScript'}, 'instancekey'); // Or set for a particular instance if you like
139
+ // Get shared state (no instance state)
140
+ const { active, timesUsed } = crann.get();
141
+
142
+ // Optionally: Get state for a specific instance (includes instance state)
143
+ const { active, timesUsed, name } = crann.get("instanceKey");
144
+
145
+ // Subscribe to state changes
146
+ crann.subscribe((state, changes, key) => {
147
+ // state contains all state (shared + relevant partition)
148
+ // changes contains only the keys that changed
149
+ // key identifies which context made the change (null if from service worker)
150
+ console.log("State changed:", changes);
151
+ });
45
152
  ```
46
153
 
47
- ### Then, connect to your Crann instance from any context you like -- eg. content scripts
154
+ ### Step 2: Connect from Other Contexts
155
+
156
+ Other parts of your extension connect to the state hub. They automatically get access to both shared and their own partitioned state:
48
157
 
49
158
  ```typescript
159
+ // popup.ts or content-script.ts
50
160
  import { connect } from "crann";
161
+
51
162
  const { get, set, subscribe } = connect();
52
163
 
53
- // Similar to the service worker environment, except we will always be dealing with service state AND our own instance state. No distinction from our point of view.
54
- const { active, trees, name } = get();
164
+ // Get all state (shared + this context's partition)
165
+ const { active, name, timesUsed } = get();
55
166
 
56
- // Set the state
57
- set({ name: "My own name" });
167
+ // Set state
168
+ set({ name: "My Context's Name" });
58
169
 
59
- // subscribe to listen for a particular item of state changing, or any change
170
+ // Subscribe to specific state changes
60
171
  subscribe(
61
172
  (changes) => {
62
- console.log("Trees changed! new value: ", changes.trees);
173
+ console.log("Times used changed:", changes.timesUsed);
63
174
  },
64
- ["trees"]
175
+ ["timesUsed"]
65
176
  );
66
177
  ```
67
178
 
68
- ### Finally, Persist state!
179
+ ## Advanced Features
69
180
 
70
- You can persist items so that they will survive a refresh or context closure.
181
+ ### Handling Complex Types
71
182
 
72
- Session state: Will last between page refreshes, generally will reset if the user closes the tab or browser.
73
- Local state: Will persist long-term (unless the user specifically clears it in browser settings)
183
+ Sometimes the default value alone isn't enough for TypeScript to infer the full type. Use type assertions to specify the complete type:
74
184
 
75
185
  ```typescript
76
- // Simply add persistence to your config when creating crann in the service-worker
186
+ import { create } from "crann";
187
+
188
+ // Example 1: Custom object type with null default
189
+ type CustomType = { name: string; age: number };
190
+
191
+ // Example 2: Specific string literal union
192
+ type ConnectionStatus = "idle" | "connecting" | "connected" | "error";
193
+
194
+ const crann = create({
195
+ person: {
196
+ default: null as null | CustomType,
197
+ },
198
+ connectionStatus: {
199
+ default: "idle" as ConnectionStatus,
200
+ },
201
+ userStatus: {
202
+ default: "active" as "active" | "inactive",
203
+ persistence: Persistence.Local,
204
+ },
205
+ });
206
+
207
+ // Now TypeScript understands the full potential types
208
+ const state = crann.get();
209
+ // state.person could be null or { name: string; age: number }
210
+ // state.connectionStatus could be 'idle', 'connecting', 'connected', or 'error'
211
+ ```
212
+
213
+ ### Understanding Partitioned State
214
+
215
+ Partitioned state (`Partition.Instance`) is useful when you want each context to have its own version of a state variable. For example:
216
+
217
+ - Each content script might need its own `selectedElement` state
218
+ - Each popup might need its own `isOpen` state
219
+ - Each devtools panel might need its own `activeTab` state
220
+
221
+ The service worker can access any context's partitioned state using `get('instanceKey')`, but typically you'll let each context manage its own partitioned state.
222
+
223
+ ### State Persistence Options
224
+
225
+ Crann offers two levels of persistence:
226
+
227
+ - **Session Storage** (`Persistence.Session`): State persists between page refreshes but resets when the browser closes
228
+ - **Local Storage** (`Persistence.Local`): State persists long-term until explicitly cleared
229
+
230
+ ```typescript
231
+ const crann = create({
232
+ // Will be remembered between browser sessions
233
+ userPreferences: {
234
+ default: { theme: "light" },
235
+ persistence: Persistence.Local,
236
+ },
77
237
 
238
+ // Will reset when browser closes
239
+ currentSession: {
240
+ default: { startTime: new Date() },
241
+ persistence: Persistence.Session,
242
+ },
243
+ });
244
+ ```
245
+
246
+ Remember: Persisted state is always shared state (not partitioned).
247
+
248
+ ### Advanced API Functions
249
+
250
+ Crann provides additional functions for monitoring and managing instance connections:
251
+
252
+ ```typescript
253
+ // In the service worker
78
254
  const crann = create({
79
- active: {default: false} // types are inferred from provided default
80
- trees: {default: 0}
81
- name: {default: '', partition: Partition.Instance} // partitioned state will be different for each connected context, so everyone connected except the service worker
82
- timesUsed: {default: 0, Persistence.Local}
83
- firstOpened: {default: new Date(), Persistence.Session}
255
+ // ... state configuration
84
256
  });
85
257
 
86
- // note: Persisted state is Service state by default (shared with all contexts)
258
+ // Listen for new instance connections
259
+ crann.onInstanceConnect((instanceKey) => {
260
+ console.log(`New instance connected: ${instanceKey}`);
261
+ // You can access this instance's partitioned state
262
+ const instanceState = crann.get(instanceKey);
263
+ });
264
+
265
+ // Listen for instance disconnections
266
+ crann.onInstanceDisconnect((instanceKey) => {
267
+ console.log(`Instance disconnected: ${instanceKey}`);
268
+ // Clean up any resources associated with this instance
269
+ });
270
+
271
+ // Get a list of all currently connected instances
272
+ const connectedInstances = crann.getConnectedInstances();
273
+ console.log("Connected instances:", connectedInstances);
274
+
275
+ // In any context (including service worker)
276
+ const { getInstanceKey } = connect();
277
+ // Get this context's unique instance key
278
+ const myInstanceKey = getInstanceKey();
279
+ ```
280
+
281
+ These functions are particularly useful for:
282
+
283
+ - Tracking which content scripts are currently active
284
+ - Cleaning up resources when instances disconnect
285
+ - Debugging connection issues
286
+ - Managing instance-specific resources in the service worker
287
+
288
+ The `getInstanceKey()` function is available in all contexts and can be useful for:
289
+
290
+ - Logging and debugging
291
+ - Coordinating with other systems that need to identify specific instances
292
+ - Managing instance-specific resources
293
+
294
+ ### React Integration
295
+
296
+ Crann provides a custom React hook for easy integration with React applications. This is particularly useful when you have a React app running in an iframe injected by your content script.
297
+
298
+ ```typescript
299
+ // In your React component
300
+ import { useCrann } from "crann";
301
+
302
+ function MyReactComponent() {
303
+ // The hook returns the same interface as connect()
304
+ const { get, set, subscribe } = useCrann();
305
+
306
+ // Get the current state
307
+ const { isEnabled, count } = get();
308
+
309
+ // Set state (triggers re-render)
310
+ const toggleEnabled = () => {
311
+ set({ isEnabled: !isEnabled });
312
+ };
313
+
314
+ // Subscribe to specific state changes
315
+ subscribe(
316
+ (changes) => {
317
+ console.log("Count changed:", changes.count);
318
+ },
319
+ ["count"]
320
+ );
321
+
322
+ return (
323
+ <div>
324
+ <button onClick={toggleEnabled}>
325
+ {isEnabled ? "Disable" : "Enable"}
326
+ </button>
327
+ <p>Count: {count}</p>
328
+ </div>
329
+ );
330
+ }
331
+ ```
332
+
333
+ The `useCrann` hook provides the same functionality as `connect()`, but with React-specific optimizations:
334
+
335
+ - Automatically re-renders components when subscribed state changes
336
+ - Handles cleanup of subscriptions when components unmount
337
+ - Provides TypeScript support for your state types
338
+
339
+ #### Using with TypeScript
340
+
341
+ For better type safety, you can create a custom hook that includes your state types:
342
+
343
+ ```typescript
344
+ // types.ts
345
+ interface MyState {
346
+ isEnabled: boolean;
347
+ count: number;
348
+ user: {
349
+ name: string;
350
+ age: number;
351
+ } | null;
352
+ }
353
+
354
+ // hooks.ts
355
+ import { useCrann } from "crann";
356
+ import type { MyState } from "./types";
357
+
358
+ export function useMyCrann() {
359
+ return useCrann<MyState>();
360
+ }
361
+
362
+ // MyComponent.tsx
363
+ import { useMyCrann } from "./hooks";
364
+
365
+ function MyComponent() {
366
+ const { get, set } = useMyCrann();
367
+
368
+ // TypeScript now knows the shape of your state
369
+ const { user } = get();
370
+
371
+ const updateUser = () => {
372
+ set({
373
+ user: {
374
+ name: "Alice",
375
+ age: 30,
376
+ },
377
+ });
378
+ };
379
+
380
+ return (
381
+ <div>
382
+ {user && <p>Hello, {user.name}!</p>}
383
+ <button onClick={updateUser}>Update User</button>
384
+ </div>
385
+ );
386
+ }
87
387
  ```
388
+
389
+ #### Performance Considerations
390
+
391
+ The `useCrann` hook is optimized for React usage:
392
+
393
+ - Only re-renders when subscribed state actually changes
394
+ - Batches multiple state updates to minimize re-renders
395
+ - Automatically cleans up subscriptions on unmount
396
+ - Supports selective subscription to specific state keys
397
+
398
+ For best performance:
399
+
400
+ 1. Subscribe only to the state keys your component needs
401
+ 2. Use the second parameter of `subscribe` to specify which keys to listen for
402
+ 3. Consider using `useMemo` for derived state
403
+ 4. Use `useCallback` for event handlers that update state
404
+
405
+ ```typescript
406
+ function OptimizedComponent() {
407
+ const { get, set, subscribe } = useCrann();
408
+ const { items, filter } = get();
409
+
410
+ // Only re-render when items or filter changes
411
+ const filteredItems = useMemo(() => {
412
+ return items.filter((item) => item.includes(filter));
413
+ }, [items, filter]);
414
+
415
+ // Memoize the handler
416
+ const handleFilterChange = useCallback(
417
+ (newFilter: string) => {
418
+ set({ filter: newFilter });
419
+ },
420
+ [set]
421
+ );
422
+
423
+ // Only subscribe to the keys we care about
424
+ subscribe(
425
+ (changes) => {
426
+ console.log("Filter changed:", changes.filter);
427
+ },
428
+ ["filter"]
429
+ );
430
+
431
+ return (
432
+ <div>
433
+ <input
434
+ value={filter}
435
+ onChange={(e) => handleFilterChange(e.target.value)}
436
+ />
437
+ <ul>
438
+ {filteredItems.map((item) => (
439
+ <li key={item}>{item}</li>
440
+ ))}
441
+ </ul>
442
+ </div>
443
+ );
444
+ }
445
+ ```
446
+
447
+
448
+ ## What Was The Problem?
449
+
450
+ Browser extensions often have multiple components:
451
+
452
+ - **Service Worker:** A background script handling core logic and events.
453
+ - **Content Scripts:** JavaScript injected directly into web pages.
454
+ - **Popup:** A small window shown when clicking the extension icon.
455
+ - **Side Panels, DevTools Pages:** Other specialized UI or inspection contexts.
456
+
457
+ These components run in isolated environments. Sharing data or coordinating actions between them traditionally requires manually sending messages back and forth using APIs like `chrome.runtime.sendMessage` and setting up listeners (`chrome.runtime.onMessage`). This can quickly lead to complex, hard-to-debug "spaghetti code" as your extension grows.
458
+
459
+ ## Why Is This Better: How Crann Simplifies Synchronization
460
+
461
+ Crann acts as a central state management hub, typically initialized in your service worker. It provides a single source of truth for your shared data. Other contexts connect to this hub, allowing them to easily read state, update it, and subscribe to changes.
462
+
463
+ **Visualizing the Problem: Manual Message Passing vs. Crann's Centralized State**
464
+
465
+ ![with_messages](img/with_messages.png)
466
+ _Traditional message passing requires complex, bidirectional communication between all parts._
467
+
468
+ ![with_crann](img/with_crann.png)
469
+ _Crann's centralized state management simplifies the architecture by eliminating the need for manual message passing._
470
+
471
+ This dramatically simplifies your architecture:
472
+
473
+ - **No more manual messaging:** Crann handles the communication internally.
474
+ - **Single source of truth:** State is managed centrally.
475
+ - **Reactivity:** Components automatically react to state changes they care about.
476
+
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "crann",
3
- "version": "1.0.34",
4
- "description": "",
3
+ "version": "1.0.36",
4
+ "description": "Effortless State Synchronization for Web Extensions",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
7
7
  "types": "dist/types/index.d.ts",