@farm.js/islx 0.0.5 → 0.0.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.7
4
+
5
+ Cloudflare Workers start, and sealed captured values name their key with 4 bytes.
6
+
7
+ - `@farm.js/islx/react/server` no longer generates random values while it loads. It made the
8
+ promise-name prefix with `crypto.getRandomValues` at module scope, which Workers reject
9
+ ("Disallowed operation called within global scope"), so no islx app started on Cloudflare
10
+ since promise props (0.0.4). The prefix is made at the first render now.
11
+ - An inline `"use server"` function's captured values are sealed with format version 2: the key id
12
+ is the first 4 bytes of the key's SHA-256 instead of the first byte. With one byte, a deploy that
13
+ changed keys without keeping the old one gave the new key the same id one time in 256, and
14
+ values from pages rendered before it failed as invalid (`400`) instead of stale (the page
15
+ reloads). An app that does not set `ISLX_ACTIONS_KEY` gets a new key on every build, so this hit
16
+ about one deploy in 256.
17
+ - Values sealed by 0.0.6 or earlier (format version 1) are stale: forms on pages open across the
18
+ upgrade reload once.
19
+
20
+ ## 0.0.6
21
+
22
+ Server action interceptor, early clicks on TanStack Start, library islands under pnpm.
23
+
24
+ - `setActionInterceptor((call, next) => …)` from `@farm.js/islx/client` wraps every server
25
+ action call, references decoded from props included. It sees the call when it is made
26
+ (`{ id, name, args }`) and `next` sends it in queue order, so a framework can unwrap what its
27
+ server side adds to a result (cache invalidations, a query's cache entry, a declared failure).
28
+ - TanStack Start: a click on a button, or a form submit, in an island before it hydrates is held
29
+ and replayed, as in a plain islx app (the pre-hydration script goes in the root route's head).
30
+ - A library's `"use client"` components (islands the Vite plugin makes in a bundled server build)
31
+ import islx's island runtime from the app instead of from the library's folder. Under pnpm the
32
+ library's folder in the store cannot see `@farm.js/islx`, so the server build failed to resolve
33
+ `@farm.js/islx/react`; now it gets the same copy the app's pages use.
34
+
3
35
  ## 0.0.5
4
36
 
5
37
  TanStack Start.
package/README.md CHANGED
@@ -13,7 +13,7 @@ islx started as [Farm.js](https://github.com/farming-labs/farm.js)'s islands mod
13
13
  replaces React Server Components' Flight payload. On a 300-page docs app it ships 26% less
14
14
  JavaScript, and a client navigation sends 2.5 KB instead of 12 KB.
15
15
 
16
- > Status: 0.0.5. React, Vite and TanStack Start are supported; other renderers and framework
16
+ > Status: 0.0.7. React, Vite and TanStack Start are supported; other renderers and framework
17
17
  > integrations follow. The wire format is in [SPEC.md](./SPEC.md), changes in
18
18
  > [CHANGELOG.md](./CHANGELOG.md).
19
19
 
@@ -186,7 +186,9 @@ export default defineConfig({ plugins: [tanstackStart(), react(), islxTanstackSt
186
186
  Routes, loaders and layouts render on the server. The browser loads islx and the `"use client"`
187
187
  components a page uses (the modules under `src/`), not the router, the route tree or loader
188
188
  data: Start's client entry becomes islx's, `<Scripts />` still loads it, and the router is not
189
- dehydrated. Links move between routes without loading a document. A loader's promise passed
189
+ dehydrated. A click on a button (or a form submit) in an island before it hydrates is held and
190
+ replayed. Links move between
191
+ routes without loading a document. A loader's promise passed
190
192
  unawaited to a client component streams in. Islands call Start server functions
191
193
  (`createServerFn`) and islx server actions (`"use server"`).
192
194
 
@@ -197,8 +199,6 @@ unawaited to a client component streams in. Islands call Start server functions
197
199
  - Client components run without a router in the browser: they cannot use `useRouter`,
198
200
  `useNavigate` or `<Link>` (render plain `<a>` links; the server renders `<Link>` in routes).
199
201
  - A library's `"use client"` components render as server HTML.
200
- - A click before the islands hydrate is not replayed yet (the `interaction` strategy hydrates on
201
- it, but the click itself is dropped).
202
202
  - Start sends a bot the whole page before any of it; people get it streamed.
203
203
  - Start needs Node 22.12 or later.
204
204
 
package/SPEC.md CHANGED
@@ -198,14 +198,17 @@ travel with the action, sealed, so the browser can neither read nor change them:
198
198
  sealed = base64url( version ‖ key id ‖ IV ‖ AES-256-GCM(ciphertext ‖ tag) )
199
199
  ```
200
200
 
201
- - `version` is `1`; `key id` is the first byte of SHA-256 of the key; the IV is 12 random bytes;
201
+ - `version` is `2` (one byte); `key id` is the first 4 bytes of SHA-256 of the key, big-endian;
202
+ the IV is 12 random bytes;
202
203
  - the additional data is `islx:action:<id>`, so values sealed for one action never open for
203
204
  another;
204
205
  - the plaintext is the captured values as a tagged JSON array.
205
206
 
206
- A value sealed with a key the server does not have is **stale** (the page reloads); anything that
207
- does not open is **invalid** (`400`). Servers may hold previous keys, so pages rendered before a
208
- key rotation keep working. Every server instance must share the keys.
207
+ A value sealed with a key the server does not have, or in another `version`, is **stale** (the
208
+ page reloads); anything that does not open is **invalid** (`400`). Servers may hold previous keys,
209
+ so pages rendered before a key rotation keep working. Every server instance must share the keys.
210
+ Version 1 named the key with one byte, so one key rotation in 256 made values from open pages
211
+ invalid instead of stale; its values are stale now.
209
212
 
210
213
  ## Integration model
211
214
 
@@ -151,6 +151,10 @@ function state() {
151
151
  function setActionNavigator(navigator) {
152
152
  state().navigator = navigator;
153
153
  }
154
+ /** Installs the framework's interceptor for server action calls (one; a later call replaces it). */
155
+ function setActionInterceptor(interceptor) {
156
+ state().interceptor = interceptor;
157
+ }
154
158
  /** Encodes an action passed back to the server as an argument. */
155
159
  function encodeFunction(value) {
156
160
  const reference = state().info.get(value);
@@ -252,7 +256,28 @@ function createReference(reference) {
252
256
  reference.redirect = void 0;
253
257
  return call(reference, args);
254
258
  };
255
- const result = client.queue.then(run).then((value) => {
259
+ const previous = client.queue;
260
+ const queued = () => previous.then(run);
261
+ const interceptor = client.interceptor;
262
+ let sent;
263
+ if (!interceptor) sent = queued();
264
+ else {
265
+ let stale = false;
266
+ const next = () => queued().then((value) => {
267
+ if (value === STALE) stale = true;
268
+ return value;
269
+ });
270
+ try {
271
+ sent = Promise.resolve(interceptor({
272
+ id,
273
+ name,
274
+ args: [...reference.bound, ...args]
275
+ }, next)).then((value) => stale ? STALE : value);
276
+ } catch (error) {
277
+ sent = Promise.reject(error);
278
+ }
279
+ }
280
+ const result = sent.then((value) => {
256
281
  if (value !== STALE) {
257
282
  const { redirect } = reference;
258
283
  emit("actionEnd", {
@@ -303,4 +328,4 @@ function createServerReference(id, token, name) {
303
328
  }
304
329
  return reference;
305
330
  }
306
- export { emit as a, createValueStore as c, defineClientHooks as i, getValueStore as l, decodeActionReference as n, hookEventName as o, setActionNavigator as r, setClientHooks as s, createServerReference as t, valueHeaders as u };
331
+ export { defineClientHooks as a, setClientHooks as c, valueHeaders as d, setActionNavigator as i, createValueStore as l, decodeActionReference as n, emit as o, setActionInterceptor as r, hookEventName as s, createServerReference as t, getValueStore as u };
@@ -24,10 +24,11 @@ import { a as encode, r as decode, t as EncodeError } from "./codec-C8to-RQW.mjs
24
24
  * Sealing is asynchronous. React's server renderer waits for it: a form whose action's values
25
25
  * are still sealing suspends, and renders once they are ready.
26
26
  *
27
- * sealed = base64url( version (1 byte) ‖ key id (1 byte) ‖ IV (12 bytes) ‖ ciphertext+tag )
27
+ * sealed = base64url( version (1 byte) ‖ key id (4 bytes) ‖ IV (12 bytes) ‖ ciphertext+tag )
28
28
  */
29
- const VERSION = 1;
29
+ const VERSION = 2;
30
30
  const IV_BYTES = 12;
31
+ const HEADER_BYTES = 5;
31
32
  const KEY_BYTES = 32;
32
33
  /** Thrown when a sealed value does not open: changed, made for another action, or stale. */
33
34
  var SealError = class extends Error {
@@ -58,7 +59,7 @@ function fromBase64url(text) {
58
59
  return null;
59
60
  }
60
61
  }
61
- /** A new random key, as base64url: what `FURROW_ACTIONS_KEY` holds. */
62
+ /** A new random key, as base64url: what `ISLX_ACTIONS_KEY` holds. */
62
63
  function generateActionKey() {
63
64
  return base64url(crypto.getRandomValues(new Uint8Array(KEY_BYTES)));
64
65
  }
@@ -67,10 +68,10 @@ async function importKey(secret) {
67
68
  if (!bytes || bytes.length !== KEY_BYTES) throw new TypeError("an actions key must be 32 random bytes as base64url; create one with generateActionKey() from @farm.js/islx/server");
68
69
  return crypto.subtle.importKey("raw", bytes, "AES-GCM", false, ["encrypt", "decrypt"]);
69
70
  }
70
- /** The key's id: its first byte of SHA-256, so a value names the key that sealed it. */
71
+ /** The key's id: the first 4 bytes of its SHA-256, so a value names the key that sealed it. */
71
72
  async function keyId(secret) {
72
73
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret.trim()));
73
- return new Uint8Array(digest)[0];
74
+ return new DataView(digest).getUint32(0);
74
75
  }
75
76
  /**
76
77
  * The keys values are sealed and opened with. `current` seals; `previous` keys still open
@@ -128,11 +129,11 @@ async function sealBound(keys, actionId, values, options = {}) {
128
129
  iv,
129
130
  additionalData: additionalData(actionId)
130
131
  }, keys.current, new TextEncoder().encode(json)));
131
- const sealed = new Uint8Array(14 + ciphertext.length);
132
+ const sealed = new Uint8Array(17 + ciphertext.length);
132
133
  sealed[0] = VERSION;
133
- sealed[1] = keys.currentId;
134
- sealed.set(iv, 2);
135
- sealed.set(ciphertext, 14);
134
+ new DataView(sealed.buffer).setUint32(1, keys.currentId);
135
+ sealed.set(iv, HEADER_BYTES);
136
+ sealed.set(ciphertext, 17);
136
137
  return base64url(sealed);
137
138
  }
138
139
  /**
@@ -141,17 +142,17 @@ async function sealBound(keys, actionId, values, options = {}) {
141
142
  */
142
143
  async function unsealBound(keys, actionId, sealed, options = {}) {
143
144
  const bytes = fromBase64url(sealed);
144
- if (!bytes || bytes.length < 30) throw new SealError("invalid", "sealed action values are malformed");
145
- if (bytes[0] !== VERSION) throw new SealError("stale", "sealed action values use another format version");
146
- const candidates = keys.byId.get(bytes[1]);
145
+ if (bytes && bytes.length > 0 && bytes[0] !== VERSION) throw new SealError("stale", "sealed action values use another format version");
146
+ if (!bytes || bytes.length < 33) throw new SealError("invalid", "sealed action values are malformed");
147
+ const candidates = keys.byId.get(new DataView(bytes.buffer).getUint32(1));
147
148
  if (!candidates) throw new SealError("stale", "sealed action values use a key this server does not have");
148
149
  let plaintext;
149
150
  for (const key of candidates) try {
150
151
  plaintext = await crypto.subtle.decrypt({
151
152
  name: "AES-GCM",
152
- iv: bytes.subarray(2, 14),
153
+ iv: bytes.subarray(HEADER_BYTES, 17),
153
154
  additionalData: additionalData(actionId)
154
- }, key, bytes.subarray(14));
155
+ }, key, bytes.subarray(17));
155
156
  break;
156
157
  } catch {}
157
158
  if (!plaintext) throw new SealError("invalid", "sealed action values do not open for this action");
@@ -1,6 +1,6 @@
1
1
  import { i as decodeTagged, n as NOT_A_VALUE, o as encodeLeaf } from "./codec-C8to-RQW.mjs";
2
2
  import { M as VALUE_SCRIPT_TYPE, c as CHUNK_SCRIPT_TYPE, k as TREE_SCRIPT_TYPE, p as HTML_ELEMENT } from "./wire-QEQWBr5s.mjs";
3
- import { l as getValueStore, t as createServerReference } from "./actions-8RQfrM-p.mjs";
3
+ import { t as createServerReference, u as getValueStore } from "./actions-C-IwUCKU.mjs";
4
4
  import { a as getIslandServerHook, o as isolateIslands, r as getClientReference } from "./island-DVOr-OUw.mjs";
5
5
  import * as React from "react";
6
6
  // src/react/tree.ts
@@ -177,6 +177,7 @@ function transformClientModule(options) {
177
177
  const RUNTIME_ID = "virtual:islx/runtime";
178
178
  /** @internal The browser entry, for a framework whose own entry imports it. */
179
179
  const ENTRY_ID = "virtual:islx/client-entry";
180
+ const LIBRARY_RUNTIME_ID = "virtual:islx/react-for-libraries";
180
181
  const CLIENT_HOOKS_ID = "virtual:islx/hooks-client";
181
182
  const SERVER_HOOKS_ID = "virtual:islx/hooks-server";
182
183
  const BROWSER_GLOBALS = /* @__PURE__ */ new Set([
@@ -366,6 +367,7 @@ function islx(options = {}, host = {}) {
366
367
  preamble = resolved.plugins.some((plugin) => plugin.name === "vite:react-virtual-preamble");
367
368
  },
368
369
  resolveId(id) {
370
+ if (id === LIBRARY_RUNTIME_ID) return this.resolve("@farm.js/islx/react", path.join(config.root, "package.json"), { skipSelf: true });
369
371
  if (id === RUNTIME_ID || id === "virtual:islx/client-entry" || id === CLIENT_HOOKS_ID || id === SERVER_HOOKS_ID) return `\0${id}`;
370
372
  },
371
373
  async load(id) {
@@ -425,7 +427,8 @@ function islx(options = {}, host = {}) {
425
427
  return JSON.stringify(relative);
426
428
  },
427
429
  sideEffects: [RUNTIME_ID],
428
- directive: clientDirective
430
+ directive: clientDirective,
431
+ ...library ? { runtime: LIBRARY_RUNTIME_ID } : {}
429
432
  });
430
433
  if (island) {
431
434
  if (!library) transformActions({
package/dist/client.d.mts CHANGED
@@ -10,8 +10,24 @@ interface ActionNavigator {
10
10
  /** A client navigation to `href`, as the action's redirect. */
11
11
  navigate?(href: string): Promise<void> | void;
12
12
  }
13
+ /** A call of a server action, as an interceptor sees it. */
14
+ interface ActionCall {
15
+ id: string;
16
+ /** The export (or inline function) name, when known. */
17
+ name?: string;
18
+ /** The arguments, after any bound with `.bind`. */
19
+ args: readonly unknown[];
20
+ }
21
+ /**
22
+ * Wraps every server action call: call `next` to make the request, and return what the caller
23
+ * gets. A framework uses it to unwrap what its own server side adds to a result (cache
24
+ * invalidations, a query's cache entry) or to turn a returned error back into one it throws.
25
+ */
26
+ type ActionInterceptor = (call: ActionCall, next: () => Promise<unknown>) => Promise<unknown>;
13
27
  /** Connects actions to the framework's router. */
14
28
  declare function setActionNavigator(navigator: ActionNavigator): void;
29
+ /** Installs the framework's interceptor for server action calls (one; a later call replaces it). */
30
+ declare function setActionInterceptor(interceptor: ActionInterceptor | undefined): void;
15
31
  /** Decodes an action in a result or props: `{"$":"a","v":id,"b"?:sealed}`. */
16
32
  declare function decodeActionReference(object: Record<string, unknown>): AnyFunction | undefined;
17
33
  /** The function that calls a server action; one per id and sealed values, so props keep identity. */
@@ -55,4 +71,4 @@ declare const INTERACTION_EVENT = "f:island-interaction";
55
71
  declare const REPLAYABLE_SELECTOR = "button,[role=\"button\"],input[type=\"button\"],input[type=\"submit\"],input[type=\"reset\"]";
56
72
  /** The inline script's source (no `<script>` tag): pass it to the page's head or bootstrap. */
57
73
  declare const PREHYDRATION_SCRIPT: string;
58
- export { type ActionNavigator, type ClientHookEvent, type ClientHookEvents, type ClientHooks, INTERACTION_EVENT, NAVIGATION_RENDER, type NavigateOptions, type NavigationOptions, PENDING_INTERACTIONS, PREHYDRATION_SCRIPT, REPLAYABLE_SELECTOR, type ValueStore, createServerReference, createValueStore, decodeActionReference, defineClientHooks, enhanceForms, getValueStore, hookEventName, navigate, setActionNavigator, setClientHooks, startNavigation, valueHeaders };
74
+ export { type ActionCall, type ActionInterceptor, type ActionNavigator, type ClientHookEvent, type ClientHookEvents, type ClientHooks, INTERACTION_EVENT, NAVIGATION_RENDER, type NavigateOptions, type NavigationOptions, PENDING_INTERACTIONS, PREHYDRATION_SCRIPT, REPLAYABLE_SELECTOR, type ValueStore, createServerReference, createValueStore, decodeActionReference, defineClientHooks, enhanceForms, getValueStore, hookEventName, navigate, setActionInterceptor, setActionNavigator, setClientHooks, startNavigation, valueHeaders };
package/dist/client.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as emit, c as createValueStore, i as defineClientHooks, l as getValueStore, n as decodeActionReference, o as hookEventName, r as setActionNavigator, s as setClientHooks, t as createServerReference, u as valueHeaders } from "./_chunks/actions-8RQfrM-p.mjs";
1
+ import { a as defineClientHooks, c as setClientHooks, d as valueHeaders, i as setActionNavigator, l as createValueStore, n as decodeActionReference, o as emit, r as setActionInterceptor, s as hookEventName, t as createServerReference, u as getValueStore } from "./_chunks/actions-C-IwUCKU.mjs";
2
2
  import { C as RENDER_HEADER, I as readPostedAction, L as stripRuntimeFields } from "./_chunks/wire-QEQWBr5s.mjs";
3
3
  import { i as REPLAYABLE_SELECTOR, n as PENDING_INTERACTIONS, r as PREHYDRATION_SCRIPT, t as INTERACTION_EVENT } from "./_chunks/prehydration-B1GLTLdk.mjs";
4
4
  // src/client/forms.ts
@@ -194,4 +194,4 @@ function navigate(href, options = {}) {
194
194
  else location.assign(href);
195
195
  return Promise.resolve();
196
196
  }
197
- export { INTERACTION_EVENT, NAVIGATION_RENDER, PENDING_INTERACTIONS, PREHYDRATION_SCRIPT, REPLAYABLE_SELECTOR, createServerReference, createValueStore, decodeActionReference, defineClientHooks, enhanceForms, getValueStore, hookEventName, navigate, setActionNavigator, setClientHooks, startNavigation, valueHeaders };
197
+ export { INTERACTION_EVENT, NAVIGATION_RENDER, PENDING_INTERACTIONS, PREHYDRATION_SCRIPT, REPLAYABLE_SELECTOR, createServerReference, createValueStore, decodeActionReference, defineClientHooks, enhanceForms, getValueStore, hookEventName, navigate, setActionInterceptor, setActionNavigator, setClientHooks, startNavigation, valueHeaders };
@@ -1,8 +1,8 @@
1
- import { a as emit, l as getValueStore, t as createServerReference, u as valueHeaders } from "../_chunks/actions-8RQfrM-p.mjs";
1
+ import { d as valueHeaders, o as emit, t as createServerReference, u as getValueStore } from "../_chunks/actions-C-IwUCKU.mjs";
2
2
  import { i as decodeTagged, n as NOT_A_VALUE, r as decode, u as setOwn } from "../_chunks/codec-C8to-RQW.mjs";
3
3
  import { F as getTag, f as FORM_STATE_SCRIPT_TYPE, h as ISLAND_ELEMENT, m as HYDRATED_ATTRIBUTE, w as SLOT_ELEMENT, x as PROPS_SCRIPT_TYPE, y as PROMISE_SCRIPT_TYPE } from "../_chunks/wire-QEQWBr5s.mjs";
4
4
  import { i as getContexts, o as isolateIslands, u as withinClientRoot } from "../_chunks/island-DVOr-OUw.mjs";
5
- import { i as createServerTreeLayout, n as PAYLOAD_ONLY_ATTRIBUTE, o as updateServerTrees, r as createServerTreeElement } from "../_chunks/tree-BUkGP4AR.mjs";
5
+ import { i as createServerTreeLayout, n as PAYLOAD_ONLY_ATTRIBUTE, o as updateServerTrees, r as createServerTreeElement } from "../_chunks/tree-C47p6khN.mjs";
6
6
  import { n as isIslandStrategy, r as scheduleHydration, t as ISLAND_STRATEGIES } from "../_chunks/schedule-CjoybS3b.mjs";
7
7
  import * as React from "react";
8
8
  import { hydrateRoot } from "react-dom/client";
@@ -1,19 +1,23 @@
1
1
  import { a as encode } from "../_chunks/codec-C8to-RQW.mjs";
2
2
  import { f as FORM_STATE_SCRIPT_TYPE } from "../_chunks/wire-QEQWBr5s.mjs";
3
3
  import { r as PREHYDRATION_SCRIPT } from "../_chunks/prehydration-B1GLTLdk.mjs";
4
- import { i as encodeActionReference } from "../_chunks/actions-BuS5jDi0.mjs";
4
+ import { i as encodeActionReference } from "../_chunks/actions-CvQ0-zjg.mjs";
5
5
  import { t as getBuildDefaults } from "../_chunks/build-COIcs7wm.mjs";
6
6
  import { a as parseHeldValues, n as addressValues, o as selectValuesToSend, r as escapeScriptJson } from "../_chunks/values-37UIzxM0.mjs";
7
7
  import { c as setIslandServerHook, i as getContexts, l as setRenderStateResolver, n as createIsland, o as isolateIslands, r as getClientReference, s as runtimeTagged, t as IslandsProvider, u as withinClientRoot } from "../_chunks/island-DVOr-OUw.mjs";
8
- import { a as renderServerTree, n as PAYLOAD_ONLY_ATTRIBUTE, t as Outlet } from "../_chunks/tree-BUkGP4AR.mjs";
8
+ import { a as renderServerTree, n as PAYLOAD_ONLY_ATTRIBUTE, t as Outlet } from "../_chunks/tree-C47p6khN.mjs";
9
9
  import * as React from "react";
10
10
  // src/react/server.ts
11
11
  /**
12
12
  * @farm.js/islx/react/server: rendering a React page with islands. Importing it gives islands the
13
13
  * server code they need (values, action references); `renderToStream` renders a page.
14
14
  */
15
- const PROMISE_PREFIX = `${Array.from(crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(2)), (part) => part.toString(36)).join("")}.`;
15
+ let promisePrefix;
16
16
  let promiseCount = 0;
17
+ function nextPromiseName() {
18
+ promisePrefix ??= `${Array.from(crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(2)), (part) => part.toString(36)).join("")}.`;
19
+ return promisePrefix + (promiseCount++).toString(36);
20
+ }
17
21
  function isThenable(value) {
18
22
  return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
19
23
  }
@@ -23,7 +27,7 @@ setIslandServerHook({
23
27
  const addressed = addressValues(props, { encodeSpecial: (value) => {
24
28
  if (value && typeof value === "object" && runtimeTagged.has(value)) return value;
25
29
  if (isThenable(value)) {
26
- const name = PROMISE_PREFIX + (promiseCount++).toString(36);
30
+ const name = nextPromiseName();
27
31
  promises.push({
28
32
  name,
29
33
  promise: value
package/dist/react.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import { n as createIsland, o as isolateIslands, t as IslandsProvider, u as withinClientRoot } from "./_chunks/island-DVOr-OUw.mjs";
2
- import { a as renderServerTree, t as Outlet } from "./_chunks/tree-BUkGP4AR.mjs";
2
+ import { a as renderServerTree, t as Outlet } from "./_chunks/tree-C47p6khN.mjs";
3
3
  export { IslandsProvider, Outlet, createIsland, isolateIslands, renderServerTree, withinClientRoot };
package/dist/server.d.mts CHANGED
@@ -71,15 +71,15 @@ declare class SealError extends Error {
71
71
  interface ActionKeys {
72
72
  /** The key new values are sealed with. */
73
73
  current: CryptoKey;
74
- /** Its id, written into each sealed value (0-255). */
74
+ /** Its id, written into each sealed value (a 32-bit unsigned integer). */
75
75
  currentId: number;
76
76
  /**
77
- * Every key that opens a value, by id: the current one and earlier ones still accepted. Ids are
78
- * one byte, so two keys can share one; each is tried.
77
+ * Every key that opens a value, by id: the current one and earlier ones still accepted. Two keys
78
+ * may still share an id; each is tried.
79
79
  */
80
80
  byId: ReadonlyMap<number, readonly CryptoKey[]>;
81
81
  }
82
- /** A new random key, as base64url: what `FURROW_ACTIONS_KEY` holds. */
82
+ /** A new random key, as base64url: what `ISLX_ACTIONS_KEY` holds. */
83
83
  declare function generateActionKey(): string;
84
84
  /**
85
85
  * The keys values are sealed and opened with. `current` seals; `previous` keys still open
package/dist/server.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as parseHeldValues, i as hashValue, n as addressValues, o as selectValuesToSend, r as escapeScriptJson, t as VALUE_MIN_LENGTH } from "./_chunks/values-37UIzxM0.mjs";
2
- import { a as getActionReference, c as resolveAction, d as generateActionKey, f as sealBound, i as encodeActionReference, l as SealError, n as configureActionKeys, o as registerInlineAction, p as unsealBound, r as decodeActionReference, s as registerServerReference, t as bindInlineAction, u as createActionKeys } from "./_chunks/actions-BuS5jDi0.mjs";
2
+ import { a as getActionReference, c as resolveAction, d as generateActionKey, f as sealBound, i as encodeActionReference, l as SealError, n as configureActionKeys, o as registerInlineAction, p as unsealBound, r as decodeActionReference, s as registerServerReference, t as bindInlineAction, u as createActionKeys } from "./_chunks/actions-CvQ0-zjg.mjs";
3
3
  import { a as encode, r as decode } from "./_chunks/codec-C8to-RQW.mjs";
4
4
  import { I as readPostedAction, L as stripRuntimeFields, N as attachmentField, n as ACTION_KEY_FIELD, o as BOUND_FIELD, r as ACTION_STALE_HEADER, s as CALL_FIELD, t as ACTION_HEADER } from "./_chunks/wire-QEQWBr5s.mjs";
5
5
  import { n as setBuildDefaults, t as getBuildDefaults } from "./_chunks/build-COIcs7wm.mjs";
@@ -1,4 +1,4 @@
1
- import { n as islx, t as ENTRY_ID } from "./_chunks/vite-DG7hBn8A.mjs";
1
+ import { n as islx, t as ENTRY_ID } from "./_chunks/vite-BTzYlEiY.mjs";
2
2
  import { normalizePath } from "vite";
3
3
  // src/tanstack-start/index.ts
4
4
  /**
@@ -30,7 +30,9 @@ export default createServerEntry({
30
30
  });
31
31
  `;
32
32
  const routerWrapper = (routerPath) => `import * as app from ${JSON.stringify(routerPath)};
33
+ import { PREHYDRATION_SCRIPT } from "@farm.js/islx/client";
33
34
  export * from ${JSON.stringify(routerPath)};
35
+ const islxHead = new WeakSet();
34
36
  export async function getRouter(...args) {
35
37
  const router = await app.getRouter(...args);
36
38
  const lifecycle = (router.serverSsrLifecycle ??= {});
@@ -38,6 +40,16 @@ export async function getRouter(...args) {
38
40
  (lifecycle.onServerSsrAttach ??= []).push((ssr) => {
39
41
  ssr.dehydrate = async () => ssr.disableHydration();
40
42
  });
43
+ const root = router.routeTree;
44
+ if (root && !islxHead.has(root)) {
45
+ islxHead.add(root);
46
+ const head = root.options.head;
47
+ const add = (own) => ({ ...own, scripts: [{ children: PREHYDRATION_SCRIPT }, ...(own?.scripts ?? [])] });
48
+ root.options.head = (context) => {
49
+ const own = head?.(context);
50
+ return own && typeof own.then === "function" ? own.then(add) : add(own);
51
+ };
52
+ }
41
53
  return router;
42
54
  }
43
55
  `;
package/dist/vite.mjs CHANGED
@@ -1 +1 @@
1
- export { t as ENTRY_ID, n as default, n as islx, r as resolveDirectives, i as serializeHooks } from "./_chunks/vite-DG7hBn8A.mjs";
1
+ export { t as ENTRY_ID, n as default, n as islx, r as resolveDirectives, i as serializeHooks } from "./_chunks/vite-BTzYlEiY.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farm.js/islx",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "A renderer-agnostic wire format for server-rendered islands: typed props, values sent once, server actions, and partial navigations.",
5
5
  "keywords": [
6
6
  "islands",