octane 0.3.0 → 0.3.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.
Files changed (55) hide show
  1. package/dist/cjs/constants.cjs +18 -25
  2. package/dist/cjs/css.cjs +6 -0
  3. package/dist/cjs/dom-binding-protocol.cjs +37 -14
  4. package/dist/cjs/hydration/control-capture.cjs +12 -4
  5. package/dist/cjs/hydration/event-capture.cjs +6 -7
  6. package/dist/cjs/hydration-markers.cjs +40 -2
  7. package/dist/cjs/index.cjs +2 -0
  8. package/dist/cjs/runtime-tags.cjs +55 -0
  9. package/dist/cjs/runtime.cjs +235 -148
  10. package/dist/cjs/runtime.server.cjs +110 -80
  11. package/dist/cjs/server/streamed-signals.cjs +102 -64
  12. package/dist/cjs/signals/control-binding.cjs +2 -1
  13. package/dist/cjs/stream-protocol.cjs +9 -8
  14. package/dist/cjs/version.cjs +1 -1
  15. package/dist/compiler/compile.js +550 -35
  16. package/dist/compiler/dom-bindings.js +5 -0
  17. package/dist/compiler/hydrate-boundaries.js +118 -0
  18. package/dist/compiler/private-context.js +160 -0
  19. package/dist/constants.d.ts +7 -18
  20. package/dist/constants.js +11 -9
  21. package/dist/css.d.ts +7 -0
  22. package/dist/css.js +4 -0
  23. package/dist/dom-binding-program.js +12 -5
  24. package/dist/dom-binding-protocol.d.ts +14 -1
  25. package/dist/dom-binding-protocol.js +40 -14
  26. package/dist/hydration/control-capture.d.ts +2 -0
  27. package/dist/hydration/control-capture.js +18 -5
  28. package/dist/hydration/event-capture.js +1 -2
  29. package/dist/hydration/independent-island.js +5 -5
  30. package/dist/hydration/stream-delivery.d.ts +1 -1
  31. package/dist/hydration/stream-delivery.js +16 -9
  32. package/dist/hydration/stream-result-receiver.d.ts +1 -0
  33. package/dist/hydration/stream-result-receiver.js +8 -4
  34. package/dist/hydration-markers.d.ts +30 -0
  35. package/dist/hydration-markers.js +27 -1
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.js +2 -0
  38. package/dist/react/index.js +1 -1
  39. package/dist/react/react-compat-shared.js +5 -3
  40. package/dist/react/shared.d.ts +1 -3
  41. package/dist/react/shared.js +1 -1
  42. package/dist/runtime-tags.d.ts +34 -0
  43. package/dist/runtime-tags.js +22 -0
  44. package/dist/runtime.d.ts +9 -14
  45. package/dist/runtime.js +213 -106
  46. package/dist/runtime.server.d.ts +10 -5
  47. package/dist/runtime.server.js +101 -56
  48. package/dist/server/streamed-signals.d.ts +1 -0
  49. package/dist/server/streamed-signals.js +102 -64
  50. package/dist/signals/control-binding.js +4 -2
  51. package/dist/stream-protocol.js +20 -9
  52. package/dist/universal-core.js +8 -4
  53. package/dist/universal-native.js +1 -1
  54. package/dist/version.js +1 -1
  55. package/package.json +3 -3
@@ -117,10 +117,7 @@ function createStreamedRendererFrameStream(frames, options = {}) {
117
117
  let totalBytes = 0;
118
118
  let finished = false;
119
119
  let controller;
120
- const timer = setTimeout(
121
- () => fail(new Error("Streamed renderer response timed out.")),
122
- budget.timeoutMs
123
- );
120
+ let timer;
124
121
  const cleanup = () => {
125
122
  clearTimeout(timer);
126
123
  options.signal?.removeEventListener("abort", abort);
@@ -149,8 +146,13 @@ function createStreamedRendererFrameStream(frames, options = {}) {
149
146
  },
150
147
  async pull() {
151
148
  if (finished) return;
149
+ timer = setTimeout(
150
+ () => fail(new Error("Streamed renderer response timed out.")),
151
+ budget.timeoutMs
152
+ );
152
153
  try {
153
154
  const next = await iterator.next();
155
+ clearTimeout(timer);
154
156
  if (finished) return;
155
157
  if (next.done) {
156
158
  finished = true;
@@ -181,6 +183,9 @@ function createStreamedRendererFrameStream(frames, options = {}) {
181
183
  function escapeAttribute(value) {
182
184
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
183
185
  }
186
+ function byteLength(value) {
187
+ return typeof globalThis.Buffer === "function" ? globalThis.Buffer.byteLength(value, "utf8") : new TextEncoder().encode(value).byteLength;
188
+ }
184
189
  function streamedRendererFrameScript(frame, nonce) {
185
190
  const encoded = (0, import_streamed_signals_protocol.encodeStreamedRendererFrameForScript)(frame);
186
191
  return `<script ${import_stream_protocol.STREAM_SCRIPT_ATTR}${nonce === void 0 ? "" : ` nonce="${escapeAttribute(nonce)}"`}>globalThis.__octaneStreamedRenderer.receive(${encoded});</script>`;
@@ -191,10 +196,15 @@ function streamedSignalSelectionScript(identity, nonce) {
191
196
  return `<script ${import_stream_protocol.STREAM_SCRIPT_ATTR}${nonce === void 0 ? "" : ` nonce="${escapeAttribute(nonce)}"`}>(function(g){var k="__octaneStreamedRenderer",e=g[k];if(!e){var q=[];g[k]={version:1,frames:q,receive:function(f){if(q.length>=512){this.overflow=true;return;}q.push(f);}};}var z="__octaneStreamedSignalSelections",v=g[z];if(!v){var a=[];v=g[z]={version:1,identities:a,register:function(i){if(a.length>=256){this.overflow=true;return;}a.push(i);}};}v.register(${encoded});})(globalThis);</script>`;
192
197
  }
193
198
  function createStreamedSignalInjection(identity, result, options = {}) {
199
+ return createSignalInjection(identity, result, options, "Streamed renderer injection timed out.");
200
+ }
201
+ function createSignalInjection(identity, result, options, timeoutMessage) {
194
202
  const budget = limits(options);
195
203
  const iterator = createStreamedSignalResultFrames(identity, result, options)[Symbol.asyncIterator]();
196
204
  let queued = options.announceSelection ? streamedSignalSelectionScript(identity, options.nonce) : "";
197
205
  let pending = false;
206
+ let awaitingAcceptance = false;
207
+ let queuedBytes = queued === "" ? 0 : byteLength(queued);
198
208
  let finished = false;
199
209
  let subscribed = false;
200
210
  let totalBytes = 0;
@@ -225,11 +235,13 @@ function createStreamedSignalInjection(identity, result, options = {}) {
225
235
  rejectDone(error);
226
236
  };
227
237
  const pump = () => {
228
- if (!subscribed || pending || queued !== "" || finished) return;
238
+ if (!subscribed || pending || awaitingAcceptance || queued !== "" || finished) return;
229
239
  pending = true;
240
+ timer = setTimeout(() => fail(new Error(timeoutMessage)), budget.timeoutMs);
230
241
  void iterator.next().then(
231
242
  (next) => {
232
243
  pending = false;
244
+ clearTimeout(timer);
233
245
  if (finished) return;
234
246
  if (next.done) {
235
247
  finished = true;
@@ -238,13 +250,14 @@ function createStreamedSignalInjection(identity, result, options = {}) {
238
250
  return;
239
251
  }
240
252
  const html = streamedRendererFrameScript(next.value, options.nonce);
241
- const bytes = new TextEncoder().encode(html).byteLength;
253
+ const bytes = byteLength(html);
242
254
  if (bytes > budget.maxFrameBytes || totalBytes + bytes > budget.maxTotalBytes) {
243
255
  fail(new Error("Streamed renderer injection exceeded its byte budget."));
244
256
  return;
245
257
  }
246
258
  totalBytes += bytes;
247
259
  queued = html;
260
+ queuedBytes = bytes;
248
261
  notify?.();
249
262
  },
250
263
  (error) => {
@@ -256,17 +269,17 @@ function createStreamedSignalInjection(identity, result, options = {}) {
256
269
  const abort = () => {
257
270
  fail(options.signal?.reason ?? new DOMException("Aborted", "AbortError"));
258
271
  };
259
- timer = setTimeout(
260
- () => fail(new Error("Streamed renderer injection timed out.")),
261
- budget.timeoutMs
262
- );
263
272
  options.signal?.addEventListener("abort", abort, { once: true });
264
273
  if (options.signal?.aborted) abort();
265
274
  return {
266
275
  streamedRenderer: true,
276
+ get queuedBytes() {
277
+ return queuedBytes;
278
+ },
267
279
  take() {
268
280
  const html = queued;
269
281
  queued = "";
282
+ if (html !== "") awaitingAcceptance = true;
270
283
  return html;
271
284
  },
272
285
  subscribe(callback) {
@@ -278,6 +291,7 @@ function createStreamedSignalInjection(identity, result, options = {}) {
278
291
  };
279
292
  },
280
293
  accepted() {
294
+ awaitingAcceptance = false;
281
295
  pump();
282
296
  },
283
297
  cancel: fail,
@@ -296,17 +310,20 @@ function createAutomaticStreamedSignalInjection(options, external) {
296
310
  if (!Number.isSafeInteger(selectionGeneration) || selectionGeneration < 0) {
297
311
  throw new RangeError("selectionGeneration must be a nonnegative safe integer.");
298
312
  }
299
- const children = [];
313
+ const children = /* @__PURE__ */ new Set();
314
+ const ready = /* @__PURE__ */ new Set();
300
315
  const observedAttempts = /* @__PURE__ */ new Set();
301
316
  let initialSelections = [];
302
317
  let queued = "";
303
318
  let active;
319
+ let awaitingAcceptance = false;
320
+ let hasSignalAttempts = false;
321
+ let pumping = false;
304
322
  let subscribed = false;
305
323
  let renderComplete = false;
306
324
  let finished = false;
307
325
  let totalBytes = 0;
308
326
  let notify;
309
- let timer;
310
327
  let resolveDone;
311
328
  let rejectDone;
312
329
  const done = new Promise((resolve, reject) => {
@@ -314,6 +331,8 @@ function createAutomaticStreamedSignalInjection(options, external) {
314
331
  rejectDone = reject;
315
332
  });
316
333
  const cleanupChild = (child) => {
334
+ if (!children.delete(child)) return;
335
+ ready.delete(child);
317
336
  try {
318
337
  child.unsubscribe();
319
338
  } catch {
@@ -326,7 +345,6 @@ function createAutomaticStreamedSignalInjection(options, external) {
326
345
  const fail = (error) => {
327
346
  if (finished) return;
328
347
  finished = true;
329
- if (timer !== void 0) clearTimeout(timer);
330
348
  for (const child of children) {
331
349
  try {
332
350
  child.source.cancel?.(error);
@@ -337,11 +355,9 @@ function createAutomaticStreamedSignalInjection(options, external) {
337
355
  rejectDone(error);
338
356
  };
339
357
  const maybeDone = () => {
340
- if (finished || !renderComplete || active !== void 0 || queued !== "") return;
341
- for (const child of children) if (!child.done) return;
358
+ if (finished || !renderComplete || active !== void 0 || awaitingAcceptance || queued !== "" || children.size !== 0)
359
+ return;
342
360
  finished = true;
343
- if (timer !== void 0) clearTimeout(timer);
344
- for (const child of children) cleanupChild(child);
345
361
  resolveDone();
346
362
  };
347
363
  const takeInitialSelections = () => {
@@ -351,7 +367,7 @@ function createAutomaticStreamedSignalInjection(options, external) {
351
367
  for (const { identity, attempt } of selections ?? []) {
352
368
  if (attempt.signal.aborted) continue;
353
369
  const script = streamedSignalSelectionScript(identity, options.nonce);
354
- const bytes = new TextEncoder().encode(script).byteLength;
370
+ const bytes = byteLength(script);
355
371
  if (bytes > budget.maxFrameBytes || totalBytes + bytes > budget.maxTotalBytes) {
356
372
  const error = new Error("Automatic streamed signals exceeded their byte budget.");
357
373
  fail(error);
@@ -363,33 +379,41 @@ function createAutomaticStreamedSignalInjection(options, external) {
363
379
  return html;
364
380
  };
365
381
  const pump = () => {
366
- if (!subscribed || finished || active !== void 0 || queued !== "") return;
367
- if (initialSelections !== void 0) {
368
- try {
369
- queued = takeInitialSelections();
370
- } catch {
371
- return;
382
+ if (pumping || !subscribed || finished || active !== void 0 || awaitingAcceptance || queued !== "")
383
+ return;
384
+ pumping = true;
385
+ try {
386
+ if (initialSelections !== void 0) {
387
+ try {
388
+ queued = takeInitialSelections();
389
+ } catch {
390
+ return;
391
+ }
392
+ if (queued !== "") notify?.();
372
393
  }
373
- if (queued !== "") {
394
+ for (const child of ready) {
395
+ if (finished || active !== void 0 || awaitingAcceptance || queued !== "") return;
396
+ ready.delete(child);
397
+ const html = child.source.take();
398
+ if (finished) return;
399
+ if (html === "") {
400
+ if (child.done && (child.attempt !== void 0 || renderComplete)) cleanupChild(child);
401
+ continue;
402
+ }
403
+ const bytes = child.attempt === void 0 ? byteLength(html) : child.source.queuedBytes;
404
+ if (bytes > budget.maxFrameBytes || totalBytes + bytes > budget.maxTotalBytes) {
405
+ fail(new Error("Automatic streamed signals exceeded their byte budget."));
406
+ return;
407
+ }
408
+ totalBytes += bytes;
409
+ active = child;
410
+ queued = html;
374
411
  notify?.();
375
- return;
376
- }
377
- }
378
- for (const child of children) {
379
- const html = child.source.take();
380
- if (html === "") continue;
381
- const bytes = new TextEncoder().encode(html).byteLength;
382
- if (bytes > budget.maxFrameBytes || totalBytes + bytes > budget.maxTotalBytes) {
383
- fail(new Error("Automatic streamed signals exceeded their byte budget."));
384
- return;
385
412
  }
386
- totalBytes += bytes;
387
- active = child;
388
- queued = html;
389
- notify?.();
390
- return;
413
+ maybeDone();
414
+ } finally {
415
+ pumping = false;
391
416
  }
392
- maybeDone();
393
417
  };
394
418
  const addChild = (source, release, attempt) => {
395
419
  const child = {
@@ -400,17 +424,23 @@ function createAutomaticStreamedSignalInjection(options, external) {
400
424
  ...attempt === void 0 ? {} : { attempt },
401
425
  done: false
402
426
  };
403
- children.push(child);
404
- child.unsubscribe = source.subscribe(pump);
427
+ children.add(child);
428
+ const available = () => {
429
+ if (finished || !children.has(child)) return;
430
+ ready.add(child);
431
+ pump();
432
+ };
433
+ ready.add(child);
434
+ child.unsubscribe = source.subscribe(available);
405
435
  source.done.then(
406
436
  () => {
407
437
  child.done = true;
408
- pump();
438
+ available();
409
439
  },
410
440
  (error) => {
411
441
  if (child.attempt !== void 0 && !child.attempt.isCurrent()) {
412
442
  child.done = true;
413
- pump();
443
+ available();
414
444
  } else fail(error);
415
445
  }
416
446
  );
@@ -421,7 +451,7 @@ function createAutomaticStreamedSignalInjection(options, external) {
421
451
  takeInitialSelections,
422
452
  createSignalAttemptObservations: import_signal_query_observation.createServerSignalQueryAttemptObservations,
423
453
  get streamedRenderer() {
424
- return external?.streamedRenderer === true || children.some((child) => child.attempt !== void 0) ? true : void 0;
454
+ return external?.streamedRenderer === true || hasSignalAttempts ? true : void 0;
425
455
  },
426
456
  observeSignalAttempt(attempt, run) {
427
457
  if (finished || !attempt.isCurrent()) {
@@ -439,18 +469,13 @@ function createAutomaticStreamedSignalInjection(options, external) {
439
469
  attempt.release();
440
470
  return;
441
471
  }
442
- if (children.length >= 256) {
472
+ if (children.size >= 256) {
443
473
  attempt.release();
444
474
  fail(new Error("Automatic streamed signals exceeded their channel budget."));
445
475
  return;
446
476
  }
447
477
  observedAttempts.add(attemptKey);
448
- if (timer === void 0) {
449
- timer = setTimeout(
450
- () => fail(new Error("Automatic streamed signals timed out.")),
451
- budget.timeoutMs
452
- );
453
- }
478
+ hasSignalAttempts = true;
454
479
  const identity = {
455
480
  protocol: 1,
456
481
  buildId: options.buildId,
@@ -463,21 +488,27 @@ function createAutomaticStreamedSignalInjection(options, external) {
463
488
  attempt: attempt.attempt
464
489
  };
465
490
  initialSelections?.push({ identity, attempt });
466
- const source = createStreamedSignalInjection(identity, attempt.result, {
467
- signal: attempt.signal,
468
- run,
469
- nonce: options.nonce,
470
- announceSelection: initialSelections === void 0,
471
- maxFrameBytes: budget.maxFrameBytes,
472
- maxTotalBytes: budget.maxTotalBytes,
473
- timeoutMs: budget.timeoutMs
474
- });
491
+ const source = createSignalInjection(
492
+ identity,
493
+ attempt.result,
494
+ {
495
+ signal: attempt.signal,
496
+ run,
497
+ nonce: options.nonce,
498
+ announceSelection: initialSelections === void 0,
499
+ maxFrameBytes: budget.maxFrameBytes,
500
+ maxTotalBytes: budget.maxTotalBytes,
501
+ timeoutMs: budget.timeoutMs
502
+ },
503
+ "Automatic streamed signals timed out."
504
+ );
475
505
  addChild(source, attempt.release, attempt);
476
506
  pump();
477
507
  },
478
508
  take() {
479
509
  const html = queued;
480
510
  queued = "";
511
+ if (html !== "") awaitingAcceptance = true;
481
512
  return html;
482
513
  },
483
514
  subscribe(callback) {
@@ -490,15 +521,22 @@ function createAutomaticStreamedSignalInjection(options, external) {
490
521
  },
491
522
  accepted() {
492
523
  const child = active;
493
- active = void 0;
494
524
  child?.source.accepted?.();
525
+ if (child !== void 0 && children.has(child)) ready.add(child);
526
+ active = void 0;
527
+ awaitingAcceptance = false;
495
528
  pump();
496
529
  },
497
530
  cancel: fail,
498
531
  done,
499
532
  renderComplete() {
500
533
  renderComplete = true;
501
- for (const child of children) child.source.renderComplete?.();
534
+ external?.renderComplete?.();
535
+ for (const child of children) {
536
+ if (child.attempt !== void 0) child.source.renderComplete?.();
537
+ if (child.done) ready.add(child);
538
+ }
539
+ pump();
502
540
  maybeDone();
503
541
  }
504
542
  };
@@ -260,7 +260,8 @@ function __createBindingControls(owner = (0, import_owner_context.currentSignalO
260
260
  let snapshot;
261
261
  do {
262
262
  snapshot = (0, import_control_capture.snapshotHydrationControl)(control);
263
- if (snapshot.editRevision > 0 && (0, import_handle_protocol.isWritableSignal)(active)) write(nativeValue());
263
+ if ((0, import_handle_protocol.isWritableSignal)(active) && (snapshot.editRevision > 0 || channel === "value" && (0, import_control_capture.isRestoredHydrationTextarea)(control, read(active))))
264
+ write(nativeValue());
264
265
  if (disposed) return;
265
266
  } while (!(0, import_control_capture.consumeHydrationControl)(control, snapshot.revision));
266
267
  },
@@ -30,15 +30,16 @@ __export(stream_protocol_exports, {
30
30
  });
31
31
  module.exports = __toCommonJS(stream_protocol_exports);
32
32
  var import_dom_binding_protocol = require("./dom-binding-protocol.cjs");
33
+ var import_hydration_markers = require("./hydration-markers.cjs");
33
34
  const STREAM_BOUNDARY_ATTR = "data-oct-b";
34
35
  const SUSPENSE_SCRIPT_ATTR = "data-octane-suspense";
35
36
  const STREAM_SCRIPT_ATTR = "data-octane-stream";
36
37
  const HYDRATE_STREAM_TOKEN_ATTR = "data-octane-stream-token";
37
38
  function hydrationMarkerMultiplicity(data, open) {
38
- const marker = open ? "[" : "]";
39
+ const marker = open ? import_hydration_markers.HYDRATION_START : import_hydration_markers.HYDRATION_END;
39
40
  if (data === marker) return 1;
40
- if (open && (data === "[f0" || data === "[f1")) return 1;
41
- if (open && (data.startsWith("[b;") || data.startsWith("[f")) && (0, import_dom_binding_protocol.isBindingOpenComment)(data))
41
+ if (open && (data === import_hydration_markers.HYDRATION_FOR_EMPTY || data === import_hydration_markers.HYDRATION_FOR_ITEMS)) return 1;
42
+ if (open && (data.startsWith(import_dom_binding_protocol.BINDING_OPEN_PREFIX) || data.startsWith(import_hydration_markers.HYDRATION_FOR_PREFIX)) && (0, import_dom_binding_protocol.isBindingOpenComment)(data))
42
43
  return 1;
43
44
  if (data.length < 2 || data.charCodeAt(0) !== marker.charCodeAt(0)) return 0;
44
45
  const first = data.charCodeAt(1);
@@ -78,8 +79,8 @@ function getLeadingHydrationListRange(host) {
78
79
  let enclosingEnd = null;
79
80
  while (start !== null && start.nodeType === 8) {
80
81
  const data = start.data;
81
- const list = data === "[f0" || data === "[f1" || (data.startsWith("[f0;b;") || data.startsWith("[f1;b;")) && (0, import_dom_binding_protocol.isBindingOpenComment)(data);
82
- const wrapperMultiplicity = data === "[" ? 1 : data.charCodeAt(1) >= 49 && data.charCodeAt(1) <= 57 ? hydrationMarkerMultiplicity(data, true) : 0;
82
+ const list = data === import_hydration_markers.HYDRATION_FOR_EMPTY || data === import_hydration_markers.HYDRATION_FOR_ITEMS || (0, import_dom_binding_protocol.isForBindingOpenComment)(data);
83
+ const wrapperMultiplicity = data === import_hydration_markers.HYDRATION_START ? 1 : data.charCodeAt(1) >= 49 && data.charCodeAt(1) <= 57 ? hydrationMarkerMultiplicity(data, true) : 0;
83
84
  if (!list && wrapperMultiplicity === 0) return null;
84
85
  const end = rendererRangeClose(start);
85
86
  if (end === null || enclosingEnd !== null && (end.compareDocumentPosition(enclosingEnd) & 4) === 0)
@@ -87,12 +88,12 @@ function getLeadingHydrationListRange(host) {
87
88
  if (hydrationMarkerMultiplicity(end.data, false) !== (list ? 1 : wrapperMultiplicity))
88
89
  return null;
89
90
  if (list) {
90
- const suffix = data.slice(3);
91
+ const suffix = data.slice(import_hydration_markers.HYDRATION_FOR_EMPTY.length);
91
92
  return {
92
93
  start,
93
94
  end,
94
- emptyMarker: "[f0" + suffix,
95
- itemsMarker: "[f1" + suffix
95
+ emptyMarker: import_hydration_markers.HYDRATION_FOR_EMPTY + suffix,
96
+ itemsMarker: import_hydration_markers.HYDRATION_FOR_ITEMS + suffix
96
97
  };
97
98
  }
98
99
  enclosingEnd = end;
@@ -21,7 +21,7 @@ __export(version_exports, {
21
21
  version: () => version
22
22
  });
23
23
  module.exports = __toCommonJS(version_exports);
24
- const version = "0.3.0";
24
+ const version = "0.3.2";
25
25
  // Annotate the CommonJS export names for ESM import in node:
26
26
  0 && (module.exports = {
27
27
  version