@astroscope/wormhole 1.0.0 → 1.1.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
@@ -209,6 +209,17 @@ import { open } from '@astroscope/wormhole/server';
209
209
  return open(myWormhole, { count: 0 }, () => next());
210
210
  ```
211
211
 
212
+ To open several wormholes at once, pass an array of `[wormhole, data]` pairs — each pair is type-checked against its own wormhole:
213
+
214
+ ```ts
215
+ return open([
216
+ [cartStore, cart],
217
+ [sessionStore, session],
218
+ ], () => next());
219
+ ```
220
+
221
+ Nested `open()` calls shadow only the wormholes they name; everything else stays visible.
222
+
212
223
  ### `useWormhole(wh)` <sub>React only</sub>
213
224
 
214
225
  React hook that reads the wormhole and re-renders on changes. Uses `useSyncExternalStore` internally.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { n as Wormhole, t as DeepReadonly } from "./types-0PQTD8gI.js";
1
+ import { n as Wormhole, t as DeepReadonly } from "./types-Q2gaO_N_.js";
2
2
 
3
3
  //#region src/define.d.ts
4
4
  /**
package/dist/index.js CHANGED
@@ -15,8 +15,9 @@ function defineWormhole(name) {
15
15
  get() {
16
16
  if (store !== void 0) return store;
17
17
  const getter = globalThis[key];
18
- if (typeof getter === "function") return getter();
19
- throw new Error(`wormhole "${name}" is not initialized`);
18
+ const value = typeof getter === "function" ? getter() : void 0;
19
+ if (value === void 0) throw new Error(`wormhole "${name}" is not initialized`);
20
+ return value;
20
21
  },
21
22
  set(data) {
22
23
  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`);
package/dist/index.js.map CHANGED
@@ -1 +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
+ {"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 const value = typeof getter === 'function' ? (getter() as DeepReadonly<T> | undefined) : undefined;\n\n if (value === undefined) {\n throw new Error(`wormhole \"${name}\" is not initialized`);\n }\n\n return value;\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;GACnC,MAAM,QAAQ,OAAO,WAAW,aAAc,OAAO,IAAoC,KAAA;GAEzF,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,aAAa,KAAK,qBAAqB;GAGzD,OAAO;EACT;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,4 +1,4 @@
1
- import { n as Wormhole, t as DeepReadonly } from "../types-0PQTD8gI.js";
1
+ import { n as Wormhole, t as DeepReadonly } from "../types-Q2gaO_N_.js";
2
2
 
3
3
  //#region src/react/index.d.ts
4
4
  declare function useWormhole<T>(wormhole: Wormhole<T>): DeepReadonly<T>;
package/dist/server.d.ts CHANGED
@@ -1,7 +1,24 @@
1
- import { n as Wormhole, t as DeepReadonly } from "./types-0PQTD8gI.js";
1
+ import { n as Wormhole, t as DeepReadonly } from "./types-Q2gaO_N_.js";
2
2
 
3
3
  //#region src/server.d.ts
4
- declare function open<T, R>(wh: Wormhole<T>, data: DeepReadonly<T>, fn: () => R): R;
4
+ /** One [wormhole, data] pair per element — data is checked against its own wormhole's T. */
5
+ type WormholeEntries<Ts extends readonly unknown[]> = { [K in keyof Ts]: readonly [Wormhole<Ts[K]>, DeepReadonly<Ts[K]>] };
6
+ /**
7
+ * Open a wormhole for the duration of `fn` — `wormhole.get()` returns `data`
8
+ * anywhere in the async execution rooted at `fn`.
9
+ */
10
+ declare function open<T, R>(wormhole: Wormhole<T>, data: DeepReadonly<T>, fn: () => R): R;
11
+ /**
12
+ * Open several wormholes at once for the duration of `fn`.
13
+ *
14
+ * ```typescript
15
+ * return open([
16
+ * [cartStore, cart],
17
+ * [sessionStore, session],
18
+ * ], () => next());
19
+ * ```
20
+ */
21
+ declare function open<Ts extends readonly unknown[], R>(entries: readonly [...WormholeEntries<Ts>], fn: () => R): R;
5
22
  //#endregion
6
23
  export { open };
7
24
  //# sourceMappingURL=server.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"mappings":";;;;KAIK,eAAA,gDACS,EAAA,aAAe,QAAA,CAAS,EAAA,CAAG,CAAA,IAAK,YAAA,CAAa,EAAA,CAAG,CAAA;;;;;iBAa9C,IAAA,OAAW,QAAA,EAAU,QAAA,CAAS,CAAA,GAAI,IAAA,EAAM,YAAA,CAAa,CAAA,GAAI,EAAA,QAAU,CAAA,GAAI,CAAA;;;;;;;;;;;iBAWvE,IAAA,mCAAuC,OAAA,eAAsB,eAAA,CAAgB,EAAA,IAAM,EAAA,QAAU,CAAA,GAAI,CAAA"}
package/dist/server.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  //#region src/server.ts
3
- const stores = /* @__PURE__ */ new Map();
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);
3
+ const als = globalThis[Symbol.for("@astroscope/wormhole/als")] ??= new AsyncLocalStorage();
4
+ function open(...args) {
5
+ const [entries, fn] = Array.isArray(args[0]) ? args : [[[args[0], args[1]]], args[2]];
6
+ const ctx = new Map(als.getStore());
7
+ for (const [wormhole, data] of entries) {
8
+ ctx.set(wormhole.key, data);
9
+ globalThis[wormhole.key] ??= () => als.getStore()?.get(wormhole.key);
9
10
  }
10
- globalThis[wh.key] = () => als.getStore();
11
- return als.run(data, fn);
11
+ return als.run(ctx, fn);
12
12
  }
13
13
  //#endregion
14
14
  export { open };
@@ -1 +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"}
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\n/** One [wormhole, data] pair per element — data is checked against its own wormhole's T. */\ntype WormholeEntries<Ts extends readonly unknown[]> = {\n [K in keyof Ts]: readonly [Wormhole<Ts[K]>, DeepReadonly<Ts[K]>];\n};\n\n// a single ALS holds an immutable map of all open wormholes; keyed on globalThis\n// so the vite-runner and native module instances share one context\nconst als: AsyncLocalStorage<ReadonlyMap<string, unknown>> = ((globalThis as any)[\n Symbol.for('@astroscope/wormhole/als')\n] ??= new AsyncLocalStorage());\n\n/**\n * Open a wormhole for the duration of `fn` — `wormhole.get()` returns `data`\n * anywhere in the async execution rooted at `fn`.\n */\nexport function open<T, R>(wormhole: Wormhole<T>, data: DeepReadonly<T>, fn: () => R): R;\n/**\n * Open several wormholes at once for the duration of `fn`.\n *\n * ```typescript\n * return open([\n * [cartStore, cart],\n * [sessionStore, session],\n * ], () => next());\n * ```\n */\nexport function open<Ts extends readonly unknown[], R>(entries: readonly [...WormholeEntries<Ts>], fn: () => R): R;\nexport function open(...args: unknown[]): unknown {\n const [entries, fn] = (Array.isArray(args[0]) ? args : [[[args[0], args[1]]], args[2]]) as [\n readonly (readonly [Wormhole<unknown>, unknown])[],\n () => unknown,\n ];\n\n const ctx = new Map(als.getStore());\n\n for (const [wormhole, data] of entries) {\n ctx.set(wormhole.key, data);\n\n (globalThis as any)[wormhole.key] ??= () => als.getStore()?.get(wormhole.key);\n }\n\n return als.run(ctx, fn);\n}\n"],"mappings":";;AAUA,MAAM,MAAwD,WAC5D,OAAO,IAAI,0BAA0B,OACjC,IAAI,kBAAkB;AAkB5B,SAAgB,KAAK,GAAG,MAA0B;CAChD,MAAM,CAAC,SAAS,MAAO,MAAM,QAAQ,KAAK,EAAE,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,KAAK,EAAE,CAAC,GAAG,KAAK,EAAE;CAKrF,MAAM,MAAM,IAAI,IAAI,IAAI,SAAS,CAAC;CAElC,KAAK,MAAM,CAAC,UAAU,SAAS,SAAS;EACtC,IAAI,IAAI,SAAS,KAAK,IAAI;EAE1B,WAAoB,SAAS,eAAe,IAAI,SAAS,CAAC,EAAE,IAAI,SAAS,GAAG;CAC9E;CAEA,OAAO,IAAI,IAAI,KAAK,EAAE;AACxB"}
@@ -15,7 +15,7 @@ type DeepReadonly<T> = T extends ((...args: never[]) => unknown) ? T : T extends
15
15
  * The stored value is exposed as deeply readonly — the only way to change it is `set()`
16
16
  * (client) or `open()` (server) with a new value, which keeps subscribers in sync.
17
17
  */
18
- interface Wormhole<T> {
18
+ interface Wormhole<in out T> {
19
19
  readonly name: string;
20
20
  readonly key: string;
21
21
  get(): DeepReadonly<T>;
@@ -28,4 +28,4 @@ interface Wormhole<T> {
28
28
  }
29
29
  //#endregion
30
30
  export { Wormhole as n, DeepReadonly as t };
31
- //# sourceMappingURL=types-0PQTD8gI.d.ts.map
31
+ //# sourceMappingURL=types-Q2gaO_N_.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-Q2gaO_N_.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;;;;;;;;;;UAaW,QAAA;EAAA,SACN,IAAA;EAAA,SACA,GAAA;EACT,GAAA,IAAO,YAAA,CAAa,CAAA;EAjBwB;;;;EAsB5C,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": "1.0.0",
3
+ "version": "1.1.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",
@@ -4,12 +4,20 @@ interface Props {
4
4
  }
5
5
 
6
6
  const { wormhole } = Astro.props;
7
- const data = wormhole.get();
7
+
8
+ let data: unknown;
9
+
10
+ try {
11
+ data = wormhole.get();
12
+ } catch {
13
+ data = undefined;
14
+ }
15
+
8
16
  const key = wormhole.key;
9
17
  ---
10
18
 
11
19
  {
12
- data ? (
20
+ data !== undefined ? (
13
21
  <script is:inline define:vars={{ data, key }}>
14
22
  globalThis[key] = function(){return data;}
15
23
  </script>
@@ -1 +0,0 @@
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"}