@farm.js/islx 0.0.3 → 0.0.4

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.4
4
+
5
+ Promise props, and islands in a page that streams after it loaded.
6
+
7
+ - A server component can pass an unawaited promise to a client component, which reads it with
8
+ `use()`. The promise travels as `{"$":"T","v":name}`; when it settles, a Suspense boundary after
9
+ the island writes `<script type="f/promise" name>` with `{"v":value}` (the same codec and values
10
+ sent once) or `{"e":1}`. A rejection's reason stays on the server. In the browser the prop is a
11
+ thenable: settled already when its outcome is on the page, so hydration does not suspend, and
12
+ settled when it streams in otherwise.
13
+ - An island holding a Suspense boundary that is still streaming, after the document has loaded (a
14
+ framework router that streams navigations), hydrates once React reveals the boundary instead of
15
+ React rendering it again in the browser (error #419). `endStreamedContent(root)` from
16
+ `@farm.js/islx/react/client` marks boundaries a stream never completed as rendered in the
17
+ browser, so their islands hydrate on the next pass.
18
+
3
19
  ## 0.0.3
4
20
 
5
21
  Server layouts as trees.
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.3. React and Vite are supported; other renderers and framework
16
+ > Status: 0.0.4. 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
 
package/SPEC.md CHANGED
@@ -35,8 +35,9 @@ Anything that writes and reads it, in any language or framework, is a islx imple
35
35
  | `<script type="f/tree" [name] [partial]>` | A server tree, so the browser can rebuild the page inside an existing root without its HTML. A page's tree has no `name`; a layout's carries its scope and sits at its `children` outlet, or after the layout when it has none. `partial` marks a navigation that sent only the tree. |
36
36
  | `<script type="f/chunk" name>` | A part of a server tree that streamed in after the tree (a resolved Suspense boundary). `name` is its number, after `scope:` in a layout's tree. |
37
37
  | `<f-html name>` | Server HTML inside a server tree, named like a chunk. |
38
+ | `<script type="f/promise" name>` | The outcome of a promise an island received, written when it settles: `{"v":value}`, or `{"e":1}` for a rejection (the reason stays on the server). `name` is the promise's name. It may land anywhere after the island, and stream in after the island hydrated. |
38
39
 
39
- A value script's and props script's JSON escapes `<`, U+2028 and U+2029.
40
+ A value script's, props script's and promise script's JSON escapes `<`, U+2028 and U+2029.
40
41
 
41
42
  ## JSON tags
42
43
 
@@ -58,6 +59,8 @@ with any other keys is data.
58
59
  | `U` | `URL` | `href` |
59
60
  | `R` | `RegExp` | `[source, flags]` |
60
61
  | `Y` | bytes | `[type, base64]`; type is `ArrayBuffer`, `DataView` or a typed array name |
62
+ | `T` | a promise an island receives | its name; its outcome arrives in the `f/promise` script of that name |
63
+ | `P` | a promise inside a server tree, resolved already | its value, tagged |
61
64
  | `p` | a Suspense boundary inside a server tree | |
62
65
  | `L` | a layout tree's outlet, where the page renders | `n`: the outlet's name, `children` for the page |
63
66
  | `F` | a `FormData` action argument | its index among the call's attachments |
@@ -102,6 +105,13 @@ islands, and handles history, scroll and focus. Anything unusual loads the page
102
105
  site, `target`, `download`, `rel="external"`, modifier keys, a non-HTML answer, or a page whose
103
106
  browser entry differs, as after a deploy).
104
107
 
108
+ A framework router may stream a navigation in: the shell first, then each Suspense boundary as
109
+ it completes. A renderer may hydrate a boundary that is still streaming only while the document
110
+ loads, so after that an island holding one waits, and the next hydration pass starts it once the
111
+ boundary is revealed. When the stream ends, boundaries it never completed are marked as rendered
112
+ in the browser, and the next pass hydrates their islands (React: a pending boundary is
113
+ `<!--$?-->`, marked `<!--$!-->` by `endStreamedContent(root)` from `@farm.js/islx/react/client`).
114
+
105
115
  A server that leaves layouts out renders each kept layout as an empty boundary, and the browser
106
116
  keeps its own. If the response does not fit what is on screen, the browser loads the full
107
117
  document. All three headers make a response private, and the server sends `Vary` for each one
@@ -1,5 +1,5 @@
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, u as setOwn } from "./codec-DqfVYKsx.mjs";
1
+ import { A as VALUES_HEADER, F as getTag, M as VALUE_SCRIPT_TYPE, N as attachmentField, j as VALUE_KEY_PATTERN, o as BOUND_FIELD, s as CALL_FIELD, t as ACTION_HEADER } from "./wire-QEQWBr5s.mjs";
2
+ import { a as encode, r as decode, u as setOwn } from "./codec-C8to-RQW.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,
@@ -1,5 +1,5 @@
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-DqfVYKsx.mjs";
1
+ import { A as VALUES_HEADER, P as formFieldName, d as FORM_REF_PREFIX, j as VALUE_KEY_PATTERN } from "./wire-QEQWBr5s.mjs";
2
+ import { a as encode, c as needsObjectTag, o as encodeLeaf, r as decode, t as EncodeError } from "./codec-C8to-RQW.mjs";
3
3
  import { t as getBuildDefaults } from "./build-COIcs7wm.mjs";
4
4
  // src/server/values.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { N as getTag } from "./wire-CXOodE06.mjs";
1
+ import { F as getTag } from "./wire-QEQWBr5s.mjs";
2
2
  // src/protocol/codec.ts
3
3
  /**
4
4
  * Values JSON cannot hold. Each travels as a tagged object: the reserved key `$` holds a short tag
@@ -117,7 +117,8 @@ function decodeTagged(object, decodeChild) {
117
117
  case "s":
118
118
  case "a":
119
119
  case "F":
120
- case "K": return NOT_A_VALUE;
120
+ case "K":
121
+ case "T": return NOT_A_VALUE;
121
122
  case "o":
122
123
  if (data && typeof data === "object" && !Array.isArray(data)) return plainObject(Object.entries(data).map(([key, value]) => [key, decodeChild(value)]));
123
124
  break;
@@ -1,5 +1,5 @@
1
1
  import { a as hasPendingInteraction, o as takePendingInteractions, t as INTERACTION_EVENT } from "./prehydration-B1GLTLdk.mjs";
2
- import { m as HYDRATED_ATTRIBUTE } from "./wire-CXOodE06.mjs";
2
+ import { m as HYDRATED_ATTRIBUTE } from "./wire-QEQWBr5s.mjs";
3
3
  // src/client/schedule.ts
4
4
  /**
5
5
  * When an island hydrates. Renderer-neutral: every adapter schedules through this.
@@ -1,6 +1,6 @@
1
- import { i as decodeTagged, n as NOT_A_VALUE, o as encodeLeaf } from "./codec-DqfVYKsx.mjs";
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-2sTj0A2T.mjs";
1
+ import { i as decodeTagged, n as NOT_A_VALUE, o as encodeLeaf } from "./codec-C8to-RQW.mjs";
2
+ import { M as VALUE_SCRIPT_TYPE, c as CHUNK_SCRIPT_TYPE, h as ISLAND_ELEMENT, k as TREE_SCRIPT_TYPE, p as HTML_ELEMENT, w as SLOT_ELEMENT, x as PROPS_SCRIPT_TYPE, y as PROMISE_SCRIPT_TYPE } from "./wire-QEQWBr5s.mjs";
3
+ import { l as getValueStore, t as createServerReference } from "./actions-8RQfrM-p.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";
@@ -141,6 +141,64 @@ function splitSlots(value, prefix, counter) {
141
141
  };
142
142
  }
143
143
  const warned = /* @__PURE__ */ new Set();
144
+ const outcomes = /* @__PURE__ */ new WeakMap();
145
+ function settle(promise) {
146
+ let outcome = outcomes.get(promise);
147
+ if (!outcome) {
148
+ outcome = Promise.resolve(promise).then((value) => ({
149
+ ok: true,
150
+ value
151
+ }), (reason) => ({
152
+ ok: false,
153
+ reason
154
+ }));
155
+ outcomes.set(promise, outcome);
156
+ }
157
+ return outcome;
158
+ }
159
+ const REJECTED = "{\"e\":1}";
160
+ /** Each promise an island received, in a boundary of its own: its outcome once it settles. */
161
+ function promiseBoundaries(promises, label) {
162
+ return promises.map(({ name, promise }) => React.createElement(React.Suspense, {
163
+ key: name,
164
+ fallback: null
165
+ }, React.createElement(IslandPromise, {
166
+ name,
167
+ promise,
168
+ label
169
+ })));
170
+ }
171
+ /**
172
+ * A promise's outcome, written when it settles. Its values are sent whether or not the page wrote
173
+ * them already: this script may land before an earlier island's, wherever React puts it.
174
+ */
175
+ function IslandPromise({ name, promise, label }) {
176
+ const outcome = React.use(settle(promise));
177
+ const state = React.useContext(getContexts().render) ?? resolveRenderState();
178
+ const hook = getIslandServerHook();
179
+ let encoded = {
180
+ json: REJECTED,
181
+ values: [],
182
+ promises: []
183
+ };
184
+ if (outcome.ok) try {
185
+ encoded = hook.encode({ v: outcome.value }, state?.held ?? null);
186
+ } catch (error) {
187
+ console.warn(`[islx] A promise passed to ${label} resolved to a value that cannot be sent to the browser: it rejects there.`, error);
188
+ }
189
+ else console.error(`[islx] A promise passed to ${label} rejected.`, outcome.reason);
190
+ return React.createElement(React.Fragment, null, ...encoded.values.map((value) => React.createElement("script", {
191
+ key: value.key,
192
+ type: VALUE_SCRIPT_TYPE,
193
+ name: value.key,
194
+ dangerouslySetInnerHTML: { __html: hook.escape(value.json) }
195
+ })), React.createElement("script", {
196
+ key: "outcome",
197
+ type: PROMISE_SCRIPT_TYPE,
198
+ name,
199
+ dangerouslySetInnerHTML: { __html: hook.escape(encoded.json) }
200
+ }), ...promiseBoundaries(encoded.promises, label));
201
+ }
144
202
  function isThenable$1(value) {
145
203
  return !!value && typeof value.then === "function";
146
204
  }
@@ -193,7 +251,7 @@ function createIsland(Component, src, exportName, strategy = "load") {
193
251
  type: PROPS_SCRIPT_TYPE,
194
252
  htmlFor: id,
195
253
  dangerouslySetInnerHTML: { __html: hook.escape(encoded.json) }
196
- }));
254
+ }), ...promiseBoundaries(encoded.promises, `${exportName} (${src})`));
197
255
  }
198
256
  Island.displayName = `Island(${Component.displayName || Component.name || exportName})`;
199
257
  Object.defineProperty(Island, CLIENT_REFERENCE, { value: {
@@ -24,6 +24,11 @@ declare const TREE_SCRIPT_TYPE = "f/tree";
24
24
  declare const CHUNK_SCRIPT_TYPE = "f/chunk";
25
25
  /** The element around server HTML inside a server tree. */
26
26
  declare const HTML_ELEMENT = "f-html";
27
+ /**
28
+ * The script type of a promise's outcome, written when it settles: `{"v":value}`, or `{"e":1}` for
29
+ * a rejection. Its `name` attribute names the promise.
30
+ */
31
+ declare const PROMISE_SCRIPT_TYPE = "f/promise";
27
32
  /** The script type of React's form state after a `useActionState` form post, for hydration. */
28
33
  declare const FORM_STATE_SCRIPT_TYPE = "f/form-state";
29
34
  /** The reserved JSON key whose value is a tag. */
@@ -38,6 +43,11 @@ declare const REFERENCE_TAG = "r";
38
43
  declare const SLOT_TAG = "s";
39
44
  /** `{"$":"a","v":id}` or `{"$":"a","v":id,"b":sealed}`: a server action. */
40
45
  declare const ACTION_TAG = "a";
46
+ /**
47
+ * `{"$":"T","v":name}`: a promise an island receives, settled by the `f/promise` script `name`.
48
+ * (Inside a server tree, `{"$":"P","v":value}` is a promise that resolved already.)
49
+ */
50
+ declare const PROMISE_TAG = "T";
41
51
  /** `{"$":"o","v":{…}}`: app data that has its own `$` key. */
42
52
  declare const OBJECT_TAG = "o";
43
53
  /** `{"$":"F","v":n}`: a `FormData` argument, sent as fields of a multipart call. */
@@ -95,4 +105,4 @@ declare const MAX_HELD_VALUES = 128;
95
105
  * `{"$":"a","v":id,"b":sealed}`), or undefined for anything else.
96
106
  */
97
107
  declare function getTag(value: unknown): string | undefined;
98
- export { VALUE_SCRIPT_TYPE as A, SLOT_TAG as C, TREE_SCRIPT_TYPE as D, TAG_VALUE as E, stripRuntimeFields as F, formFieldName as M, getTag as N, VALUES_HEADER as O, readPostedAction as P, SLOT_ELEMENT as S, TAG_BOUND as T, MAX_HELD_VALUES as _, BLOB_TAG as a, REFERENCE_TAG as b, CHUNK_SCRIPT_TYPE as c, FORM_REF_PREFIX as d, FORM_STATE_SCRIPT_TYPE as f, LAYOUTS_HEADER as g, ISLAND_ELEMENT as h, ACTION_TAG as i, attachmentField as j, VALUE_KEY_PATTERN as k, FORM_DATA_TAG as l, HYDRATED_ATTRIBUTE as m, ACTION_KEY_FIELD as n, BOUND_FIELD as o, HTML_ELEMENT as p, ACTION_STALE_HEADER as r, CALL_FIELD as s, ACTION_HEADER as t, FORM_FIELD_PREFIX as u, OBJECT_TAG as v, TAG as w, RENDER_HEADER as x, PROPS_SCRIPT_TYPE as y };
108
+ export { VALUES_HEADER as A, RENDER_HEADER as C, TAG_BOUND as D, TAG as E, getTag as F, readPostedAction as I, stripRuntimeFields as L, VALUE_SCRIPT_TYPE as M, attachmentField as N, TAG_VALUE as O, formFieldName as P, REFERENCE_TAG as S, SLOT_TAG as T, MAX_HELD_VALUES as _, BLOB_TAG as a, PROMISE_TAG as b, CHUNK_SCRIPT_TYPE as c, FORM_REF_PREFIX as d, FORM_STATE_SCRIPT_TYPE as f, LAYOUTS_HEADER as g, ISLAND_ELEMENT as h, ACTION_TAG as i, VALUE_KEY_PATTERN as j, TREE_SCRIPT_TYPE as k, FORM_DATA_TAG as l, HYDRATED_ATTRIBUTE as m, ACTION_KEY_FIELD as n, BOUND_FIELD as o, HTML_ELEMENT as p, ACTION_STALE_HEADER as r, CALL_FIELD as s, ACTION_HEADER as t, FORM_FIELD_PREFIX as u, OBJECT_TAG as v, SLOT_ELEMENT as w, PROPS_SCRIPT_TYPE as x, PROMISE_SCRIPT_TYPE as y };
@@ -25,6 +25,11 @@ const TREE_SCRIPT_TYPE = "f/tree";
25
25
  const CHUNK_SCRIPT_TYPE = "f/chunk";
26
26
  /** The element around server HTML inside a server tree. */
27
27
  const HTML_ELEMENT = "f-html";
28
+ /**
29
+ * The script type of a promise's outcome, written when it settles: `{"v":value}`, or `{"e":1}` for
30
+ * a rejection. Its `name` attribute names the promise.
31
+ */
32
+ const PROMISE_SCRIPT_TYPE = "f/promise";
28
33
  /** The script type of React's form state after a `useActionState` form post, for hydration. */
29
34
  const FORM_STATE_SCRIPT_TYPE = "f/form-state";
30
35
  /** The reserved JSON key whose value is a tag. */
@@ -39,6 +44,11 @@ const REFERENCE_TAG = "r";
39
44
  const SLOT_TAG = "s";
40
45
  /** `{"$":"a","v":id}` or `{"$":"a","v":id,"b":sealed}`: a server action. */
41
46
  const ACTION_TAG = "a";
47
+ /**
48
+ * `{"$":"T","v":name}`: a promise an island receives, settled by the `f/promise` script `name`.
49
+ * (Inside a server tree, `{"$":"P","v":value}` is a promise that resolved already.)
50
+ */
51
+ const PROMISE_TAG = "T";
42
52
  /** `{"$":"o","v":{…}}`: app data that has its own `$` key. */
43
53
  const OBJECT_TAG = "o";
44
54
  /** `{"$":"F","v":n}`: a `FormData` argument, sent as fields of a multipart call. */
@@ -118,4 +128,4 @@ function getTag(value) {
118
128
  if (keys.length === 2 && "v" in value) return tag;
119
129
  if (keys.length === 3 && tag === "a" && "v" in value && "b" in value) return tag;
120
130
  }
121
- export { VALUE_SCRIPT_TYPE as A, SLOT_TAG as C, TREE_SCRIPT_TYPE as D, TAG_VALUE as E, stripRuntimeFields as F, formFieldName as M, getTag as N, VALUES_HEADER as O, readPostedAction as P, SLOT_ELEMENT as S, TAG_BOUND as T, MAX_HELD_VALUES as _, BLOB_TAG as a, REFERENCE_TAG as b, CHUNK_SCRIPT_TYPE as c, FORM_REF_PREFIX as d, FORM_STATE_SCRIPT_TYPE as f, LAYOUTS_HEADER as g, ISLAND_ELEMENT as h, ACTION_TAG as i, attachmentField as j, VALUE_KEY_PATTERN as k, FORM_DATA_TAG as l, HYDRATED_ATTRIBUTE as m, ACTION_KEY_FIELD as n, BOUND_FIELD as o, HTML_ELEMENT as p, ACTION_STALE_HEADER as r, CALL_FIELD as s, ACTION_HEADER as t, FORM_FIELD_PREFIX as u, OBJECT_TAG as v, TAG as w, RENDER_HEADER as x, PROPS_SCRIPT_TYPE as y };
131
+ export { VALUES_HEADER as A, RENDER_HEADER as C, TAG_BOUND as D, TAG as E, getTag as F, readPostedAction as I, stripRuntimeFields as L, VALUE_SCRIPT_TYPE as M, attachmentField as N, TAG_VALUE as O, formFieldName as P, REFERENCE_TAG as S, SLOT_TAG as T, MAX_HELD_VALUES as _, BLOB_TAG as a, PROMISE_TAG as b, CHUNK_SCRIPT_TYPE as c, FORM_REF_PREFIX as d, FORM_STATE_SCRIPT_TYPE as f, LAYOUTS_HEADER as g, ISLAND_ELEMENT as h, ACTION_TAG as i, VALUE_KEY_PATTERN as j, TREE_SCRIPT_TYPE as k, FORM_DATA_TAG as l, HYDRATED_ATTRIBUTE as m, ACTION_KEY_FIELD as n, BOUND_FIELD as o, HTML_ELEMENT as p, ACTION_STALE_HEADER as r, CALL_FIELD as s, ACTION_HEADER as t, FORM_FIELD_PREFIX as u, OBJECT_TAG as v, SLOT_ELEMENT as w, PROPS_SCRIPT_TYPE as x, PROMISE_SCRIPT_TYPE as y };
package/dist/client.mjs CHANGED
@@ -1,5 +1,5 @@
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
- import { F as stripRuntimeFields, P as readPostedAction, x as RENDER_HEADER } from "./_chunks/wire-CXOodE06.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-8RQfrM-p.mjs";
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
5
5
  /**
package/dist/index.d.mts CHANGED
@@ -1,3 +1,3 @@
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-C3Rpbwl6.mjs";
1
+ import { A as VALUES_HEADER, C as RENDER_HEADER, D as TAG_BOUND, E as TAG, F as getTag, I as readPostedAction, L as stripRuntimeFields, M as VALUE_SCRIPT_TYPE, N as attachmentField, O as TAG_VALUE, P as formFieldName, S as REFERENCE_TAG, T as SLOT_TAG, _ as MAX_HELD_VALUES, a as BLOB_TAG, b as PROMISE_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 VALUE_KEY_PATTERN, k as TREE_SCRIPT_TYPE, 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 SLOT_ELEMENT, x as PROPS_SCRIPT_TYPE, y as PROMISE_SCRIPT_TYPE } from "./_chunks/wire-Dipqh-EB.mjs";
2
2
  import { a as decode, c as encodeLeaf, d as plainObject, f as tagged, i as NOT_A_VALUE, l as fromBase64, n as EncodeError, o as decodeTagged, p as toBase64, r as EncodeOptions, s as encode, t as DecodeOptions, u as needsObjectTag } from "./_chunks/codec-CK8kZp4u.mjs";
3
- export { ACTION_HEADER, ACTION_KEY_FIELD, ACTION_STALE_HEADER, ACTION_TAG, BLOB_TAG, BOUND_FIELD, CALL_FIELD, CHUNK_SCRIPT_TYPE, type DecodeOptions, EncodeError, type EncodeOptions, 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 };
3
+ export { ACTION_HEADER, ACTION_KEY_FIELD, ACTION_STALE_HEADER, ACTION_TAG, BLOB_TAG, BOUND_FIELD, CALL_FIELD, CHUNK_SCRIPT_TYPE, type DecodeOptions, EncodeError, type EncodeOptions, 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, PROMISE_SCRIPT_TYPE, PROMISE_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 };
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
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 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
- 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
+ import { A as VALUES_HEADER, C as RENDER_HEADER, D as TAG_BOUND, E as TAG, F as getTag, I as readPostedAction, L as stripRuntimeFields, M as VALUE_SCRIPT_TYPE, N as attachmentField, O as TAG_VALUE, P as formFieldName, S as REFERENCE_TAG, T as SLOT_TAG, _ as MAX_HELD_VALUES, a as BLOB_TAG, b as PROMISE_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 VALUE_KEY_PATTERN, k as TREE_SCRIPT_TYPE, 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 SLOT_ELEMENT, x as PROPS_SCRIPT_TYPE, y as PROMISE_SCRIPT_TYPE } from "./_chunks/wire-QEQWBr5s.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-C8to-RQW.mjs";
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, PROMISE_SCRIPT_TYPE, PROMISE_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,5 +1,5 @@
1
1
  import { a as scheduleHydration, i as isIslandStrategy, n as IslandStrategy, r as ScheduleOptions, t as ISLAND_STRATEGIES } from "../_chunks/schedule-Dw14dW0H.mjs";
2
- import { m as HYDRATED_ATTRIBUTE } from "../_chunks/wire-C3Rpbwl6.mjs";
2
+ import { m as HYDRATED_ATTRIBUTE } from "../_chunks/wire-Dipqh-EB.mjs";
3
3
  import { t as ValueStore } from "../_chunks/value-store-Dm-7xJ6W.mjs";
4
4
  import { a as ServerTreeLayoutOptions, c as createServerTreeLayout, g as isolateIslands, i as ServerTreeLayout, n as PAYLOAD_ONLY_ATTRIBUTE, r as ServerTreeElementOptions, s as createServerTreeElement, u as updateServerTrees, v as withinClientRoot } from "../_chunks/tree-B4RMU2UX.mjs";
5
5
  import * as React from "react";
@@ -21,6 +21,12 @@ interface HydrationOptions {
21
21
  */
22
22
  schedule: typeof scheduleHydration;
23
23
  }
24
+ /**
25
+ * Marks the Suspense boundaries under `root` that still wait once their stream has ended (the
26
+ * connection dropped) as rendered in the browser, as React does when it finds one: the next
27
+ * `hydrate()` pass starts their islands. Call it when a stream that filled `root` ends.
28
+ */
29
+ declare function endStreamedContent(root: ParentNode): void;
24
30
  /** The hydration runtime for one page. */
25
31
  declare function createHydrationRuntime(options: HydrationOptions): {
26
32
  hydrate: (scope?: ParentNode, signal?: AbortSignal) => Promise<void>;
@@ -46,4 +52,4 @@ declare function createServerHtml(runtime: Pick<HydrationRuntime, "hydrate" | "d
46
52
  declare function refreshDocument(runtime: HydrationRuntime): Promise<void>;
47
53
  /** Puts another page's body in place of this one's, and hydrates its islands. */
48
54
  declare function swapDocument(runtime: HydrationRuntime, next: Document): Promise<void>;
49
- export { HYDRATED_ATTRIBUTE, type HydrationOptions, type HydrationRuntime, ISLAND_STRATEGIES, type IslandStrategy, PAYLOAD_ONLY_ATTRIBUTE, type ScheduleOptions, type ServerTreeElementOptions, type ServerTreeLayout, type ServerTreeLayoutOptions, createHydrationRuntime, createServerHtml, createServerTreeElement, createServerTreeLayout, isIslandStrategy, isolateIslands, refreshDocument, scheduleHydration, swapDocument, updateServerTrees, withinClientRoot };
55
+ export { HYDRATED_ATTRIBUTE, type HydrationOptions, type HydrationRuntime, ISLAND_STRATEGIES, type IslandStrategy, PAYLOAD_ONLY_ATTRIBUTE, type ScheduleOptions, type ServerTreeElementOptions, type ServerTreeLayout, type ServerTreeLayoutOptions, createHydrationRuntime, createServerHtml, createServerTreeElement, createServerTreeLayout, endStreamedContent, isIslandStrategy, isolateIslands, refreshDocument, scheduleHydration, swapDocument, updateServerTrees, withinClientRoot };
@@ -1,8 +1,8 @@
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
- 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 { d as isolateIslands, h as withinClientRoot, i as createServerTreeLayout, n as PAYLOAD_ONLY_ATTRIBUTE, o as updateServerTrees, r as createServerTreeElement, u as getContexts } from "../_chunks/tree-BvYR6-PF.mjs";
5
- import { n as isIslandStrategy, r as scheduleHydration, t as ISLAND_STRATEGIES } from "../_chunks/schedule-BFoGKRrU.mjs";
1
+ import { a as emit, l as getValueStore, t as createServerReference, u as valueHeaders } from "../_chunks/actions-8RQfrM-p.mjs";
2
+ import { i as decodeTagged, n as NOT_A_VALUE, r as decode, u as setOwn } from "../_chunks/codec-C8to-RQW.mjs";
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
+ import { d as isolateIslands, h as withinClientRoot, i as createServerTreeLayout, n as PAYLOAD_ONLY_ATTRIBUTE, o as updateServerTrees, r as createServerTreeElement, u as getContexts } from "../_chunks/tree-opIpaCke.mjs";
5
+ import { n as isIslandStrategy, r as scheduleHydration, t as ISLAND_STRATEGIES } from "../_chunks/schedule-CjoybS3b.mjs";
6
6
  import * as React from "react";
7
7
  import { hydrateRoot } from "react-dom/client";
8
8
  // src/react/hydrate.ts
@@ -39,22 +39,22 @@ function readFormState(document) {
39
39
  const revivedValues = /* @__PURE__ */ new WeakMap();
40
40
  let slotsRevived = 0;
41
41
  /** Turns travelled props back into values: slots keep their server HTML, actions become callable. */
42
- function revive(value, container) {
42
+ function revive(value, container, values) {
43
43
  if (value && typeof value === "object") {
44
44
  const cached = revivedValues.get(value);
45
45
  if (cached !== void 0) return cached;
46
46
  const slotsBefore = slotsRevived;
47
- const revived = reviveUncached(value, container);
47
+ const revived = reviveUncached(value, container, values);
48
48
  if (slotsRevived === slotsBefore) revivedValues.set(value, revived);
49
49
  return revived;
50
50
  }
51
51
  return value;
52
52
  }
53
- function reviveUncached(value, container) {
53
+ function reviveUncached(value, container, values) {
54
54
  if (Array.isArray(value)) {
55
55
  let changed = false;
56
56
  const revived = value.map((entry) => {
57
- const next = revive(entry, container);
57
+ const next = revive(entry, container, values);
58
58
  if (next !== entry) changed = true;
59
59
  return next;
60
60
  });
@@ -82,25 +82,152 @@ function reviveUncached(value, container) {
82
82
  dangerouslySetInnerHTML: { __html: html }
83
83
  });
84
84
  }
85
+ if (tag === "T" && typeof object["v"] === "string") return islandPromise(object["v"], container, values);
85
86
  if (tag === "r") throw new Error(`islx value ${String(object["v"])} was not resolved`);
86
87
  if (tag !== void 0) {
87
- const decoded = decodeTagged(object, (child) => revive(child, container));
88
+ const decoded = decodeTagged(object, (child) => revive(child, container, values));
88
89
  if (decoded !== NOT_A_VALUE) return decoded;
89
90
  }
90
91
  let changed = false;
91
92
  const revived = {};
92
93
  for (const [key, entry] of Object.entries(object)) {
93
- const next = revive(entry, container);
94
+ const next = revive(entry, container, values);
94
95
  setOwn(revived, key, next);
95
96
  if (next !== entry) changed = true;
96
97
  }
97
98
  return changed ? revived : value;
98
99
  }
100
+ const pendingPromises = /* @__PURE__ */ new Map();
101
+ let promiseObserver = null;
102
+ function findPromiseScript(container, name) {
103
+ const documents = new Set([container.ownerDocument, globalThis.document].filter(Boolean));
104
+ for (const document of documents) for (const script of document.querySelectorAll(`script[type="${PROMISE_SCRIPT_TYPE}"]`)) if (script.getAttribute("name") === name) return script;
105
+ return null;
106
+ }
107
+ function readOutcome(script, container, values) {
108
+ try {
109
+ const text = script.textContent || "{}";
110
+ let parsed = JSON.parse(text);
111
+ if (!parsed || typeof parsed !== "object" || !("v" in parsed)) return {
112
+ ok: false,
113
+ reason: /* @__PURE__ */ new Error("islx: the promise rejected on the server")
114
+ };
115
+ if (text.includes(`"\$":"r"`)) {
116
+ values.ingest(container.ownerDocument);
117
+ parsed = values.resolve(parsed);
118
+ }
119
+ return {
120
+ ok: true,
121
+ value: revive(parsed, container, values).v
122
+ };
123
+ } catch (reason) {
124
+ return {
125
+ ok: false,
126
+ reason
127
+ };
128
+ }
129
+ }
130
+ /**
131
+ * The promise an island received, as a thenable React's `use` reads: settled already when its
132
+ * outcome is on the page, so hydration does not suspend, and settled when the outcome streams in
133
+ * otherwise.
134
+ */
135
+ function islandPromise(name, container, values) {
136
+ const pending = pendingPromises.get(name);
137
+ if (pending) return pending.thenable;
138
+ const script = findPromiseScript(container, name);
139
+ if (script) {
140
+ const outcome = readOutcome(script, container, values);
141
+ const thenable = outcome.ok ? Promise.resolve(outcome.value) : Promise.reject(outcome.reason);
142
+ if (outcome.ok) Object.assign(thenable, {
143
+ status: "fulfilled",
144
+ value: outcome.value
145
+ });
146
+ else {
147
+ thenable.catch(() => {});
148
+ Object.assign(thenable, {
149
+ status: "rejected",
150
+ reason: outcome.reason
151
+ });
152
+ }
153
+ return thenable;
154
+ }
155
+ let resolve;
156
+ let reject;
157
+ const thenable = new Promise((done, fail) => {
158
+ resolve = done;
159
+ reject = fail;
160
+ });
161
+ thenable.catch(() => {});
162
+ thenable.status = "pending";
163
+ pendingPromises.set(name, {
164
+ thenable,
165
+ container,
166
+ values,
167
+ settle(found) {
168
+ pendingPromises.delete(name);
169
+ const outcome = readOutcome(found, container, values);
170
+ if (outcome.ok) {
171
+ Object.assign(thenable, {
172
+ status: "fulfilled",
173
+ value: outcome.value
174
+ });
175
+ resolve(outcome.value);
176
+ } else {
177
+ Object.assign(thenable, {
178
+ status: "rejected",
179
+ reason: outcome.reason
180
+ });
181
+ reject(outcome.reason);
182
+ }
183
+ }
184
+ });
185
+ if (!promiseObserver && typeof MutationObserver === "function") {
186
+ promiseObserver = new MutationObserver(() => {
187
+ for (const [pendingName, entry] of pendingPromises) {
188
+ const found = findPromiseScript(entry.container, pendingName);
189
+ if (found) entry.settle(found);
190
+ }
191
+ if (pendingPromises.size === 0) {
192
+ promiseObserver?.disconnect();
193
+ promiseObserver = null;
194
+ }
195
+ });
196
+ promiseObserver.observe(globalThis.document ?? container.ownerDocument, {
197
+ childList: true,
198
+ subtree: true
199
+ });
200
+ }
201
+ return thenable;
202
+ }
99
203
  /**
100
204
  * React streams Suspense content that resolves late into a hidden `<div id="S:n">`, then moves it
101
205
  * into place. Its islands hydrate once moved, not while staged.
102
206
  */
103
207
  const STREAM_STAGING = "div[hidden][id^=\"S:\"]";
208
+ const PENDING_BOUNDARY = "$?";
209
+ /**
210
+ * Whether React still streams Suspense content into `container`. React waits for such a boundary
211
+ * only while the document loads; later (a navigation that streams) it would render it again in
212
+ * the browser, so the island hydrates once React has revealed it.
213
+ */
214
+ function awaitsStreamedContent(container) {
215
+ const document = container.ownerDocument;
216
+ if (document.readyState === "loading") return false;
217
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_COMMENT);
218
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) if (node.data === PENDING_BOUNDARY) return true;
219
+ return false;
220
+ }
221
+ /**
222
+ * Marks the Suspense boundaries under `root` that still wait once their stream has ended (the
223
+ * connection dropped) as rendered in the browser, as React does when it finds one: the next
224
+ * `hydrate()` pass starts their islands. Call it when a stream that filled `root` ends.
225
+ */
226
+ function endStreamedContent(root) {
227
+ const node = root;
228
+ const walker = (node.nodeType === 9 ? node : node.ownerDocument).createTreeWalker(node, NodeFilter.SHOW_COMMENT);
229
+ for (let comment = walker.nextNode(); comment; comment = walker.nextNode()) if (comment.data === PENDING_BOUNDARY) comment.data = "$!";
230
+ }
104
231
  /** The hydration runtime for one page. */
105
232
  function createHydrationRuntime(options) {
106
233
  const roots = /* @__PURE__ */ new Map();
@@ -169,7 +296,7 @@ function createHydrationRuntime(options) {
169
296
  values.ingest(container.ownerDocument);
170
297
  parsed = values.resolve(parsed);
171
298
  }
172
- return revive(parsed, container);
299
+ return revive(parsed, container, values);
173
300
  }
174
301
  const topLevel = (scope) => Array.from(scope.querySelectorAll(ISLAND_SELECTOR)).filter((container) => !container.parentElement?.closest(ISLAND_SELECTOR));
175
302
  const keyOf = (container) => `${container.getAttribute("src")}#${container.getAttribute("export") || "default"}`;
@@ -182,7 +309,7 @@ function createHydrationRuntime(options) {
182
309
  if (scope instanceof Document) watchStream(scope);
183
310
  const candidates = Array.from(scope.querySelectorAll(ISLAND_SELECTOR));
184
311
  if (scope instanceof Element && scope.matches(ISLAND_SELECTOR)) candidates.unshift(scope);
185
- const islands = candidates.filter((container) => !roots.has(container) && !pending.has(container) && !container.parentElement?.closest(ISLAND_SELECTOR) && !container.closest(STREAM_STAGING));
312
+ const islands = candidates.filter((container) => !roots.has(container) && !pending.has(container) && !container.parentElement?.closest(ISLAND_SELECTOR) && !container.closest(STREAM_STAGING) && !awaitsStreamedContent(container));
186
313
  await Promise.all(islands.map(async (container) => {
187
314
  const src = container.getAttribute("src");
188
315
  const id = container.getAttribute("props");
@@ -350,6 +477,7 @@ function createHydrationRuntime(options) {
350
477
  if (document.readyState !== "loading" && !document.querySelector(STREAM_STAGING)) {
351
478
  observer.disconnect();
352
479
  watching = false;
480
+ endStreamedContent(document);
353
481
  }
354
482
  hydrate(document).catch((error) => report("Hydrating streamed islands failed.", error));
355
483
  };
@@ -366,6 +494,11 @@ function createHydrationRuntime(options) {
366
494
  document.addEventListener("DOMContentLoaded", run, { once: true });
367
495
  }
368
496
  function dispose(scope) {
497
+ for (const [name, entry] of pendingPromises) if (scope.contains(entry.container)) pendingPromises.delete(name);
498
+ if (pendingPromises.size === 0) {
499
+ promiseObserver?.disconnect();
500
+ promiseObserver = null;
501
+ }
369
502
  for (const [container, controller] of pending) if (container === scope || scope.contains(container)) {
370
503
  controller.abort();
371
504
  pending.delete(container);
@@ -453,4 +586,4 @@ async function swapDocument(runtime, next) {
453
586
  document.body.replaceChildren(...Array.from(body.childNodes));
454
587
  await runtime.hydrate(document);
455
588
  }
456
- export { HYDRATED_ATTRIBUTE, ISLAND_STRATEGIES, PAYLOAD_ONLY_ATTRIBUTE, createHydrationRuntime, createServerHtml, createServerTreeElement, createServerTreeLayout, isIslandStrategy, isolateIslands, refreshDocument, scheduleHydration, swapDocument, updateServerTrees, withinClientRoot };
589
+ export { HYDRATED_ATTRIBUTE, ISLAND_STRATEGIES, PAYLOAD_ONLY_ATTRIBUTE, createHydrationRuntime, createServerHtml, createServerTreeElement, createServerTreeLayout, endStreamedContent, isIslandStrategy, isolateIslands, refreshDocument, scheduleHydration, swapDocument, updateServerTrees, withinClientRoot };
@@ -1,19 +1,36 @@
1
- import { a as encode } from "../_chunks/codec-DqfVYKsx.mjs";
2
- import { f as FORM_STATE_SCRIPT_TYPE } from "../_chunks/wire-CXOodE06.mjs";
1
+ import { a as encode } from "../_chunks/codec-C8to-RQW.mjs";
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 { g as escapeScriptJson, h as addressValues, i as encodeActionReference, v as parseHeldValues, y as selectValuesToSend } from "../_chunks/actions-B8dJ8f7L.mjs";
4
+ import { g as escapeScriptJson, h as addressValues, i as encodeActionReference, v as parseHeldValues, y as selectValuesToSend } from "../_chunks/actions-CtLpJPEB.mjs";
5
5
  import { t as getBuildDefaults } from "../_chunks/build-COIcs7wm.mjs";
6
- import { a as renderServerTree, c as createIsland, d as isolateIslands, f as runtimeTagged, h as withinClientRoot, l as getClientReference, m as setRenderStateResolver, n as PAYLOAD_ONLY_ATTRIBUTE, p as setIslandServerHook, s as IslandsProvider, t as Outlet, u as getContexts } from "../_chunks/tree-BvYR6-PF.mjs";
6
+ import { a as renderServerTree, c as createIsland, d as isolateIslands, f as runtimeTagged, h as withinClientRoot, l as getClientReference, m as setRenderStateResolver, n as PAYLOAD_ONLY_ATTRIBUTE, p as setIslandServerHook, s as IslandsProvider, t as Outlet, u as getContexts } from "../_chunks/tree-opIpaCke.mjs";
7
7
  import * as React from "react";
8
8
  // src/react/server.ts
9
9
  /**
10
10
  * @farm.js/islx/react/server: rendering a React page with islands. Importing it gives islands the
11
11
  * server code they need (values, action references); `renderToStream` renders a page.
12
12
  */
13
+ const PROMISE_PREFIX = `${Array.from(crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(2)), (part) => part.toString(36)).join("")}.`;
14
+ let promiseCount = 0;
15
+ function isThenable(value) {
16
+ return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
17
+ }
13
18
  setIslandServerHook({
14
19
  encode(props, held) {
20
+ const promises = [];
15
21
  const addressed = addressValues(props, { encodeSpecial: (value) => {
16
22
  if (value && typeof value === "object" && runtimeTagged.has(value)) return value;
23
+ if (isThenable(value)) {
24
+ const name = PROMISE_PREFIX + (promiseCount++).toString(36);
25
+ promises.push({
26
+ name,
27
+ promise: value
28
+ });
29
+ return {
30
+ ["$"]: "T",
31
+ ["v"]: name
32
+ };
33
+ }
17
34
  if (typeof value === "function") {
18
35
  const reference = encodeActionReference(value);
19
36
  if (reference) return reference;
@@ -26,7 +43,8 @@ setIslandServerHook({
26
43
  values: selectValuesToSend(addressed, held).map(({ key, json }) => ({
27
44
  key,
28
45
  json
29
- }))
46
+ })),
47
+ promises
30
48
  };
31
49
  },
32
50
  escape: escapeScriptJson,
package/dist/react.mjs CHANGED
@@ -1 +1 @@
1
- export { s as IslandsProvider, t as Outlet, c as createIsland, d as isolateIslands, a as renderServerTree, h as withinClientRoot } from "./_chunks/tree-BvYR6-PF.mjs";
1
+ export { s as IslandsProvider, t as Outlet, c as createIsland, d as isolateIslands, a as renderServerTree, h as withinClientRoot } from "./_chunks/tree-opIpaCke.mjs";
package/dist/server.mjs CHANGED
@@ -1,6 +1,6 @@
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
- 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";
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-CtLpJPEB.mjs";
2
+ import { a as encode, r as decode } from "./_chunks/codec-C8to-RQW.mjs";
3
+ 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";
4
4
  import { n as setBuildDefaults, t as getBuildDefaults } from "./_chunks/build-COIcs7wm.mjs";
5
5
  // src/server/hooks.ts
6
6
  const HOOKS_KEY = Symbol.for("islx.server.hooks");
package/dist/vite.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { i as transformActions, r as hasModuleDirective, t as ActionCompileError } from "./_chunks/actions-Eutk6UzT.mjs";
2
- import { t as ISLAND_STRATEGIES } from "./_chunks/schedule-BFoGKRrU.mjs";
2
+ import { t as ISLAND_STRATEGIES } from "./_chunks/schedule-CjoybS3b.mjs";
3
3
  import { randomBytes } from "node:crypto";
4
4
  import path from "node:path";
5
5
  import { walk } from "estree-walker";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farm.js/islx",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
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",