@farm.js/islx 0.0.1 → 0.0.2

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.2
4
+
5
+ Streaming and hydration fixes.
6
+
7
+ - Islands inside Suspense content that streams in after the browser entry has run now hydrate.
8
+ While the document streams, the runtime watches it: an island hydrates once React moves it out
9
+ of its hidden staging `<div>`, an island React removes (a replaced fallback) is released, and
10
+ watching stops once the document is parsed and every staged boundary is revealed. React 19.2
11
+ can reveal boundaries after parsing ends.
12
+ - Islands that share a value decode it once and share the result. Props and values without tags
13
+ skip decoding, and decoded objects are built with plain assignment (`__proto__` stays data).
14
+
3
15
  ## 0.0.1
4
16
 
5
17
  The first release: the islx wire format, server actions, React islands, and the Vite plugin.
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.1, the first release. React and Vite are supported; other renderers and framework
16
+ > Status: 0.0.2. React and Vite 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
 
@@ -1,5 +1,5 @@
1
1
  import { A as VALUE_SCRIPT_TYPE, N as getTag, O as VALUES_HEADER, j as attachmentField, k as VALUE_KEY_PATTERN, o as BOUND_FIELD, s as CALL_FIELD, t as ACTION_HEADER } from "./wire-CXOodE06.mjs";
2
- import { a as encode, r as decode } from "./codec-BTDkzzPP.mjs";
2
+ import { a as encode, r as decode, u as setOwn } from "./codec-DqfVYKsx.mjs";
3
3
  // src/client/value-store.ts
4
4
  /**
5
5
  * The browser's store of values sent once. It keeps every value it has read, parses each once,
@@ -21,7 +21,8 @@ function createValueStore() {
21
21
  if (entry.state === "resolving") throw new Error(`islx value ${key} refers to itself`);
22
22
  entry.state = "resolving";
23
23
  try {
24
- entry.value = resolve(JSON.parse(entry.json));
24
+ const parsed = JSON.parse(entry.json);
25
+ entry.value = entry.json.includes(`"\$"`) ? resolve(parsed) : parsed;
25
26
  } catch (error) {
26
27
  entry.state = "raw";
27
28
  throw error;
@@ -44,12 +45,7 @@ function createValueStore() {
44
45
  };
45
46
  const resolveEntries = (object) => {
46
47
  const resolved = {};
47
- for (const key of Object.keys(object)) Object.defineProperty(resolved, key, {
48
- value: resolve(object[key]),
49
- enumerable: true,
50
- writable: true,
51
- configurable: true
52
- });
48
+ for (const key of Object.keys(object)) setOwn(resolved, key, resolve(object[key]));
53
49
  return resolved;
54
50
  };
55
51
  return {
@@ -1,5 +1,5 @@
1
1
  import { M as formFieldName, O as VALUES_HEADER, d as FORM_REF_PREFIX, k as VALUE_KEY_PATTERN } from "./wire-CXOodE06.mjs";
2
- import { a as encode, c as needsObjectTag, o as encodeLeaf, r as decode, t as EncodeError } from "./codec-BTDkzzPP.mjs";
2
+ import { a as encode, c as needsObjectTag, o as encodeLeaf, r as decode, t as EncodeError } from "./codec-DqfVYKsx.mjs";
3
3
  import { t as getBuildDefaults } from "./build-COIcs7wm.mjs";
4
4
  // src/server/values.ts
5
5
  /**
@@ -88,13 +88,21 @@ const NOT_A_VALUE = Symbol.for("islx.codec.not-a-value");
88
88
  /** A plain object from entries; `defineProperty`, so a "__proto__" key stays an ordinary key. */
89
89
  function plainObject(entries) {
90
90
  const object = {};
91
- for (const [key, value] of entries) Object.defineProperty(object, key, {
91
+ for (const [key, value] of entries) setOwn(object, key, value);
92
+ return object;
93
+ }
94
+ /**
95
+ * Sets an own property. Plain assignment keeps the object fast; only a `"__proto__"` key needs
96
+ * `defineProperty`, which assignment would turn into a prototype change.
97
+ */
98
+ function setOwn(object, key, value) {
99
+ if (key === "__proto__") Object.defineProperty(object, key, {
92
100
  value,
93
101
  enumerable: true,
94
102
  writable: true,
95
103
  configurable: true
96
104
  });
97
- return object;
105
+ else object[key] = value;
98
106
  }
99
107
  /**
100
108
  * The value a tagged object stands for, decoding children (a `Map`'s or `Set`'s entries, an `o`
@@ -268,4 +276,4 @@ function decode(value, options = {}) {
268
276
  };
269
277
  return visit(value);
270
278
  }
271
- export { encode as a, needsObjectTag as c, toBase64 as d, decodeTagged as i, plainObject as l, NOT_A_VALUE as n, encodeLeaf as o, decode as r, fromBase64 as s, EncodeError as t, tagged as u };
279
+ export { encode as a, needsObjectTag as c, tagged as d, toBase64 as f, decodeTagged as i, plainObject as l, NOT_A_VALUE as n, encodeLeaf as o, decode as r, fromBase64 as s, EncodeError as t, setOwn as u };
@@ -1,6 +1,6 @@
1
- import { i as decodeTagged, n as NOT_A_VALUE, o as encodeLeaf } from "./codec-BTDkzzPP.mjs";
1
+ import { i as decodeTagged, n as NOT_A_VALUE, o as encodeLeaf } from "./codec-DqfVYKsx.mjs";
2
2
  import { A as VALUE_SCRIPT_TYPE, D as TREE_SCRIPT_TYPE, S as SLOT_ELEMENT, c as CHUNK_SCRIPT_TYPE, h as ISLAND_ELEMENT, p as HTML_ELEMENT, y as PROPS_SCRIPT_TYPE } from "./wire-CXOodE06.mjs";
3
- import { l as getValueStore, t as createServerReference } from "./actions-CA_K-In7.mjs";
3
+ import { l as getValueStore, t as createServerReference } from "./actions-2sTj0A2T.mjs";
4
4
  import { t as getBuildDefaults } from "./build-COIcs7wm.mjs";
5
5
  import * as React from "react";
6
6
  import * as ReactDOM from "react-dom";
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-CA_K-In7.mjs";
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-2sTj0A2T.mjs";
2
2
  import { F as stripRuntimeFields, P as readPostedAction, x as RENDER_HEADER } from "./_chunks/wire-CXOodE06.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
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import { A as VALUE_SCRIPT_TYPE, C as SLOT_TAG, D as TREE_SCRIPT_TYPE, E as TAG_VALUE, F as stripRuntimeFields, M as formFieldName, N as getTag, O as VALUES_HEADER, P as readPostedAction, S as SLOT_ELEMENT, T as TAG_BOUND, _ as MAX_HELD_VALUES, a as BLOB_TAG, b as REFERENCE_TAG, c as CHUNK_SCRIPT_TYPE, d as FORM_REF_PREFIX, f as FORM_STATE_SCRIPT_TYPE, g as LAYOUTS_HEADER, h as ISLAND_ELEMENT, i as ACTION_TAG, j as attachmentField, k as VALUE_KEY_PATTERN, l as FORM_DATA_TAG, m as HYDRATED_ATTRIBUTE, n as ACTION_KEY_FIELD, o as BOUND_FIELD, p as HTML_ELEMENT, r as ACTION_STALE_HEADER, s as CALL_FIELD, t as ACTION_HEADER, u as FORM_FIELD_PREFIX, v as OBJECT_TAG, w as TAG, x as RENDER_HEADER, y as PROPS_SCRIPT_TYPE } from "./_chunks/wire-CXOodE06.mjs";
2
- import { a as encode, c as needsObjectTag, d as toBase64, i as decodeTagged, l as plainObject, n as NOT_A_VALUE, o as encodeLeaf, r as decode, s as fromBase64, t as EncodeError, u as tagged } from "./_chunks/codec-BTDkzzPP.mjs";
2
+ import { a as encode, c as needsObjectTag, d as tagged, f as toBase64, i as decodeTagged, l as plainObject, n as NOT_A_VALUE, o as encodeLeaf, r as decode, s as fromBase64, t as EncodeError } from "./_chunks/codec-DqfVYKsx.mjs";
3
3
  export { ACTION_HEADER, ACTION_KEY_FIELD, ACTION_STALE_HEADER, ACTION_TAG, BLOB_TAG, BOUND_FIELD, CALL_FIELD, CHUNK_SCRIPT_TYPE, EncodeError, FORM_DATA_TAG, FORM_FIELD_PREFIX, FORM_REF_PREFIX, FORM_STATE_SCRIPT_TYPE, HTML_ELEMENT, HYDRATED_ATTRIBUTE, ISLAND_ELEMENT, LAYOUTS_HEADER, MAX_HELD_VALUES, NOT_A_VALUE, OBJECT_TAG, PROPS_SCRIPT_TYPE, REFERENCE_TAG, RENDER_HEADER, SLOT_ELEMENT, SLOT_TAG, TAG, TAG_BOUND, TAG_VALUE, TREE_SCRIPT_TYPE, VALUES_HEADER, VALUE_KEY_PATTERN, VALUE_SCRIPT_TYPE, attachmentField, decode, decodeTagged, encode, encodeLeaf, formFieldName, fromBase64, getTag, needsObjectTag, plainObject, readPostedAction, stripRuntimeFields, tagged, toBase64 };
@@ -1,7 +1,7 @@
1
- import { a as emit, l as getValueStore, t as createServerReference, u as valueHeaders } from "../_chunks/actions-CA_K-In7.mjs";
2
- import { i as decodeTagged, n as NOT_A_VALUE, r as decode } from "../_chunks/codec-BTDkzzPP.mjs";
1
+ import { a as emit, l as getValueStore, t as createServerReference, u as valueHeaders } from "../_chunks/actions-2sTj0A2T.mjs";
2
+ import { i as decodeTagged, n as NOT_A_VALUE, r as decode, u as setOwn } from "../_chunks/codec-DqfVYKsx.mjs";
3
3
  import { N as getTag, S as SLOT_ELEMENT, f as FORM_STATE_SCRIPT_TYPE, h as ISLAND_ELEMENT, m as HYDRATED_ATTRIBUTE, y as PROPS_SCRIPT_TYPE } from "../_chunks/wire-CXOodE06.mjs";
4
- import { c as getContexts, i as updateServerTrees, l as isolateIslands, n as createServerTreeElement, p as withinClientRoot, t as PAYLOAD_ONLY_ATTRIBUTE } from "../_chunks/tree-BEV73f_8.mjs";
4
+ import { c as getContexts, i as updateServerTrees, l as isolateIslands, n as createServerTreeElement, p as withinClientRoot, t as PAYLOAD_ONLY_ATTRIBUTE } from "../_chunks/tree-BDB5VEao.mjs";
5
5
  import { n as isIslandStrategy, r as scheduleHydration, t as ISLAND_STRATEGIES } from "../_chunks/schedule-BFoGKRrU.mjs";
6
6
  import * as React from "react";
7
7
  import { hydrateRoot } from "react-dom/client";
@@ -31,8 +31,26 @@ function readFormState(document) {
31
31
  return;
32
32
  }
33
33
  }
34
+ /**
35
+ * Revived values, by the object the value store resolved. Islands that share a value (one sent
36
+ * once for several islands) then share the revived object instead of walking it again. A value
37
+ * with a slot is not kept: a slot's HTML belongs to one island.
38
+ */
39
+ const revivedValues = /* @__PURE__ */ new WeakMap();
40
+ let slotsRevived = 0;
34
41
  /** Turns travelled props back into values: slots keep their server HTML, actions become callable. */
35
42
  function revive(value, container) {
43
+ if (value && typeof value === "object") {
44
+ const cached = revivedValues.get(value);
45
+ if (cached !== void 0) return cached;
46
+ const slotsBefore = slotsRevived;
47
+ const revived = reviveUncached(value, container);
48
+ if (slotsRevived === slotsBefore) revivedValues.set(value, revived);
49
+ return revived;
50
+ }
51
+ return value;
52
+ }
53
+ function reviveUncached(value, container) {
36
54
  if (Array.isArray(value)) {
37
55
  let changed = false;
38
56
  const revived = value.map((entry) => {
@@ -42,7 +60,6 @@ function revive(value, container) {
42
60
  });
43
61
  return changed ? revived : value;
44
62
  }
45
- if (!value || typeof value !== "object") return value;
46
63
  const object = value;
47
64
  const tag = getTag(object);
48
65
  if (tag === "a" && typeof object["v"] === "string") {
@@ -50,6 +67,7 @@ function revive(value, container) {
50
67
  return createServerReference(object["v"], typeof token === "string" ? token : void 0);
51
68
  }
52
69
  if (tag === "s" && typeof object["v"] === "string") {
70
+ slotsRevived++;
53
71
  const name = object["v"];
54
72
  let html = "";
55
73
  for (const slot of container.querySelectorAll(SLOT_ELEMENT)) if (slot.getAttribute("name") === name) {
@@ -73,16 +91,16 @@ function revive(value, container) {
73
91
  const revived = {};
74
92
  for (const [key, entry] of Object.entries(object)) {
75
93
  const next = revive(entry, container);
76
- Object.defineProperty(revived, key, {
77
- value: next,
78
- enumerable: true,
79
- writable: true,
80
- configurable: true
81
- });
94
+ setOwn(revived, key, next);
82
95
  if (next !== entry) changed = true;
83
96
  }
84
97
  return changed ? revived : value;
85
98
  }
99
+ /**
100
+ * React streams Suspense content that resolves late into a hidden `<div id="S:n">`, then moves it
101
+ * into place. Its islands hydrate once moved, not while staged.
102
+ */
103
+ const STREAM_STAGING = "div[hidden][id^=\"S:\"]";
86
104
  /** The hydration runtime for one page. */
87
105
  function createHydrationRuntime(options) {
88
106
  const roots = /* @__PURE__ */ new Map();
@@ -146,6 +164,7 @@ function createHydrationRuntime(options) {
146
164
  const text = script.textContent || "{}";
147
165
  let parsed = JSON.parse(text);
148
166
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("props must be an object");
167
+ if (!text.includes(`"\$"`)) return parsed;
149
168
  if (text.includes(`"\$":"r"`)) {
150
169
  values.ingest(container.ownerDocument);
151
170
  parsed = values.resolve(parsed);
@@ -160,9 +179,10 @@ function createHydrationRuntime(options) {
160
179
  formStateRead = true;
161
180
  formState = readFormState(scope instanceof Document ? scope : scope.ownerDocument ?? document);
162
181
  }
182
+ if (scope instanceof Document) watchStream(scope);
163
183
  const candidates = Array.from(scope.querySelectorAll(ISLAND_SELECTOR));
164
184
  if (scope instanceof Element && scope.matches(ISLAND_SELECTOR)) candidates.unshift(scope);
165
- const islands = candidates.filter((container) => !roots.has(container) && !pending.has(container) && !container.parentElement?.closest(ISLAND_SELECTOR));
185
+ const islands = candidates.filter((container) => !roots.has(container) && !pending.has(container) && !container.parentElement?.closest(ISLAND_SELECTOR) && !container.closest(STREAM_STAGING));
166
186
  await Promise.all(islands.map(async (container) => {
167
187
  const src = container.getAttribute("src");
168
188
  const id = container.getAttribute("props");
@@ -312,6 +332,39 @@ function createHydrationRuntime(options) {
312
332
  return updated;
313
333
  }
314
334
  /** Unmounts the islands inside `scope`, and stops scheduled ones. */
335
+ /**
336
+ * A page still streaming: islands in Suspense content that arrives later hydrate as React puts it
337
+ * in place, and islands React removes (a replaced fallback) are released. Ends once the document
338
+ * is parsed and React has revealed every staged boundary (it may batch reveals past parsing).
339
+ */
340
+ let watching = false;
341
+ function watchStream(document) {
342
+ if (watching || document.readyState !== "loading" || typeof MutationObserver !== "function") return;
343
+ watching = true;
344
+ const removed = /* @__PURE__ */ new Set();
345
+ let queued = false;
346
+ const run = () => {
347
+ queued = false;
348
+ for (const node of removed) if (!node.isConnected) dispose(node);
349
+ removed.clear();
350
+ if (document.readyState !== "loading" && !document.querySelector(STREAM_STAGING)) {
351
+ observer.disconnect();
352
+ watching = false;
353
+ }
354
+ hydrate(document).catch((error) => report("Hydrating streamed islands failed.", error));
355
+ };
356
+ const observer = new MutationObserver((records) => {
357
+ for (const record of records) for (const node of record.removedNodes) removed.add(node);
358
+ if (queued) return;
359
+ queued = true;
360
+ queueMicrotask(run);
361
+ });
362
+ observer.observe(document, {
363
+ childList: true,
364
+ subtree: true
365
+ });
366
+ document.addEventListener("DOMContentLoaded", run, { once: true });
367
+ }
315
368
  function dispose(scope) {
316
369
  for (const [container, controller] of pending) if (container === scope || scope.contains(container)) {
317
370
  controller.abort();
@@ -1,9 +1,9 @@
1
- import { a as encode } from "../_chunks/codec-BTDkzzPP.mjs";
1
+ import { a as encode } from "../_chunks/codec-DqfVYKsx.mjs";
2
2
  import { f as FORM_STATE_SCRIPT_TYPE } from "../_chunks/wire-CXOodE06.mjs";
3
3
  import { r as PREHYDRATION_SCRIPT } from "../_chunks/prehydration-B1GLTLdk.mjs";
4
- import { g as escapeScriptJson, h as addressValues, i as encodeActionReference, v as parseHeldValues, y as selectValuesToSend } from "../_chunks/actions-C4dZ1-XD.mjs";
4
+ import { g as escapeScriptJson, h as addressValues, i as encodeActionReference, v as parseHeldValues, y as selectValuesToSend } from "../_chunks/actions-B8dJ8f7L.mjs";
5
5
  import { t as getBuildDefaults } from "../_chunks/build-COIcs7wm.mjs";
6
- import { a as IslandsProvider, c as getContexts, d as setIslandServerHook, f as setRenderStateResolver, l as isolateIslands, o as createIsland, p as withinClientRoot, r as renderServerTree, s as getClientReference, t as PAYLOAD_ONLY_ATTRIBUTE, u as runtimeTagged } from "../_chunks/tree-BEV73f_8.mjs";
6
+ import { a as IslandsProvider, c as getContexts, d as setIslandServerHook, f as setRenderStateResolver, l as isolateIslands, o as createIsland, p as withinClientRoot, r as renderServerTree, s as getClientReference, t as PAYLOAD_ONLY_ATTRIBUTE, u as runtimeTagged } from "../_chunks/tree-BDB5VEao.mjs";
7
7
  import * as React from "react";
8
8
  // src/react/server.ts
9
9
  /**
package/dist/react.mjs CHANGED
@@ -1 +1 @@
1
- export { a as IslandsProvider, o as createIsland, l as isolateIslands, r as renderServerTree, p as withinClientRoot } from "./_chunks/tree-BEV73f_8.mjs";
1
+ export { a as IslandsProvider, o as createIsland, l as isolateIslands, r as renderServerTree, p as withinClientRoot } from "./_chunks/tree-BDB5VEao.mjs";
package/dist/server.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { _ as hashValue, a as getActionReference, c as resolveAction, d as generateActionKey, f as sealBound, g as escapeScriptJson, h as addressValues, i as encodeActionReference, l as SealError, m as VALUE_MIN_LENGTH, n as configureActionKeys, o as registerInlineAction, p as unsealBound, r as decodeActionReference, s as registerServerReference, t as bindInlineAction, u as createActionKeys, v as parseHeldValues, y as selectValuesToSend } from "./_chunks/actions-C4dZ1-XD.mjs";
2
- import { a as encode, r as decode } from "./_chunks/codec-BTDkzzPP.mjs";
1
+ import { _ as hashValue, a as getActionReference, c as resolveAction, d as generateActionKey, f as sealBound, g as escapeScriptJson, h as addressValues, i as encodeActionReference, l as SealError, m as VALUE_MIN_LENGTH, n as configureActionKeys, o as registerInlineAction, p as unsealBound, r as decodeActionReference, s as registerServerReference, t as bindInlineAction, u as createActionKeys, v as parseHeldValues, y as selectValuesToSend } from "./_chunks/actions-B8dJ8f7L.mjs";
2
+ import { a as encode, r as decode } from "./_chunks/codec-DqfVYKsx.mjs";
3
3
  import { F as stripRuntimeFields, P as readPostedAction, j 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-CXOodE06.mjs";
4
4
  import { n as setBuildDefaults, t as getBuildDefaults } from "./_chunks/build-COIcs7wm.mjs";
5
5
  // src/server/hooks.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farm.js/islx",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
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",