crann 1.0.35 → 1.0.37

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 CHANGED
@@ -1,3 +1,5 @@
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`
@@ -5,18 +7,19 @@
5
7
  ## Table of Contents
6
8
 
7
9
  - [Core Features](#core-features)
8
- - [The Challenge](#the-challenge-state-across-extension-contexts)
9
- - [The Solution](#the-solution-how-crann-simplifies-synchronization)
10
10
  - [Quick Start](#quick-start-a-simple-synchronization-example)
11
- - [Getting Started](#getting-started-core-usage)
11
+ - [Core Usage](#getting-started-core-usage)
12
12
  - [Advanced Features](#advanced-features)
13
13
  - [Complex Types](#handling-complex-types)
14
14
  - [Partitioned State](#understanding-partitioned-state)
15
15
  - [Persistence](#state-persistence-options)
16
16
  - [Advanced API](#advanced-api-functions)
17
+ - [Remote Procedure Calls (RPC Actions)](#remote-procedure-calls-rpc-actions)
17
18
  - [React Integration](#react-integration)
19
+ - [What Was The Problem?](#what-was-the-problem)
20
+ - [Why Is This Better?](#why-is-this-better-how-crann-simplifies-synchronization)
18
21
 
19
- ## Crann: Effortless State Synchronization for Web Extensions
22
+ ##State Synchronization for Web Extensions
20
23
 
21
24
  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.
22
25
 
@@ -29,35 +32,6 @@ Crann synchronizes state across all parts of your Web Extension with full TypeSc
29
32
  - Optional state persistence (`Persistence.Local` / `Persistence.Session`)
30
33
  - Strong TypeScript inference and support for type safety
31
34
 
32
- ## The Challenge: State Across Extension Contexts
33
-
34
- Browser extensions often have multiple components:
35
-
36
- - **Service Worker:** A background script handling core logic and events.
37
- - **Content Scripts:** JavaScript injected directly into web pages.
38
- - **Popup:** A small window shown when clicking the extension icon.
39
- - **Side Panels, DevTools Pages:** Other specialized UI or inspection contexts.
40
-
41
- 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.
42
-
43
- ## The Solution: How Crann Simplifies Synchronization
44
-
45
- 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.
46
-
47
- **Visualizing the Problem: Manual Message Passing vs. Crann's Centralized State**
48
-
49
- ![with_messages](img/with_messages.png)
50
- _Traditional message passing requires complex, bidirectional communication between all parts._
51
-
52
- ![with_crann](img/with_crann.png)
53
- _Crann's centralized state management simplifies the architecture by eliminating the need for manual message passing._
54
-
55
- This dramatically simplifies your architecture:
56
-
57
- - **No more manual messaging:** Crann handles the communication internally.
58
- - **Single source of truth:** State is managed centrally.
59
- - **Reactivity:** Components automatically react to state changes they care about.
60
-
61
35
  ### Quick Start: A Simple Synchronization Example
62
36
 
63
37
  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.
@@ -274,49 +248,136 @@ Remember: Persisted state is always shared state (not partitioned).
274
248
 
275
249
  ### Advanced API Functions
276
250
 
277
- Crann provides additional functions for monitoring and managing instance connections:
251
+ The `create` function returns an object with the following methods:
278
252
 
279
253
  ```typescript
280
- // In the service worker
281
254
  const crann = create({
282
- // ... state configuration
255
+ // ... state config ...
256
+ });
257
+
258
+ // Get state
259
+ const state = crann.get(); // Get all state
260
+ const instanceState = crann.get("instanceKey"); // Get state for specific instance
261
+
262
+ // Set state
263
+ await crann.set({ key: "value" }); // Set service state
264
+ await crann.set({ key: "value" }, "instanceKey"); // Set instance state
265
+
266
+ // Subscribe to state changes
267
+ crann.subscribe((state, changes, agent) => {
268
+ // state: The complete state
269
+ // changes: Only the changed values
270
+ // agent: Info about which context made the change
271
+ });
272
+
273
+ // Subscribe to instance ready events
274
+ const unsubscribe = crann.onInstanceReady((instanceId, agent) => {
275
+ // Called when a new instance connects
276
+ // Returned function can be called to unsubscribe
283
277
  });
284
278
 
285
- // Listen for new instance connections
286
- crann.onInstanceConnect((instanceKey) => {
287
- console.log(`New instance connected: ${instanceKey}`);
288
- // You can access this instance's partitioned state
289
- const instanceState = crann.get(instanceKey);
279
+ // Find an instance by location
280
+ const instanceId = crann.findInstance({
281
+ context: "content-script",
282
+ tabId: 123,
283
+ frameId: 0,
290
284
  });
291
285
 
292
- // Listen for instance disconnections
293
- crann.onInstanceDisconnect((instanceKey) => {
294
- console.log(`Instance disconnected: ${instanceKey}`);
295
- // Clean up any resources associated with this instance
286
+ // Query agents by location
287
+ const agents = crann.queryAgents({
288
+ context: "content-script",
296
289
  });
297
290
 
298
- // Get a list of all currently connected instances
299
- const connectedInstances = crann.getConnectedInstances();
300
- console.log("Connected instances:", connectedInstances);
291
+ // Clear all state
292
+ await crann.clear();
293
+ ```
294
+
295
+ ### Remote Procedure Calls (RPC Actions)
296
+
297
+ Crann now supports RPC-style actions that execute in the service worker context while being callable from any extension context. This is perfect for operations that need to run in the service worker, like making network requests or accessing extension APIs.
298
+
299
+ #### Defining Actions in the Service Worker
300
+
301
+ Actions are defined in your config alongside regular state items. The key difference is that actions have a `handler` property:
302
+
303
+ ```typescript
304
+ // service-worker.ts
305
+ import { create } from "crann";
306
+
307
+ const crann = create({
308
+ // Regular state
309
+ counter: {
310
+ default: 0,
311
+ partition: "service",
312
+ persist: "local",
313
+ },
314
+
315
+ // RPC action
316
+ increment: {
317
+ handler: async (state, amount: number) => {
318
+ // This runs in the service worker
319
+ return { counter: state.counter + amount };
320
+ },
321
+ validate: (amount: number) => {
322
+ if (amount < 0) throw new Error("Amount must be positive");
323
+ },
324
+ },
301
325
 
302
- // In any context (including service worker)
303
- const { getInstanceKey } = connect();
304
- // Get this context's unique instance key
305
- const myInstanceKey = getInstanceKey();
326
+ // Another action example
327
+ fetchData: {
328
+ handler: async (state, url: string) => {
329
+ // This runs in the service worker where we can make network requests
330
+ const response = await fetch(url);
331
+ const data = await response.json();
332
+ return { data };
333
+ },
334
+ validate: (url: string) => {
335
+ if (!url.startsWith("https://")) {
336
+ throw new Error("URL must be HTTPS");
337
+ }
338
+ },
339
+ },
340
+ });
306
341
  ```
307
342
 
308
- These functions are particularly useful for:
343
+ #### Using Actions in Other Contexts
344
+
345
+ Actions can be called from any context that connects to Crann:
346
+
347
+ ```typescript
348
+ // content-script.ts
349
+ import { connect } from "crann";
350
+
351
+ const [useCrann, get, set, subscribe, getAgentInfo, onReady, callAction] =
352
+ connect(config);
353
+
354
+ // Wait for connection
355
+ onReady((status) => {
356
+ if (status.connected) {
357
+ // Call the increment action
358
+ callAction("increment", 1).then((result) => {
359
+ console.log("Counter incremented:", result);
360
+ });
309
361
 
310
- - Tracking which content scripts are currently active
311
- - Cleaning up resources when instances disconnect
312
- - Debugging connection issues
313
- - Managing instance-specific resources in the service worker
362
+ // Call the fetchData action
363
+ callAction("fetchData", "https://api.example.com/data")
364
+ .then((result) => {
365
+ console.log("Fetched data:", result.data);
366
+ })
367
+ .catch((error) => {
368
+ console.error("Failed to fetch data:", error);
369
+ });
370
+ }
371
+ });
372
+ ```
314
373
 
315
- The `getInstanceKey()` function is available in all contexts and can be useful for:
374
+ #### Key Benefits of RPC Actions
316
375
 
317
- - Logging and debugging
318
- - Coordinating with other systems that need to identify specific instances
319
- - Managing instance-specific resources
376
+ 1. **Type Safety**: Full TypeScript support for action parameters and return values
377
+ 2. **Validation**: Optional validation of action parameters before execution
378
+ 3. **Service Worker Context**: Actions run in the service worker where they have access to all extension APIs
379
+ 4. **Automatic State Updates**: Actions can return state updates that are automatically synchronized
380
+ 5. **Error Handling**: Proper error propagation from service worker to calling context
320
381
 
321
382
  ### React Integration
322
383
 
@@ -470,3 +531,32 @@ function OptimizedComponent() {
470
531
  );
471
532
  }
472
533
  ```
534
+
535
+ ## What Was The Problem?
536
+
537
+ Browser extensions often have multiple components:
538
+
539
+ - **Service Worker:** A background script handling core logic and events.
540
+ - **Content Scripts:** JavaScript injected directly into web pages.
541
+ - **Popup:** A small window shown when clicking the extension icon.
542
+ - **Side Panels, DevTools Pages:** Other specialized UI or inspection contexts.
543
+
544
+ 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.
545
+
546
+ ## Why Is This Better: How Crann Simplifies Synchronization
547
+
548
+ 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.
549
+
550
+ **Visualizing the Problem: Manual Message Passing vs. Crann's Centralized State**
551
+
552
+ ![with_messages](img/with_messages.png)
553
+ _Traditional message passing requires complex, bidirectional communication between all parts._
554
+
555
+ ![with_crann](img/with_crann.png)
556
+ _Crann's centralized state management simplifies the architecture by eliminating the need for manual message passing._
557
+
558
+ This dramatically simplifies your architecture:
559
+
560
+ - **No more manual messaging:** Crann handles the communication internally.
561
+ - **Single source of truth:** State is managed centrally.
562
+ - **Reactivity:** Components automatically react to state changes they care about.
package/dist/cjs/index.js CHANGED
@@ -1,3 +1,3 @@
1
- "use strict";var Q=Object.create;var b=Object.defineProperty;var j=Object.getOwnPropertyDescriptor;var X=Object.getOwnPropertyNames;var Y=Object.getPrototypeOf,Z=Object.prototype.hasOwnProperty;var _=(a,e)=>{for(var t in e)b(a,t,{get:e[t],enumerable:!0})},B=(a,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of X(e))!Z.call(a,i)&&i!==t&&b(a,i,{get:()=>e[i],enumerable:!(n=j(e,i))||n.enumerable});return a};var ee=(a,e,t)=>(t=a!=null?Q(Y(a)):{},B(e||!a||!a.__esModule?b(t,"default",{value:a,enumerable:!0}):t,a)),te=a=>B(b({},"__esModule",{value:!0}),a),U=(a,e,t,n)=>{for(var i=n>1?void 0:n?j(e,t):e,s=a.length-1,o;s>=0;s--)(o=a[s])&&(i=(n?o(e,t,i):o(i))||i);return n&&i&&b(e,t,i),i};var ae={};_(ae,{Crann:()=>A,Partition:()=>I,Persistence:()=>N,connect:()=>k,connected:()=>F,create:()=>V,createCrannStateHook:()=>W});module.exports=te(ae);var P=ee(require("webextension-polyfill"));var I={Instance:"instance",Service:"service"},N={Session:"session",Local:"local",None:"none"};var z=require("porter-source");function w(a,e){if(a===e)return!0;if(a==null||typeof a!="object"||e==null||typeof e!="object")return!1;let t=Object.keys(a),n=Object.keys(e);if(t.length!==n.length)return!1;t.sort(),n.sort();for(let i=0;i<t.length;i++){let s=t[i];if(s!==n[i]||!w(a[s],e[s]))return!1}return!0}var m=class m{static setDebug(e){m._debug=e}static isDebugEnabled(){return m._debug}};m._debug=!1;var D=m;function $(){return D.isDebugEnabled()}function O(a,e,t){let n=t.value;return t.value=function(...i){var s;if($()){let o=new Error().stack,u=(s=((o==null?void 0:o.split(`
2
- `))||[])[3])==null?void 0:s.match(/at\s+(\S+)\s+/),h=u?u[1]:"unknown",g=i.length>1?i[1]:void 0,d={source:h,timestamp:Date.now(),instanceKey:i[0],changes:g};console.log("Crann State Change:",d)}return n.apply(this,i)},t}var v=class v{constructor(e,t){this.config=e;this.instances=new Map;this.stateChangeListeners=[];this.instanceReadyListeners=[];this.storagePrefix="crann_";this.debug=!1;this.porter=(0,z.source)("crann");t!=null&&t.debug&&D.setDebug(!0),this.debug=(t==null?void 0:t.debug)||!1,this.storagePrefix=(t==null?void 0:t.storagePrefix)??this.storagePrefix,this.log("Constructing"),this.defaultInstanceState=this.initializeInstanceDefault(),this.defaultServiceState=this.serviceState=this.initializeServiceDefault(),this.hydrate(),this.porter.onMessage({setState:(n,i)=>{if(!i){this.log("setState message heard from unknown agent");return}let s=this.getAgentTag(i);this.instanceLog("Setting state: ",s,n),this.set(n.payload.state,i.id)}}),this.porter.onMessagesSet(n=>{if(!n){this.error("Messages set but no agent info.",{info:n});return}this.instanceLog("Messages set received. Sending initial state.",this.getAgentTag(n),{info:n});let i=this.get(n.id);this.porter.post({action:"initialState",payload:{state:i,info:n}},n.location),this.notifyInstanceReady(n.id,n)}),this.porter.onConnect(n=>{if(!n){this.error("Agent connected but no agent info.",{info:n});return}let i=this.getAgentTag(n);this.instanceLog("Agent connected ",i,{info:n}),this.addInstance(n.id),this.porter.onDisconnect(s=>{this.instanceLog("Agent disconnect heard. Connection type, context and location: ",this.getAgentTag(s),{info:s}),this.removeInstance(s.id)})})}static getInstance(e,t){return v.instance?t!=null&&t.debug&&console.log("CrannSource [static-core], Instance requested and already existed, returning"):v.instance=new v(e,t),v.instance}async addInstance(e){if(this.instances.has(e))this.instanceLog("Instance was already registered, ignoring request from key: ",e);else{this.instanceLog("Adding instance from agent key: ",e);let t={...this.defaultInstanceState};this.instances.set(e,t)}}async removeInstance(e){this.instances.has(e)?(this.instanceLog("Remove instance requested. ",e),this.instances.delete(e)):this.instanceLog("Remove instance requested but it did not exist!. ",e)}async setServiceState(e){this.log("Request to set service state with: ",e);let t={...this.serviceState,...e};w(this.serviceState,t)?this.log("New state seems to be the same as existing, skipping"):(this.log("Confirmed new state was different than existing so proceeding to persist then notify all connected instances."),this.serviceState=t,await this.persist(e),this.notify(e))}async setInstanceState(e,t){this.instanceLog("Request to update instance state, update: ",e,t);let n=this.instances.get(e)||this.defaultInstanceState,i={...n,...t};w(n,i)?this.instanceLog("Instance state update is not different, skipping update. ",e):(this.instanceLog("Instance state update is different, updating and notifying. ",e),this.instances.set(e,i),this.notify(t,e))}async persist(e){this.log("Persisting state");let t=!1;for(let n in e||this.serviceState){let s=this.config[n].persist||"none",o=e?e[n]:this.serviceState[n];switch(s){case"session":await P.default.storage.session.set({[this.storagePrefix+n]:o}),t=!0;break;case"local":await P.default.storage.local.set({[this.storagePrefix+n]:o}),t=!0;break;default:break}}t?this.log("State was persisted"):this.log("Nothing to persist")}async clear(){this.log("Clearing state"),this.serviceState=this.defaultServiceState,this.instances.forEach((e,t)=>{this.instances.set(t,this.defaultInstanceState)}),await this.persist(),this.notify({})}subscribe(e){this.log("Subscribing to state"),this.stateChangeListeners.push(e)}notify(e,t){let n=t?this.porter.getAgentById(t):void 0,i=t?this.get(t):this.get();this.stateChangeListeners.length>0&&(this.log("Notifying state change listeners in source"),this.stateChangeListeners.forEach(s=>{s(i,e,n==null?void 0:n.info)})),t&&(n!=null&&n.info.location)?(this.instanceLog("Notifying of state change.",t),this.porter.post({action:"stateUpdate",payload:{state:e}},n.info.location)):(console.log("Notifying everyone"),this.instances.forEach((s,o)=>{this.porter.post({action:"stateUpdate",payload:{state:e}},o)}))}get(e){return e?{...this.serviceState,...this.instances.get(e)}:{...this.serviceState}}findInstance(e){let t=this.porter.getAgentByLocation(e);if(!t)return this.log("Could not find agent for location: ",{location:e}),null;for(let[n,i]of this.instances)if(n===t.info.id)return this.log("Found instance for key: ",n),n;return this.log("Could not find instance for context and location: ",{location:e}),null}queryAgents(e){return this.porter.queryAgents(e)}async set(e,t){let n={},i={};for(let s in e){let o=this.config[s];if(o.partition==="instance"){let f=s,u=e;n[f]=u[f]}else if(!o.partition||o.partition===I.Service){let f=s,u=e;i[f]=u[f]}}t&&Object.keys(n).length>0&&(this.instanceLog("Setting instance state: ",t,n),this.setInstanceState(t,n)),Object.keys(i).length>0&&(this.log("Setting service state: ",i),this.setServiceState(i))}async hydrate(){this.log("Hydrating state from storage.");let e=await P.default.storage.local.get(null),t=await P.default.storage.session.get(null),n={...e,...t},i={},s=!1;for(let o in n){let f=this.removePrefix(o);if(this.config.hasOwnProperty(f)){let u=n[f];i[f]=u,s=!0}}s?this.log("Hydrated some items."):this.log("No items found in storage."),this.serviceState={...this.defaultServiceState,...i}}removePrefix(e){return e.startsWith(this.storagePrefix)?e.replace(this.storagePrefix,""):e}getAgentTag(e){return`${e.location.context}:${e.location.tabId}:${e.location.frameId}`}initializeInstanceDefault(){let e={};return Object.keys(this.config).forEach(t=>{let n=this.config[t];n.partition==="instance"&&(e[t]=n.default)}),e}initializeServiceDefault(){let e={};return Object.keys(this.config).forEach(t=>{let n=this.config[t];n.partition===I.Service&&(e[t]=n.default)}),e}subscribeToInstanceReady(e){return this.log("Subscribing to instance ready events"),this.instanceReadyListeners.push(e),this.instances.forEach((t,n)=>{let i=this.porter.getAgentById(n);i!=null&&i.info&&e(n,i.info)}),()=>{this.log("Unsubscribing from instance ready events");let t=this.instanceReadyListeners.indexOf(e);t!==-1&&this.instanceReadyListeners.splice(t,1)}}notifyInstanceReady(e,t){this.instanceReadyListeners.length>0&&(this.instanceLog("Notifying instance ready listeners",e),this.instanceReadyListeners.forEach(n=>{n(e,t)}))}log(e,...t){this.debug&&console.log("CrannSource [core], "+e,...t)}instanceLog(e,t,...n){this.debug&&console.log(`CrannSource [${t}], `+e,...n)}error(e,...t){console.error("CrannSource [core], "+e,...t)}warn(e,...t){console.warn("CrannSource [core], "+e,...t)}};v.instance=null,U([O],v.prototype,"setServiceState",1),U([O],v.prototype,"setInstanceState",1);var A=v;function V(a,e){let t=A.getInstance(a,e);return{get:t.get.bind(t),set:t.set.bind(t),subscribe:t.subscribe.bind(t),onInstanceReady:t.subscribeToInstanceReady.bind(t),findInstance:t.findInstance.bind(t),queryAgents:t.queryAgents.bind(t),clear:t.clear.bind(t)}}var H=require("porter-source"),C={connected:!1},x=null;function k(a,e){let t=(e==null?void 0:e.debug)||!1,n=e==null?void 0:e.context,i,s="unset",o=(r,...c)=>{t&&console.log(`CrannAgent [${s}] `+r,...c)},f=new Set;if(o("Initializing with context: ",n),x){o("We had an instance already, returning"),C.connected&&(console.log("[Crann:Agent] connect, calling onReady callback"),setTimeout(()=>{f.forEach(c=>c(C))},0));let r=x;return[r[0],r[1],r[2],r[3],r[4],c=>(console.log("[Crann:Agent] connect, adding onReady callback"),f.add(c),()=>f.delete(c))]}o("No existing instance, creating a new one");let{post:u,onMessage:h}=(0,H.connect)({namespace:"crann"});h({initialState:r=>{g=r.payload.state,i=r.payload.info,s=ie(i),C={connected:!0,agent:i},o(`Initial state received and ${S.size} listeners notified`,{message:r}),f.forEach(c=>c(C)),S.forEach(c=>{c.callback(g)})},stateUpdate:r=>{d=r.payload.state,g={...g,...d},o("State updated: ",{message:r,changes:d,_state:g}),d&&S.forEach(c=>{(c.keys===void 0||c.keys.some(K=>K in d))&&c.callback(d)})}}),o("Porter connected. Setting up state and listeners");let g=ne(a),d=null,S=new Set;o("Completed setup, returning instance");let p=()=>g,R=r=>{console.log("CrannAgent, calling post with setState"),u({action:"setState",payload:{state:r}})},y=(r,c)=>{let T={keys:c,callback:r};return S.add(T),()=>{S.delete(T)}};return x=[r=>{let c=()=>p()[r],T=E=>R({[r]:E}),K=E=>{let q=c();return y(G=>{if(r in G){let M=c(),J=p();E({current:M,previous:q,state:J}),q=M}},[r])};return[c(),T,K]},p,R,y,()=>i,r=>(f.add(r),C.connected&&(console.log("[Crann:Agent], calling onReady callback"),setTimeout(()=>{r(C)},0)),()=>f.delete(r))],x}function F(){return x!==null}function ne(a){let e={};Object.keys(a).forEach(n=>{let i=a[n];i.partition==="instance"&&(e[n]=i.default)});let t={};return Object.keys(a).forEach(n=>{let i=a[n];i.partition==="service"&&(t[n]=i.default)}),{...e,...t}}function ie(a){return`${a.location.context}:${a.location.tabId}:${a.location.frameId}`}var l=require("react");function W(a){return function(t){let[n,i,s,o]=(0,l.useMemo)(()=>k(a),[t]),f=(0,l.useCallback)(g=>{let[d,S]=(0,l.useState)(i()[g]),p=(0,l.useRef)(d);(0,l.useEffect)(()=>{p.current=d},[d]),(0,l.useEffect)(()=>(S(i()[g]),o(L=>{g in L&&S(L[g])},[g])),[g]);let R=(0,l.useCallback)(y=>{s({[g]:y})},[g]);return[d,R]},[i,s,o]),u=(0,l.useCallback)(()=>i(),[i]),h=(0,l.useCallback)(g=>{s(g)},[s]);return{useStateItem:f,getState:u,setState:h,useCrann:n}}}0&&(module.exports={Crann,Partition,Persistence,connect,connected,create,createCrannStateHook});
1
+ "use strict";var ge=Object.create;var M=Object.defineProperty;var G=Object.getOwnPropertyDescriptor;var ce=Object.getOwnPropertyNames;var le=Object.getPrototypeOf,fe=Object.prototype.hasOwnProperty;var de=(r,e)=>{for(var t in e)M(r,t,{get:e[t],enumerable:!0})},X=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of ce(e))!fe.call(r,i)&&i!==t&&M(r,i,{get:()=>e[i],enumerable:!(n=G(e,i))||n.enumerable});return r};var ue=(r,e,t)=>(t=r!=null?ge(le(r)):{},X(e||!r||!r.__esModule?M(t,"default",{value:r,enumerable:!0}):t,r)),pe=r=>X(M({},"__esModule",{value:!0}),r),V=(r,e,t,n)=>{for(var i=n>1?void 0:n?G(e,t):e,a=r.length-1,s;a>=0;a--)(s=r[a])&&(i=(n?s(e,t,i):s(i))||i);return n&&i&&M(e,t,i),i};var Se={};de(Se,{Crann:()=>N,Partition:()=>k,Persistence:()=>J,connect:()=>z,connected:()=>ae,create:()=>te,createCrannStateHook:()=>re});module.exports=pe(Se);var O=ue(require("webextension-polyfill"));var k={Instance:"instance",Service:"service"},J={Session:"session",Local:"local",None:"none"},L=r=>!("handler"in r),q=r=>"handler"in r;var ee=require("porter-source");function F(r,e){if(r===e)return!0;if(r==null||typeof r!="object"||e==null||typeof e!="object")return!1;let t=Object.keys(r),n=Object.keys(e);if(t.length!==n.length)return!1;t.sort(),n.sort();for(let i=0;i<t.length;i++){let a=t[i];if(a!==n[i]||!F(r[a],e[a]))return!1}return!0}var K=class K{static setDebug(e){K._debug=e}static isDebugEnabled(){return K._debug}};K._debug=!1;var U=K;function Q(){return U.isDebugEnabled()}function H(r,e,t){let n=t.value;return t.value=function(...i){var a;if(Q()){let s=new Error().stack,c=(a=((s==null?void 0:s.split(`
2
+ `))||[])[3])==null?void 0:a.match(/at\s+(\S+)\s+/),S=c?c[1]:"unknown",l=i.length>1?i[1]:void 0,C={source:S,timestamp:Date.now(),instanceKey:i[0],changes:l};console.log("Crann State Change:",C)}return n.apply(this,i)},t}var _=require("porter-source");var y=class y{constructor(e){this.tag=null;this.context=e}static setDebug(e){y.debug=e}static setPrefix(e){y.prefix=e}setTag(e){this.tag=e}withTag(e){let t=new y(this.context);return t.setTag(e),t}getFullContext(){return this.tag?`${this.context}:${this.tag}`:this.context}createLogMethods(){let e=this.getFullContext(),t=`%c${y.prefix}%c [%c${e}%c]`,n="color: #3fcbff; font-weight: bold",i="color: #d58cff; font-weight: bold",a="";return y.debug?{debug:console.log.bind(console,t,n,a,i,a),log:console.log.bind(console,t,n,a,i,a),info:console.info.bind(console,t,n,a,i,a),warn:console.warn.bind(console,t,n,a,i,a),error:console.error.bind(console,t,n,a,i,a)}:{debug:y.noOp,log:y.noOp,info:y.noOp,warn:y.noOp,error:y.noOp}}get debug(){return this.createLogMethods().debug}get log(){return this.createLogMethods().log}get info(){return this.createLogMethods().info}get warn(){return this.createLogMethods().warn}get error(){return this.createLogMethods().error}static forContext(e,t){let n=new y(e);return t&&n.setTag(t),n}};y.debug=!1,y.prefix="CrannLogger",y.noOp=function(){};var m=y;function b(r,e={terse:!0}){let n=(i=>{let a=String(i);return a.length>=4?a.slice(-4):a.padStart(4,"0")})(r.location.tabId);return e!=null&&e.terse?`${n}:${r.location.frameId}`:`${r.location.context}:${n}:${r.location.frameId}`}function Z(r,e,t,n){var l,C;let i=new Map,a=new Map,s=(l=r.context)!=null&&l.isServiceWorker?"Core":"Agent",o=m.forContext(`${s}:RPC`);(C=r.context)!=null&&C.agentInfo&&o.setTag(b(r.context.agentInfo));let c=async h=>(Object.assign(e,h),Promise.resolve());return r.addEventListener("message",h=>{o.debug("Message received:",h);let[d,u]=h.data;if("call"in u&&"args"in u.call){o.debug("Processing call message:",u);let p=u.call,{id:f,args:P,target:A}=p,E=t[f];if(!E){r.postMessage([d,{error:{id:f,error:"Action not found",target:A}}]);return}try{E.validate&&E.validate(...P),Promise.resolve(E.handler(e,c,...P)).then(x=>{o.debug("Action handler result:",{result:x,target:A}),r.postMessage([d,{result:{id:f,result:x,target:A}}])},x=>{r.postMessage([d,{error:{id:f,error:x.message,target:A}}])})}catch(x){x instanceof Error?r.postMessage([d,{error:{id:f,error:x.message,target:A}}]):r.postMessage([d,{error:{id:f,error:"Unknown error occurred",target:A}}])}}else if("result"in u){let p=u.result,f=i.get(d);f&&(f(p.result),i.delete(d))}else if("error"in u){let p=u.error,f=i.get(d);f&&(f(Promise.reject(new Error(p.error))),i.delete(d))}else if("release"in u){let p=u.release,f=a.get(p.id);f&&(f.clear(),a.delete(p.id))}}),new Proxy({},{get(h,d){return(...u)=>{let p=Math.random();return new Promise((f,P)=>{i.set(p,A=>{A instanceof Promise?A.then(f,P):f(A)}),r.postMessage([p,{call:{id:d,args:u}}])})}}})}function B(r,e,t){let n=t||(0,_.source)("crann"),i=n.type!=="agent",a=m.forContext(i?"Core:RPC":"Agent:RPC"),s={postMessage:(o,c)=>{if(i){a.debug("Posting message from service worker:",{message:o,transferables:c});let[,S]=o,l=Ce(S);if(!l){a.warn("No target specified for RPC response in service worker");return}n.post({action:"rpc",payload:{message:o,transferables:c||[]}},l)}else{let S=n.getAgentInfo();if(!S){a.warn("No agent info found for posting message",{agentInfo:S});return}let l=b(S),[,C]=o;"call"in C&&(C.call.target=S==null?void 0:S.location),a.withTag(l).debug("Sending RPC message from agent:",{rpcPayload:C,message:o}),n.post({action:"rpc",payload:{message:o,transferables:c||[]}})}},addEventListener:(o,c)=>{n.on({rpc:(S,l)=>{try{if(!l)a.debug("RPC message received:",{message:S,event:o});else{let p=b(l);a.withTag(p).debug("RPC message received:",{message:S,event:o})}let{payload:C}=S,{message:h,transferables:d=[]}=C,u=new MessageEvent("message",{data:h,ports:d.filter(p=>p instanceof MessagePort)||[]});c(u)}catch(C){a.error("Failed to parse RPC message payload:",C)}}})},removeEventListener:()=>{},context:{isServiceWorker:i,agentInfo:i?void 0:n.getAgentInfo()}};return Z(s,r,e)}function Ce(r){if("result"in r)return r.result.target;if("error"in r)return r.error.target;if("call"in r)return r.call.target;if("release"in r)return r.release.target}var I=class I{constructor(e,t){this.config=e;this.instances=new Map;this.stateChangeListeners=[];this.instanceReadyListeners=[];this.storagePrefix="crann_";this.porter=(0,ee.source)("crann",{debug:!1});t!=null&&t.debug&&(U.setDebug(!0),m.setDebug(!0)),this.storagePrefix=(t==null?void 0:t.storagePrefix)??this.storagePrefix,this.logger=m.forContext("Core"),this.logger.log("Constructing Crann with new logger"),this.defaultInstanceState=this.initializeInstanceDefault(),this.defaultServiceState=this.serviceState=this.initializeServiceDefault(),this.hydrate(),this.logger.log("Crann constructed, setting initial message handlers"),this.porter.on({setState:(a,s)=>{if(!s){this.logger.warn("setState message heard from unknown agent");return}let o=b(s);this.logger.withTag(o).log("Setting state:",a),this.set(a.payload.state,s.id)}});let n=new Set;this.porter.onMessagesSet(a=>{if(!a){this.logger.error("Messages set but no agent info.",{info:a});return}if(this.logger.log("onMessagesSet received for agent:",{id:a.id,context:a.location.context,tabId:a.location.tabId,frameId:a.location.frameId,alreadyInitialized:n.has(a.id)}),n.has(a.id)){this.logger.log("Already sent initialState to agent, skipping:",a.id);return}n.add(a.id);let s=b(a);this.logger.withTag(s).log("Messages set received. Sending initial state.",{info:a});let o=this.get(a.id);this.porter.post({action:"initialState",payload:{state:o,info:a}},a.location),this.notifyInstanceReady(a.id,a)}),this.porter.onConnect(a=>{if(!a){this.logger.error("Agent connected but no agent info.",{info:a});return}let s=b(a);this.logger.withTag(s).log("Agent connected",{info:a}),this.addInstance(a.id,s),this.porter.onDisconnect(o=>{this.logger.withTag(b(o)).log("Agent disconnect heard. Connection type, context and location:",{info:o}),this.removeInstance(o.id)})});let i=this.extractActions(e);this.rpcEndpoint=B(this.get(),i,this.porter)}static getInstance(e,t){return I.instance?t!=null&&t.debug&&m.forContext("Core").log("Instance requested and already existed, returning"):I.instance=new I(e,t),I.instance}async addInstance(e,t){if(this.instances.has(e))this.logger.withTag(t).log("Instance was already registered, ignoring request from key");else{this.logger.withTag(t).log("Adding instance from agent key");let n={...this.defaultInstanceState};this.instances.set(e,n)}}async removeInstance(e){this.instances.has(e)?(this.logger.withTag(e).log("Remove instance requested"),this.instances.delete(e)):this.logger.withTag(e).log("Remove instance requested but it did not exist!")}async setServiceState(e){this.logger.log("Request to set service state with:",e);let t={...this.serviceState,...e};F(this.serviceState,t)?this.logger.log("New state seems to be the same as existing, skipping"):(this.logger.log("Confirmed new state was different than existing so proceeding to persist then notify all connected instances."),this.serviceState=t,await this.persist(e),this.notify(e))}async setInstanceState(e,t){this.logger.withTag(e).log("Request to update instance state, update:",t);let n=this.instances.get(e)||this.defaultInstanceState,i={...n,...t};F(n,i)?this.logger.withTag(e).log("Instance state update is not different, skipping update."):(this.logger.withTag(e).log("Instance state update is different, updating and notifying."),this.instances.set(e,i),this.notify(t,e))}async persist(e){this.logger.log("Persisting state");let t=!1;for(let n in e||this.serviceState){let a=this.config[n].persist||"none",s=e?e[n]:this.serviceState[n];switch(a){case"session":await O.default.storage.session.set({[this.storagePrefix+n]:s}),t=!0;break;case"local":await O.default.storage.local.set({[this.storagePrefix+n]:s}),t=!0;break;default:break}}t?this.logger.log("State was persisted"):this.logger.log("Nothing to persist")}async clear(){this.logger.log("Clearing state"),this.serviceState=this.defaultServiceState,this.instances.forEach((e,t)=>{this.instances.set(t,this.defaultInstanceState)}),await this.persist(),this.notify({})}subscribe(e){this.logger.log("Subscribing to state"),this.stateChangeListeners.push(e)}notify(e,t){let n=t?this.porter.getAgentById(t):void 0,i=t?this.get(t):this.get();this.stateChangeListeners.length>0&&(this.logger.log("Notifying state change listeners in source"),this.stateChangeListeners.forEach(a=>{a(i,e,n==null?void 0:n.info)})),t&&(n!=null&&n.info.location)?(this.logger.withTag(t).log("Notifying of state change."),this.porter.post({action:"stateUpdate",payload:{state:e}},n.info.location)):(this.logger.log("Notifying everyone"),this.instances.forEach((a,s)=>{this.porter.post({action:"stateUpdate",payload:{state:e}},s)}))}get(e){return e?{...this.serviceState,...this.instances.get(e)}:{...this.serviceState}}findInstance(e){let t=this.porter.getAgentByLocation(e);if(!t)return this.logger.log("Could not find agent for location:",{location:e}),null;for(let[n,i]of this.instances)if(n===t.info.id)return this.logger.log("Found instance for key:",n),n;return this.logger.log("Could not find instance for context and location:",{location:e}),null}queryAgents(e){return this.porter.queryAgents(e)}async set(e,t){let n={},i={};for(let a in e){let s=this.config[a];if(ve(s)){if(s.partition==="instance"){let o=a,c=e;n[o]=c[o]}else if(!s.partition||s.partition===k.Service){let o=a,c=e;i[o]=c[o]}}}t&&Object.keys(n).length>0&&(this.logger.withTag(t).log("Setting instance state:",n),this.setInstanceState(t,n)),Object.keys(i).length>0&&(this.logger.log("Setting service state:",i),this.setServiceState(i))}async hydrate(){this.logger.log("Hydrating state from storage.");let e=await O.default.storage.local.get(null),t=await O.default.storage.session.get(null),n={...e,...t},i={},a=!1;for(let s in n){let o=this.removePrefix(s);if(this.config.hasOwnProperty(o)){let c=n[o];i[o]=c,a=!0}}a?this.logger.log("Hydrated some items."):this.logger.log("No items found in storage."),this.serviceState={...this.defaultServiceState,...i}}removePrefix(e){return e.startsWith(this.storagePrefix)?e.replace(this.storagePrefix,""):e}initializeInstanceDefault(){let e={};return Object.keys(this.config).forEach(t=>{let n=this.config[t];L(n)&&n.partition==="instance"&&(e[t]=n.default)}),e}initializeServiceDefault(){let e={};return Object.keys(this.config).forEach(t=>{let n=this.config[t];L(n)&&(!n.partition||n.partition===k.Service)&&(e[t]=n.default)}),e}subscribeToInstanceReady(e){return this.logger.log("Subscribing to instance ready events"),this.instanceReadyListeners.push(e),this.instances.forEach((t,n)=>{let i=this.porter.getAgentById(n);i!=null&&i.info&&e(n,i.info)}),()=>{this.logger.log("Unsubscribing from instance ready events");let t=this.instanceReadyListeners.indexOf(e);t!==-1&&this.instanceReadyListeners.splice(t,1)}}notifyInstanceReady(e,t){if(this.instanceReadyListeners.length>0){let n=b(t);this.logger.withTag(n).log("Notifying instance ready listeners"),this.instanceReadyListeners.forEach(i=>{i(e,t)})}}extractActions(e){return Object.entries(e).filter(([t,n])=>q(n)).reduce((t,[n,i])=>{let a=i;return{...t,[n]:{handler:async(s,...o)=>{a.validate&&a.validate(...o);let c=await a.handler(s,...o);return c&&await this.set(c),c},validate:a.validate}}},{})}};I.instance=null,V([H],I.prototype,"setServiceState",1),V([H],I.prototype,"setInstanceState",1);var N=I;function te(r,e){let t=N.getInstance(r,e);return{get:t.get.bind(t),set:t.set.bind(t),subscribe:t.subscribe.bind(t),onInstanceReady:t.subscribeToInstanceReady.bind(t),findInstance:t.findInstance.bind(t),queryAgents:t.queryAgents.bind(t),clear:t.clear.bind(t)}}function ve(r){return r&&typeof r=="object"&&"default"in r}var ie=require("porter-source");var R={connected:!1},j=null;function z(r,e){let t=(e==null?void 0:e.debug)||!1,n=e==null?void 0:e.context;t&&m.setDebug(!0);let i=m.forContext("Agent"),a,s="unset",o=new Set;if(i.log("Initializing Crann Agent"+(n?` with context: ${n}`:"")),j)return i.log("We had an instance already, returning"),R.connected&&(i.log("Connect, calling onReady callback"),setTimeout(()=>{o.forEach(g=>g(R))},0)),j;i.log("No existing instance, creating a new one");let c=(0,ie.connect)({namespace:"crann",debug:!1});i.log("Porter connection created");let S=Object.entries(r).filter(([g,v])=>q(v)).reduce((g,[v,D])=>{let w=D;return{...g,[v]:{type:"action",handler:w.handler,validate:w.validate}}},{}),l=B(ne(r),S,c),C=!1;c.on({initialState:g=>{if(i.log("initialState received",{alreadyReceived:C,payload:g.payload}),C){i.log("Ignoring duplicate initialState message");return}C=!0,h=g.payload.state,a=g.payload.info,s=b(a),R={connected:!0,agent:a},i.setTag(s),i.log(`Initial state received and ${u.size} listeners notified`,{message:g}),o.forEach(v=>{i.log("Calling onReady callbacks"),v(R)}),u.forEach(v=>{v.callback(h)})},stateUpdate:g=>{d=g.payload.state,h={...h,...d},i.log("State updated:",{message:g,changes:d,_state:h}),d&&u.forEach(v=>{(v.keys===void 0||v.keys.some(w=>w in d))&&v.callback(d)})}}),i.log("Porter connected. Setting up state and listeners");let h=ne(r),d=null,u=new Set;i.log("Completed setup, returning instance");let p=()=>h,f=g=>{i.log("Calling post with setState",g),c.post({action:"setState",payload:{state:g}})},P=(g,v)=>{let D={keys:v,callback:g};return u.add(D),()=>{u.delete(D)}};return j={useCrann:g=>{let v=()=>p()[g],D=$=>f({[g]:$}),w=$=>{let W=v();return P(oe=>{if(g in oe){let Y=v(),se=p();$({current:Y,previous:W,state:se}),W=Y}},[g])};return[v(),D,w]},get:p,set:f,subscribe:P,getAgentInfo:()=>a,onReady:g=>(i.log("onReady callback added"),o.add(g),R.connected&&(i.log("calling onReady callback"),setTimeout(()=>{g(R)},0)),()=>o.delete(g)),callAction:async(g,...v)=>(i.log("Calling action",g,v),l[g](...v))},j}function ae(){return j!==null}function ne(r){let e={};return Object.keys(r).forEach(t=>{let n=r[t];L(n)&&(e[t]=n.default)}),e}var T=require("react");function re(r){return function(t){let{useCrann:n,get:i,set:a,subscribe:s}=(0,T.useMemo)(()=>z(r),[t]),o=(0,T.useCallback)(l=>{let[C,h]=(0,T.useState)(i()[l]),d=(0,T.useRef)(C);(0,T.useEffect)(()=>{d.current=C},[C]),(0,T.useEffect)(()=>(h(i()[l]),s(f=>{l in f&&h(f[l])},[l])),[l]);let u=(0,T.useCallback)(p=>{a({[l]:p})},[l]);return[C,u]},[i,a,s]),c=(0,T.useCallback)(()=>i(),[i]),S=(0,T.useCallback)(l=>{a(l)},[a]);return{useStateItem:o,getState:c,setState:S,useCrann:n}}}0&&(module.exports={Crann,Partition,Persistence,connect,connected,create,createCrannStateHook});
3
3
  //# sourceMappingURL=index.js.map