@astroscope/wormhole 0.2.3 → 1.0.0

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
@@ -22,6 +22,7 @@ Astro recommends [nanostores](https://docs.astro.build/en/recipes/sharing-state/
22
22
 
23
23
  - **No secrets in wormholes.** All wormhole data is serialized into an inline `<script>` tag and sent to the browser. Never store tokens, API keys, credentials, or any sensitive data in a wormhole.
24
24
  - **`set()` is client-only.** Calling `set()` does not work on the server. Use `open(wormhole, data, fn)` from `@astroscope/wormhole/server` to populate data in middleware.
25
+ - **Values are deeply readonly.** `get()`, `subscribe()` and `useWormhole()` expose the value as `DeepReadonly<T>` — in-place mutation would silently bypass subscribers (and on the server would corrupt the request-scoped store). The only way to update is `set()` with a new value: `wh.set({ ...wh.get(), count: 1 })`.
25
26
 
26
27
  ## Examples
27
28
 
@@ -180,12 +181,24 @@ const wh = defineWormhole<{ count: number }>('counter');
180
181
 
181
182
  | Method | Description |
182
183
  | ------------------ | --------------------------------------------------------------------------------------------------- |
183
- | `wh.get()` | Read current value (from `AsyncLocalStorage` on server, from `globalThis` or local store on client) |
184
- | `wh.set(data)` | Update value and notify all subscribers (client-side) |
184
+ | `wh.get()` | Read current value as `DeepReadonly<T>` (from `AsyncLocalStorage` on server, from `globalThis` or local store on client) |
185
+ | `wh.set(data)` | Replace value with a new one and notify all subscribers (client-side) |
185
186
  | `wh.subscribe(fn)` | Listen for changes, returns unsubscribe function |
186
187
  | `wh.name` | The wormhole name |
187
188
  | `wh.key` | The internal `globalThis` key |
188
189
 
190
+ ### `DeepReadonly<T>`
191
+
192
+ Exported helper type — recursively `readonly` version of `T`. Wormhole values are exposed with this type; use it in your own function signatures when passing wormhole data around:
193
+
194
+ ```ts
195
+ import type { DeepReadonly } from '@astroscope/wormhole';
196
+
197
+ function isWithinCart(cart: DeepReadonly<Cart>, id: string) {
198
+ return cart.items.some((item) => item.productId === id);
199
+ }
200
+ ```
201
+
189
202
  ### `open(wh, data, fn)` <sub>server only</sub>
190
203
 
191
204
  Runs `fn` with `data` available via `wh.get()` for the duration of the call. Uses `AsyncLocalStorage` for request isolation.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { W as Wormhole } from './types-hCfcC5bZ.js';
1
+ import { n as Wormhole, t as DeepReadonly } from "./types-0PQTD8gI.js";
2
2
 
3
+ //#region src/define.d.ts
3
4
  /**
4
5
  * Define a wormhole that transfers state from server middleware to client components.
5
6
  *
@@ -7,5 +8,6 @@ import { W as Wormhole } from './types-hCfcC5bZ.js';
7
8
  * Never store secrets (tokens, API keys, credentials) in a wormhole.
8
9
  */
9
10
  declare function defineWormhole<T>(name: string): Wormhole<T>;
10
-
11
- export { Wormhole, defineWormhole };
11
+ //#endregion
12
+ export { type DeepReadonly, type Wormhole, defineWormhole };
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/define.ts"],"mappings":";;;;;AAQA;;;;iBAAgB,cAAA,IAAkB,IAAA,WAAe,QAAQ,CAAC,CAAA"}
package/dist/index.js CHANGED
@@ -1,34 +1,37 @@
1
- // src/define.ts
1
+ //#region src/define.ts
2
+ /**
3
+ * Define a wormhole that transfers state from server middleware to client components.
4
+ *
5
+ * **Security:** wormhole data is serialized into the HTML and sent to the browser.
6
+ * Never store secrets (tokens, API keys, credentials) in a wormhole.
7
+ */
2
8
  function defineWormhole(name) {
3
- const listeners = /* @__PURE__ */ new Set();
4
- const key = `__wormhole_${name}__`;
5
- let store;
6
- return {
7
- name,
8
- key,
9
- get() {
10
- if (store !== void 0) return store;
11
- const getter = globalThis[key];
12
- if (typeof getter === "function") return getter();
13
- throw new Error(`wormhole "${name}" is not initialized`);
14
- },
15
- set(data) {
16
- if (typeof globalThis.window === "undefined") {
17
- throw new Error(
18
- `wormhole "${name}" set() cannot be called on the server as it is not request-scoped; use open(wormhole, data, fn) from "@astroscope/wormhole/server" instead`
19
- );
20
- }
21
- store = data;
22
- for (const fn of listeners) fn(data);
23
- },
24
- subscribe(fn) {
25
- listeners.add(fn);
26
- return () => {
27
- listeners.delete(fn);
28
- };
29
- }
30
- };
9
+ const listeners = /* @__PURE__ */ new Set();
10
+ const key = `__wormhole_${name}__`;
11
+ let store;
12
+ return {
13
+ name,
14
+ key,
15
+ get() {
16
+ if (store !== void 0) return store;
17
+ const getter = globalThis[key];
18
+ if (typeof getter === "function") return getter();
19
+ throw new Error(`wormhole "${name}" is not initialized`);
20
+ },
21
+ set(data) {
22
+ if (typeof globalThis.window === "undefined") throw new Error(`wormhole "${name}" set() cannot be called on the server as it is not request-scoped; use open(wormhole, data, fn) from "@astroscope/wormhole/server" instead`);
23
+ store = data;
24
+ for (const fn of listeners) fn(data);
25
+ },
26
+ subscribe(fn) {
27
+ listeners.add(fn);
28
+ return () => {
29
+ listeners.delete(fn);
30
+ };
31
+ }
32
+ };
31
33
  }
32
- export {
33
- defineWormhole
34
- };
34
+ //#endregion
35
+ export { defineWormhole };
36
+
37
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/define.ts"],"sourcesContent":["import type { DeepReadonly, Wormhole } from './types.js';\n\n/**\n * Define a wormhole that transfers state from server middleware to client components.\n *\n * **Security:** wormhole data is serialized into the HTML and sent to the browser.\n * Never store secrets (tokens, API keys, credentials) in a wormhole.\n */\nexport function defineWormhole<T>(name: string): Wormhole<T> {\n const listeners = new Set<(data: DeepReadonly<T>) => void>();\n const key = `__wormhole_${name}__`;\n let store: DeepReadonly<T> | undefined;\n\n return {\n name,\n key,\n\n get(): DeepReadonly<T> {\n if (store !== undefined) return store;\n\n const getter = (globalThis as any)[key];\n\n if (typeof getter === 'function') return getter() as DeepReadonly<T>;\n\n throw new Error(`wormhole \"${name}\" is not initialized`);\n },\n\n set(data: DeepReadonly<T>): void {\n if (typeof (globalThis as any).window === 'undefined') {\n throw new Error(\n `wormhole \"${name}\" set() cannot be called on the server as it is not request-scoped; use open(wormhole, data, fn) from \"@astroscope/wormhole/server\" instead`,\n );\n }\n\n store = data;\n\n for (const fn of listeners) fn(data);\n },\n\n subscribe(fn: (data: DeepReadonly<T>) => void): () => void {\n listeners.add(fn);\n\n return () => {\n listeners.delete(fn);\n };\n },\n };\n}\n"],"mappings":";;;;;;;AAQA,SAAgB,eAAkB,MAA2B;CAC3D,MAAM,4BAAY,IAAI,IAAqC;CAC3D,MAAM,MAAM,cAAc,KAAK;CAC/B,IAAI;CAEJ,OAAO;EACL;EACA;EAEA,MAAuB;GACrB,IAAI,UAAU,KAAA,GAAW,OAAO;GAEhC,MAAM,SAAU,WAAmB;GAEnC,IAAI,OAAO,WAAW,YAAY,OAAO,OAAO;GAEhD,MAAM,IAAI,MAAM,aAAa,KAAK,qBAAqB;EACzD;EAEA,IAAI,MAA6B;GAC/B,IAAI,OAAQ,WAAmB,WAAW,aACxC,MAAM,IAAI,MACR,aAAa,KAAK,4IACpB;GAGF,QAAQ;GAER,KAAK,MAAM,MAAM,WAAW,GAAG,IAAI;EACrC;EAEA,UAAU,IAAiD;GACzD,UAAU,IAAI,EAAE;GAEhB,aAAa;IACX,UAAU,OAAO,EAAE;GACrB;EACF;CACF;AACF"}
@@ -1,5 +1,7 @@
1
- import { W as Wormhole } from '../types-hCfcC5bZ.js';
2
-
3
- declare function useWormhole<T>(wormhole: Wormhole<T>): T;
1
+ import { n as Wormhole, t as DeepReadonly } from "../types-0PQTD8gI.js";
4
2
 
3
+ //#region src/react/index.d.ts
4
+ declare function useWormhole<T>(wormhole: Wormhole<T>): DeepReadonly<T>;
5
+ //#endregion
5
6
  export { useWormhole };
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/react/index.ts"],"mappings":";;;iBAGgB,WAAA,IAAe,QAAA,EAAU,QAAA,CAAS,CAAA,IAAK,YAAA,CAAa,CAAA"}
@@ -1,8 +1,9 @@
1
- // src/react/index.ts
2
1
  import { useSyncExternalStore } from "react";
2
+ //#region src/react/index.ts
3
3
  function useWormhole(wormhole) {
4
- return useSyncExternalStore(wormhole.subscribe, wormhole.get, wormhole.get);
4
+ return useSyncExternalStore(wormhole.subscribe, wormhole.get, wormhole.get);
5
5
  }
6
- export {
7
- useWormhole
8
- };
6
+ //#endregion
7
+ export { useWormhole };
8
+
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/react/index.ts"],"sourcesContent":["import { useSyncExternalStore } from 'react';\nimport type { DeepReadonly, Wormhole } from '../types.js';\n\nexport function useWormhole<T>(wormhole: Wormhole<T>): DeepReadonly<T> {\n return useSyncExternalStore(wormhole.subscribe, wormhole.get, wormhole.get);\n}\n"],"mappings":";;AAGA,SAAgB,YAAe,UAAwC;CACrE,OAAO,qBAAqB,SAAS,WAAW,SAAS,KAAK,SAAS,GAAG;AAC5E"}
package/dist/server.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- import { W as Wormhole } from './types-hCfcC5bZ.js';
2
-
3
- declare function open<T, R>(wh: Wormhole<T>, data: T, fn: () => R): R;
1
+ import { n as Wormhole, t as DeepReadonly } from "./types-0PQTD8gI.js";
4
2
 
3
+ //#region src/server.d.ts
4
+ declare function open<T, R>(wh: Wormhole<T>, data: DeepReadonly<T>, fn: () => R): R;
5
+ //#endregion
5
6
  export { open };
7
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"mappings":";;;iBAKgB,IAAA,OAAW,EAAA,EAAI,QAAA,CAAS,CAAA,GAAI,IAAA,EAAM,YAAA,CAAa,CAAA,GAAI,EAAA,QAAU,CAAA,GAAI,CAAA"}
package/dist/server.js CHANGED
@@ -1,15 +1,16 @@
1
- // src/server.ts
2
- import { AsyncLocalStorage } from "async_hooks";
3
- var stores = /* @__PURE__ */ new Map();
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ //#region src/server.ts
3
+ const stores = /* @__PURE__ */ new Map();
4
4
  function open(wh, data, fn) {
5
- let als = stores.get(wh.key);
6
- if (!als) {
7
- als = new AsyncLocalStorage();
8
- stores.set(wh.key, als);
9
- }
10
- globalThis[wh.key] = () => als.getStore();
11
- return als.run(data, fn);
5
+ let als = stores.get(wh.key);
6
+ if (!als) {
7
+ als = new AsyncLocalStorage();
8
+ stores.set(wh.key, als);
9
+ }
10
+ globalThis[wh.key] = () => als.getStore();
11
+ return als.run(data, fn);
12
12
  }
13
- export {
14
- open
15
- };
13
+ //#endregion
14
+ export { open };
15
+
16
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport type { DeepReadonly, Wormhole } from './types.js';\n\nconst stores = new Map<string, AsyncLocalStorage<unknown>>();\n\nexport function open<T, R>(wh: Wormhole<T>, data: DeepReadonly<T>, fn: () => R): R {\n let als = stores.get(wh.key) as AsyncLocalStorage<DeepReadonly<T>> | undefined;\n\n if (!als) {\n als = new AsyncLocalStorage<DeepReadonly<T>>();\n stores.set(wh.key, als as AsyncLocalStorage<unknown>);\n }\n\n (globalThis as any)[wh.key] = () => als!.getStore();\n\n return als.run(data, fn);\n}\n"],"mappings":";;AAGA,MAAM,yBAAS,IAAI,IAAwC;AAE3D,SAAgB,KAAW,IAAiB,MAAuB,IAAgB;CACjF,IAAI,MAAM,OAAO,IAAI,GAAG,GAAG;CAE3B,IAAI,CAAC,KAAK;EACR,MAAM,IAAI,kBAAmC;EAC7C,OAAO,IAAI,GAAG,KAAK,GAAiC;CACtD;CAEA,WAAoB,GAAG,aAAa,IAAK,SAAS;CAElD,OAAO,IAAI,IAAI,MAAM,EAAE;AACzB"}
@@ -0,0 +1,31 @@
1
+ //#region src/types.d.ts
2
+ /**
3
+ * Recursively marks all properties of `T` as `readonly`.
4
+ *
5
+ * Wormhole data is JSON-serializable by design (it is inlined into a `<script>` tag),
6
+ * so only plain objects, arrays, and primitives need to be handled.
7
+ */
8
+ type DeepReadonly<T> = T extends ((...args: never[]) => unknown) ? T : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T;
9
+ /**
10
+ * A wormhole transfers state from server middleware to client-side components.
11
+ *
12
+ * **Security:** wormhole data is serialized into an inline `<script>` tag and sent to the browser.
13
+ * Never store secrets (tokens, API keys, credentials) in a wormhole.
14
+ *
15
+ * The stored value is exposed as deeply readonly — the only way to change it is `set()`
16
+ * (client) or `open()` (server) with a new value, which keeps subscribers in sync.
17
+ */
18
+ interface Wormhole<T> {
19
+ readonly name: string;
20
+ readonly key: string;
21
+ get(): DeepReadonly<T>;
22
+ /**
23
+ * Update the wormhole value on the **client only**.
24
+ * Throws on the server — use `open(wormhole, data, fn)` from `@astroscope/wormhole/server` instead.
25
+ */
26
+ set(data: DeepReadonly<T>): void;
27
+ subscribe(fn: (data: DeepReadonly<T>) => void): () => void;
28
+ }
29
+ //#endregion
30
+ export { Wormhole as n, DeepReadonly as t };
31
+ //# sourceMappingURL=types-0PQTD8gI.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-0PQTD8gI.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;AAMA;;;;;KAAY,YAAA,MAAkB,CAAA,cAAc,IAAA,yBACxC,CAAA,GACA,CAAA,yCACyB,CAAA,GAAI,YAAA,CAAa,CAAA,CAAE,CAAA,OAC1C,CAAA;;;;;;;;;;UAWW,QAAA;EAAA,SACN,IAAA;EAAA,SACA,GAAA;EACT,GAAA,IAAO,YAAA,CAAa,CAAA;EAfwB;;;;EAoB5C,GAAA,CAAI,IAAA,EAAM,YAAA,CAAa,CAAA;EACvB,SAAA,CAAU,EAAA,GAAK,IAAA,EAAM,YAAA,CAAa,CAAA;AAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astroscope/wormhole",
3
- "version": "0.2.3",
3
+ "version": "1.0.0",
4
4
  "description": "Share dynamic server data with React islands and client scripts — typed, reactive",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -50,14 +50,15 @@
50
50
  },
51
51
  "homepage": "https://github.com/entwico/astroscope/tree/main/packages/wormhole#readme",
52
52
  "devDependencies": {
53
- "@types/react": "^19.2.14",
54
- "astro": "^6.1.8",
55
- "react": "^19.2.5",
56
- "tsup": "^8.5.1",
57
- "typescript": "^5.9.3"
53
+ "@types/node": "^26.0.1",
54
+ "@types/react": "^19.2.17",
55
+ "astro": "^7.0.3",
56
+ "react": "^19.2.7",
57
+ "tsdown": "^0.22.3",
58
+ "typescript": "^6.0.3"
58
59
  },
59
60
  "peerDependencies": {
60
- "astro": "^6.0.0",
61
+ "astro": "^7.0.0",
61
62
  "react": "^18.0.0 || ^19.0.0"
62
63
  },
63
64
  "peerDependenciesMeta": {
@@ -66,9 +67,9 @@
66
67
  }
67
68
  },
68
69
  "scripts": {
69
- "build": "tsup src/index.ts src/server.ts src/react/index.ts --format esm --dts",
70
+ "build": "tsdown src/index.ts src/server.ts src/react/index.ts --format esm --dts --no-fixed-extension",
70
71
  "typecheck": "tsc --noEmit",
71
- "lint": "eslint src",
72
+ "lint": "eslint src --cache",
72
73
  "lint:fix": "eslint src --fix"
73
74
  }
74
75
  }
@@ -1,19 +0,0 @@
1
- /**
2
- * A wormhole transfers state from server middleware to client-side components.
3
- *
4
- * **Security:** wormhole data is serialized into an inline `<script>` tag and sent to the browser.
5
- * Never store secrets (tokens, API keys, credentials) in a wormhole.
6
- */
7
- interface Wormhole<T> {
8
- readonly name: string;
9
- readonly key: string;
10
- get(): T;
11
- /**
12
- * Update the wormhole value on the **client only**.
13
- * Throws on the server — use `open(wormhole, data, fn)` from `@astroscope/wormhole/server` instead.
14
- */
15
- set(data: T): void;
16
- subscribe(fn: (data: T) => void): () => void;
17
- }
18
-
19
- export type { Wormhole as W };