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 +151 -61
- package/dist/cjs/index.js +2 -2
- package/dist/cjs/index.js.map +4 -4
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +4 -4
- package/dist/types/crann.d.ts +14 -12
- package/dist/types/crannAgent.d.ts +2 -2
- package/dist/types/model/crann.model.d.ts +41 -17
- package/dist/types/rpc/adapter.d.ts +3 -0
- package/dist/types/rpc/encoding/basic.d.ts +2 -0
- package/dist/types/rpc/encoding/index.d.ts +1 -0
- package/dist/types/rpc/endpoint.d.ts +39 -0
- package/dist/types/rpc/memory.d.ts +16 -0
- package/dist/types/rpc/types.d.ts +102 -0
- package/dist/types/utils/agent.d.ts +10 -0
- package/dist/types/utils/logger.d.ts +43 -0
- package/package.json +4 -7
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
Crann: Effortless State Synchronization for Web Extensions
|
|
2
|
+
|
|
1
3
|

|
|
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
|
-
- [
|
|
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
|
-
##
|
|
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
|
-

|
|
50
|
-
_Traditional message passing requires complex, bidirectional communication between all parts._
|
|
51
|
-
|
|
52
|
-

|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
286
|
-
crann.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
//
|
|
293
|
-
crann.
|
|
294
|
-
|
|
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
|
-
//
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
374
|
+
#### Key Benefits of RPC Actions
|
|
316
375
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+

|
|
553
|
+
_Traditional message passing requires complex, bidirectional communication between all parts._
|
|
554
|
+
|
|
555
|
+

|
|
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
|
|
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
|