foldkit 0.146.0 → 0.148.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/buildToken.d.ts +3 -0
- package/dist/buildToken.d.ts.map +1 -0
- package/dist/buildToken.js +21 -0
- package/dist/controlledDomState.d.ts +25 -0
- package/dist/controlledDomState.d.ts.map +1 -0
- package/dist/controlledDomState.js +240 -0
- package/dist/cssStyleProperties.d.ts +6 -0
- package/dist/cssStyleProperties.d.ts.map +1 -0
- package/dist/cssStyleProperties.js +91 -0
- package/dist/customElement/index.d.ts.map +1 -1
- package/dist/customElement/index.js +23 -0
- package/dist/domReflection.d.ts +73 -0
- package/dist/domReflection.d.ts.map +1 -0
- package/dist/domReflection.js +557 -0
- package/dist/experimental/index.d.ts +1 -0
- package/dist/experimental/index.d.ts.map +1 -1
- package/dist/experimental/index.js +1 -0
- package/dist/experimental/machine/machine.d.ts +22 -3
- package/dist/experimental/machine/machine.d.ts.map +1 -1
- package/dist/experimental/machine/machine.js +8 -0
- package/dist/experimental/server/entry.d.ts +73 -0
- package/dist/experimental/server/entry.d.ts.map +1 -0
- package/dist/experimental/server/entry.js +41 -0
- package/dist/experimental/server/host.d.ts +136 -0
- package/dist/experimental/server/host.d.ts.map +1 -0
- package/dist/experimental/server/host.js +359 -0
- package/dist/experimental/server/index.d.ts +5 -0
- package/dist/experimental/server/index.d.ts.map +1 -0
- package/dist/experimental/server/index.js +4 -0
- package/dist/experimental/server/public.d.ts +3 -0
- package/dist/experimental/server/public.d.ts.map +1 -0
- package/dist/experimental/server/public.js +1 -0
- package/dist/experimental/server/serialize.d.ts +43 -0
- package/dist/experimental/server/serialize.d.ts.map +1 -0
- package/dist/experimental/server/serialize.js +786 -0
- package/dist/experimental/server/server.d.ts +242 -0
- package/dist/experimental/server/server.d.ts.map +1 -0
- package/dist/experimental/server/server.js +946 -0
- package/dist/experimental/server/template.d.ts +53 -0
- package/dist/experimental/server/template.d.ts.map +1 -0
- package/dist/experimental/server/template.js +617 -0
- package/dist/html/index.d.ts +5 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +474 -33
- package/dist/hydrate.d.ts +4 -0
- package/dist/hydrate.d.ts.map +1 -0
- package/dist/hydrate.js +850 -0
- package/dist/hydrationMarker.d.ts +10 -0
- package/dist/hydrationMarker.d.ts.map +1 -0
- package/dist/hydrationMarker.js +9 -0
- package/dist/hydrationMarkers.d.ts +15 -0
- package/dist/hydrationMarkers.d.ts.map +1 -0
- package/dist/hydrationMarkers.js +70 -0
- package/dist/nativeInnerHtml.d.ts +13 -0
- package/dist/nativeInnerHtml.d.ts.map +1 -0
- package/dist/nativeInnerHtml.js +30 -0
- package/dist/propertyProvenance.d.ts +33 -0
- package/dist/propertyProvenance.d.ts.map +1 -0
- package/dist/propertyProvenance.js +78 -0
- package/dist/propsModule.d.ts.map +1 -1
- package/dist/propsModule.js +149 -15
- package/dist/runtime/public.d.ts +2 -2
- package/dist/runtime/public.d.ts.map +1 -1
- package/dist/runtime/public.js +1 -1
- package/dist/runtime/runtime.d.ts +99 -36
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +412 -71
- package/dist/snabbdom/attributes.d.ts.map +1 -1
- package/dist/snabbdom/attributes.js +65 -37
- package/dist/snabbdom/h.d.ts +1 -0
- package/dist/snabbdom/h.d.ts.map +1 -1
- package/dist/snabbdom/h.js +85 -4
- package/dist/snabbdom/style.d.ts.map +1 -1
- package/dist/snabbdom/style.js +53 -34
- package/dist/snabbdom/tovnode.d.ts.map +1 -1
- package/dist/snabbdom/tovnode.js +5 -1
- package/dist/tagName.d.ts +6 -0
- package/dist/tagName.d.ts.map +1 -0
- package/dist/tagName.js +11 -0
- package/dist/test/apps/attributes.d.ts +1 -0
- package/dist/test/apps/attributes.d.ts.map +1 -1
- package/dist/test/apps/attributes.js +8 -1
- package/dist/test/apps/login.js +1 -1
- package/dist/test/matchers.d.ts.map +1 -1
- package/dist/test/matchers.js +2 -1
- package/dist/test/scene.d.ts.map +1 -1
- package/dist/test/scene.js +2 -1
- package/dist/vdom.d.ts.map +1 -1
- package/dist/vdom.js +25 -1
- package/package.json +9 -2
package/dist/runtime/runtime.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { Array, Cause, Context, Duration, Effect, Exit, Fiber, Function, Layer, Match, Option, Predicate, PubSub, Record, Ref, Runtime, Scheduler, Schema, Stream, SubscriptionRef, pipe, } from 'effect';
|
|
2
|
+
import { HYDRATION_BUILD_ATTRIBUTE } from '../buildToken.js';
|
|
2
3
|
import { __CurrentRegistry as __CurrentInterruptRegistry, __makeRegistry as __makeInterruptRegistry, } from '../command/interruptible/index.js';
|
|
3
4
|
import { createDevToolsStore, } from '../devTools/store.js';
|
|
4
5
|
import { startWebSocketBridge } from '../devTools/webSocketBridge.js';
|
|
5
|
-
import { __beginRender as beginHtmlRender, __beginReplayRender as beginReplayHtmlRender, __clearRuntime as clearHtmlRuntime, __createBoundaryRegistry as createHtmlBoundaryRegistry, __endReplayRender as endReplayHtmlRender, __htmlBuilder as htmlBuilderFor, __setRuntime as setHtmlRuntime, } from '../html/index.js';
|
|
6
|
+
import { __beginRender as beginHtmlRender, __beginReplayRender as beginReplayHtmlRender, __clearRuntime as clearHtmlRuntime, __createBoundaryRegistry as createHtmlBoundaryRegistry, __endReplayRender as endReplayHtmlRender, __htmlBuilder as htmlBuilderFor, __setRuntime as setHtmlRuntime, textDirectionToAttribute, } from '../html/index.js';
|
|
7
|
+
import { __hydrateVNode } from '../hydrate.js';
|
|
8
|
+
import { FOLDKIT_APP_ATTRIBUTE, FOLDKIT_FLAGS_ATTRIBUTE, } from '../hydrationMarker.js';
|
|
6
9
|
import { MountTracker } from '../mount/index.js';
|
|
7
10
|
import { __CurrentPortChannels, __makeInboundChannel, } from '../port/index.js';
|
|
8
11
|
import { RenderCommit, createCommitNotifier } from '../render/commit.js';
|
|
@@ -191,6 +194,205 @@ const createDuplicateIdScanner = () => {
|
|
|
191
194
|
/** Effect service tag that provides message dispatching to the view layer. */
|
|
192
195
|
export class Dispatch extends Context.Service()('@foldkit/Dispatch') {
|
|
193
196
|
}
|
|
197
|
+
const hydrationForRoot = (root, isFlagsRequired) => {
|
|
198
|
+
const runtimeId = root.getAttribute(FOLDKIT_APP_ATTRIBUTE) ?? '';
|
|
199
|
+
const flagsScripts = pipe(Array.fromIterable(document.querySelectorAll(`script[${FOLDKIT_FLAGS_ATTRIBUTE}]`)), Array.filter(script => script.getAttribute(FOLDKIT_FLAGS_ATTRIBUTE) === runtimeId));
|
|
200
|
+
return { root, runtimeId, flagsScripts, isFlagsRequired };
|
|
201
|
+
};
|
|
202
|
+
// NOTE: hydration is scoped to the app's own stamped root so a server-rendered
|
|
203
|
+
// app never adopts another app's DOM. A container that carries the stamp is that
|
|
204
|
+
// root. A container that does not is a non-root element the caller resolved,
|
|
205
|
+
// which happens when the rendered view has its own element with the container's
|
|
206
|
+
// id (a descendant `id="root"`) and `getElementById` returned that inner element
|
|
207
|
+
// instead of the stamped root above it. That inner element resolves to the app
|
|
208
|
+
// root only when the page has exactly one stamped root and this container sits
|
|
209
|
+
// inside it, so a sibling widget (a stamped root the container is not inside) or
|
|
210
|
+
// an outer application's root (when a nested page carries more than one stamped
|
|
211
|
+
// root) is never wrongly adopted; those fall through to a fresh boot or a hard
|
|
212
|
+
// failure instead. A null container is the replace-parity case, where the server
|
|
213
|
+
// root took the placeholder's place and `getElementById` no longer finds it, so
|
|
214
|
+
// the stamp is the only handle; more than one stamped root is then ambiguous and
|
|
215
|
+
// a hard error rather than a silent wrong-DOM adoption.
|
|
216
|
+
// A runtime id names one application for the whole page: it pairs a root with
|
|
217
|
+
// its Flags payload, and it keys the Model and scroll position hot reloading
|
|
218
|
+
// preserves. Two roots sharing one are not two applications but one claimed
|
|
219
|
+
// twice, so whichever boots second would read the other's handoff and restore
|
|
220
|
+
// the other's Model. `injectIntoTemplate` refuses to build such a page; this is
|
|
221
|
+
// the check for a page assembled some other way.
|
|
222
|
+
//
|
|
223
|
+
// Distinct ids do not make two hydrated applications independent, which is not
|
|
224
|
+
// a supported arrangement: each page-owning application rewrites the document's
|
|
225
|
+
// metadata and installs document-wide navigation listeners.
|
|
226
|
+
const assertRuntimeIdsAreUnique = (stampedRoots) => {
|
|
227
|
+
const seen = new Set();
|
|
228
|
+
for (const root of stampedRoots) {
|
|
229
|
+
const runtimeId = root.getAttribute(FOLDKIT_APP_ATTRIBUTE) ?? '';
|
|
230
|
+
if (seen.has(runtimeId)) {
|
|
231
|
+
containRefusedPage(root.ownerDocument);
|
|
232
|
+
throw new Error(`[foldkit] Found more than one server-rendered root stamped ` +
|
|
233
|
+
`"${runtimeId}". A runtime id names one application for the whole ` +
|
|
234
|
+
'page: it pairs a root with its Flags payload and keys the Model and ' +
|
|
235
|
+
'scroll position hot reloading preserves, so two roots sharing one ' +
|
|
236
|
+
"would take each other's state. Render each application with its " +
|
|
237
|
+
'own `runtimeId`.');
|
|
238
|
+
}
|
|
239
|
+
seen.add(runtimeId);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
// The reason this page cannot be adopted by this client, or `undefined` when
|
|
243
|
+
// the two name the same deployment.
|
|
244
|
+
//
|
|
245
|
+
// The client's id is required and must be non-empty. An absent one would
|
|
246
|
+
// otherwise equal the absent marker on a page served before build ids existed,
|
|
247
|
+
// which reads a page from an unknown deployment as one of this build's own: the
|
|
248
|
+
// exact case the id exists to refuse.
|
|
249
|
+
const buildSkew = (root, buildId, runtimeId) => {
|
|
250
|
+
if (buildId === undefined || buildId === '') {
|
|
251
|
+
return new Error('[foldkit] Runtime.hydrate was given no build id. Hydration compares ' +
|
|
252
|
+
'the id the server stamped on the root with this client’s own before ' +
|
|
253
|
+
'it adopts any DOM, and without one a page from any deployment would ' +
|
|
254
|
+
'be adopted as this one. Pass ' +
|
|
255
|
+
'`buildId: import.meta.env.FOLDKIT_BUILD_ID`, the same value the ' +
|
|
256
|
+
'server entry passes to `renderToString`.');
|
|
257
|
+
}
|
|
258
|
+
const servedBuild = root.getAttribute(HYDRATION_BUILD_ATTRIBUTE);
|
|
259
|
+
if (servedBuild === buildId) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
return new Error(`[foldkit] Runtime.hydrate found application "${runtimeId}" served by ` +
|
|
263
|
+
`${servedBuild === null ? 'no known deployment' : `deployment "${servedBuild}"`}` +
|
|
264
|
+
`, but this client belongs to deployment "${buildId}". Startup stops ` +
|
|
265
|
+
'here rather than reading a handoff written by other code: the Flags in ' +
|
|
266
|
+
'the page are that deployment’s, and this build could accept them while ' +
|
|
267
|
+
'every value in them means something else. Serve the page from the ' +
|
|
268
|
+
'running deployment, and keep stale HTML out of shared caches.');
|
|
269
|
+
};
|
|
270
|
+
const REFUSED_ATTRIBUTE = 'data-foldkit-refused';
|
|
271
|
+
const REFUSAL_SHIELD_ATTRIBUTE = 'data-foldkit-refusal-shield';
|
|
272
|
+
const refusalShields = new WeakMap();
|
|
273
|
+
const REFUSAL_SHIELD_INPUT_EVENTS = [
|
|
274
|
+
'auxclick',
|
|
275
|
+
'click',
|
|
276
|
+
'contextmenu',
|
|
277
|
+
'dblclick',
|
|
278
|
+
'keydown',
|
|
279
|
+
'keypress',
|
|
280
|
+
'keyup',
|
|
281
|
+
'mousedown',
|
|
282
|
+
'mouseup',
|
|
283
|
+
'pointerdown',
|
|
284
|
+
'pointerup',
|
|
285
|
+
'touchend',
|
|
286
|
+
'touchstart',
|
|
287
|
+
];
|
|
288
|
+
const preventRefusalShieldInteraction = (event) => {
|
|
289
|
+
event.preventDefault();
|
|
290
|
+
event.stopImmediatePropagation();
|
|
291
|
+
};
|
|
292
|
+
const openRefusalShield = (shield) => {
|
|
293
|
+
shield.inert = true;
|
|
294
|
+
shield.setAttribute('inert', '');
|
|
295
|
+
if (shield.open && typeof shield.close === 'function') {
|
|
296
|
+
shield.close();
|
|
297
|
+
}
|
|
298
|
+
if (typeof shield.showModal === 'function') {
|
|
299
|
+
shield.showModal();
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
shield.setAttribute('open', '');
|
|
303
|
+
}
|
|
304
|
+
shield.inert = false;
|
|
305
|
+
shield.removeAttribute('inert');
|
|
306
|
+
shield.focus({ preventScroll: true });
|
|
307
|
+
};
|
|
308
|
+
const installRefusalShield = (ownerDocument) => {
|
|
309
|
+
const existing = refusalShields.get(ownerDocument);
|
|
310
|
+
if (existing !== undefined && existing.isConnected) {
|
|
311
|
+
openRefusalShield(existing);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const shield = ownerDocument.createElement('dialog');
|
|
315
|
+
shield.setAttribute(REFUSAL_SHIELD_ATTRIBUTE, '');
|
|
316
|
+
shield.setAttribute('aria-label', 'Page unavailable');
|
|
317
|
+
shield.setAttribute('aria-modal', 'true');
|
|
318
|
+
shield.setAttribute('closedby', 'none');
|
|
319
|
+
shield.tabIndex = -1;
|
|
320
|
+
shield.textContent =
|
|
321
|
+
'This page could not start safely. Reload to get the current version.';
|
|
322
|
+
shield.style.alignItems = 'center';
|
|
323
|
+
shield.style.background = 'rgba(15, 23, 42, 0.96)';
|
|
324
|
+
shield.style.border = '0';
|
|
325
|
+
shield.style.boxSizing = 'border-box';
|
|
326
|
+
shield.style.color = 'white';
|
|
327
|
+
shield.style.font = '600 1rem/1.5 system-ui, sans-serif';
|
|
328
|
+
shield.style.display = 'grid';
|
|
329
|
+
shield.style.height = '100vh';
|
|
330
|
+
shield.style.inset = '0';
|
|
331
|
+
shield.style.margin = '0';
|
|
332
|
+
shield.style.maxHeight = 'none';
|
|
333
|
+
shield.style.maxWidth = 'none';
|
|
334
|
+
shield.style.overflow = 'hidden';
|
|
335
|
+
shield.style.padding = '2rem';
|
|
336
|
+
shield.style.position = 'fixed';
|
|
337
|
+
shield.style.touchAction = 'none';
|
|
338
|
+
shield.style.userSelect = 'none';
|
|
339
|
+
shield.style.width = '100vw';
|
|
340
|
+
shield.addEventListener('cancel', preventRefusalShieldInteraction);
|
|
341
|
+
for (const eventName of REFUSAL_SHIELD_INPUT_EVENTS) {
|
|
342
|
+
shield.addEventListener(eventName, preventRefusalShieldInteraction, {
|
|
343
|
+
capture: true,
|
|
344
|
+
passive: false,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
ownerDocument.documentElement.appendChild(shield);
|
|
348
|
+
refusalShields.set(ownerDocument, shield);
|
|
349
|
+
openRefusalShield(shield);
|
|
350
|
+
};
|
|
351
|
+
const containRefusedPage = (ownerDocument) => {
|
|
352
|
+
const boundary = ownerDocument.body ?? ownerDocument.documentElement;
|
|
353
|
+
boundary.inert = true;
|
|
354
|
+
boundary.setAttribute('inert', '');
|
|
355
|
+
boundary.setAttribute('aria-hidden', 'true');
|
|
356
|
+
boundary.setAttribute(REFUSED_ATTRIBUTE, '');
|
|
357
|
+
installRefusalShield(ownerDocument);
|
|
358
|
+
};
|
|
359
|
+
// Whether this page carries anything a Foldkit server render leaves behind. A
|
|
360
|
+
// resolution failure on such a page is a refused handoff, and the markup is
|
|
361
|
+
// contained; the same failure on a page with none of these markers is a client
|
|
362
|
+
// application whose container never existed, where there is no server render to
|
|
363
|
+
// refuse and nothing to take out of reach.
|
|
364
|
+
const hasServerRenderedMarkup = (ownerDocument) => ownerDocument.querySelector(`[${FOLDKIT_APP_ATTRIBUTE}], [${HYDRATION_BUILD_ATTRIBUTE}], ` +
|
|
365
|
+
`[${FOLDKIT_FLAGS_ATTRIBUTE}]`) !== null;
|
|
366
|
+
const findDocumentHydration = (container, isFlagsRequired) => {
|
|
367
|
+
const stampedRoots = Array.fromIterable(document.querySelectorAll(`[${FOLDKIT_APP_ATTRIBUTE}]`));
|
|
368
|
+
assertRuntimeIdsAreUnique(stampedRoots);
|
|
369
|
+
if (container !== null) {
|
|
370
|
+
if (container.hasAttribute(FOLDKIT_APP_ATTRIBUTE)) {
|
|
371
|
+
return hydrationForRoot(container, isFlagsRequired);
|
|
372
|
+
}
|
|
373
|
+
return Array.match(stampedRoots, {
|
|
374
|
+
onEmpty: () => undefined,
|
|
375
|
+
onNonEmpty: roots => {
|
|
376
|
+
const onlyRoot = Array.headNonEmpty(roots);
|
|
377
|
+
return roots.length === 1 &&
|
|
378
|
+
container.closest(`[${FOLDKIT_APP_ATTRIBUTE}]`) === onlyRoot
|
|
379
|
+
? hydrationForRoot(onlyRoot, isFlagsRequired)
|
|
380
|
+
: undefined;
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
if (stampedRoots.length > 1) {
|
|
385
|
+
containRefusedPage(document);
|
|
386
|
+
throw new Error('[foldkit] Found multiple server-rendered roots stamped with ' +
|
|
387
|
+
`\`${FOLDKIT_APP_ATTRIBUTE}\` but no container to disambiguate them. ` +
|
|
388
|
+
'Give each app its own container element so the runtime can tell ' +
|
|
389
|
+
'which root to hydrate.');
|
|
390
|
+
}
|
|
391
|
+
return Option.match(Array.head(stampedRoots), {
|
|
392
|
+
onNone: () => undefined,
|
|
393
|
+
onSome: root => hydrationForRoot(root, isFlagsRequired),
|
|
394
|
+
});
|
|
395
|
+
};
|
|
194
396
|
const makeHostConnector = () => {
|
|
195
397
|
let isDisposed = false;
|
|
196
398
|
let maybeDeliverInbound = Option.none();
|
|
@@ -314,7 +516,7 @@ const validatePorts = (ports) => {
|
|
|
314
516
|
});
|
|
315
517
|
};
|
|
316
518
|
const runtimeInternals = new WeakMap();
|
|
317
|
-
const makeRuntime = ({ ports, Model,
|
|
519
|
+
const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, isFlagsRequired, init, update, view, manageDocument, subscriptions, container, hydration, routing: routingConfig, crash, slow, viewTransition, freezeModel, preserveScroll, resources, managedResources, devTools, }) => {
|
|
318
520
|
const isSlowVisible = (show) => Match.value(show).pipe(Match.when('Always', () => true), Match.when('Development', () => !!import.meta.hot), Match.exhaustive);
|
|
319
521
|
const htmlBuilder = htmlBuilderFor();
|
|
320
522
|
const resolvedSlow = __resolveSlowConfig(slow, isSlowVisible);
|
|
@@ -352,8 +554,10 @@ const makeRuntime = ({ ports, Model, flags: maybeResolveFlags, init, update, vie
|
|
|
352
554
|
if (Predicate.isNotUndefined(ports)) {
|
|
353
555
|
validatePorts(ports);
|
|
354
556
|
}
|
|
355
|
-
const runtimeId =
|
|
356
|
-
|
|
557
|
+
const runtimeId = hydration !== undefined && hydration.runtimeId !== ''
|
|
558
|
+
? hydration.runtimeId
|
|
559
|
+
: (container?.id ?? '');
|
|
560
|
+
const startWith = (maybeConnector, hmrModel, bootMode = 'Fresh', bootFlags, buildId) => {
|
|
357
561
|
// NOTE: one notifier per runtime, provided across the whole runtime
|
|
358
562
|
// Effect so Commands, Subscriptions, and Mount-forked Effects all resolve
|
|
359
563
|
// the same signal. A commit in one embedded application must never wake a
|
|
@@ -362,7 +566,8 @@ const makeRuntime = ({ ports, Model, flags: maybeResolveFlags, init, update, vie
|
|
|
362
566
|
return Effect.scoped(Effect.gen(function* () {
|
|
363
567
|
if (runtimeId === '') {
|
|
364
568
|
return yield* Effect.die(new Error('[foldkit] Runtime container must have an `id` for HMR model preservation. ' +
|
|
365
|
-
'Set `container.id = "app"` (or any unique string) before passing it to makeApplication or makeElement.'
|
|
569
|
+
'Set `container.id = "app"` (or any unique string) before passing it to makeApplication or makeElement. ' +
|
|
570
|
+
'On a server-rendered page the id comes from the `data-foldkit-app` root stamp instead.'));
|
|
366
571
|
}
|
|
367
572
|
// NOTE: every perpetual fiber (for example, Subscription streams
|
|
368
573
|
// and ManagedResource lifecycles) and every Command fiber forks
|
|
@@ -434,28 +639,28 @@ const makeRuntime = ({ ports, Model, flags: maybeResolveFlags, init, update, vie
|
|
|
434
639
|
});
|
|
435
640
|
return Effect.provideService(withPortChannels, __CurrentInterruptRegistry, interruptRegistry);
|
|
436
641
|
};
|
|
437
|
-
// NOTE:
|
|
642
|
+
// NOTE: Flags run through the same cached build that Commands and
|
|
438
643
|
// Subscriptions use, rather than being handed the Layer again, so a
|
|
439
644
|
// service needed both at startup and by a Command is constructed
|
|
440
|
-
// once. An app without
|
|
645
|
+
// once. An app without Flags never reaches it, which keeps the Layer
|
|
441
646
|
// lazy when the first thing that needs it is a Command.
|
|
442
647
|
//
|
|
443
648
|
// NOTE: a Layer that fails to build is not fatal here. Flags resolve
|
|
444
649
|
// before `init`, so there is no Model for a crash view to render
|
|
445
650
|
// against and a failure escaping this point kills the app with a
|
|
446
|
-
// blank container. Running
|
|
447
|
-
// lets an app whose
|
|
448
|
-
// before
|
|
651
|
+
// blank container. Running Flags against an empty context instead
|
|
652
|
+
// lets an app whose Flags never touch the Layer boot as it did
|
|
653
|
+
// before Flags could consume `resources`: the cached failure then
|
|
449
654
|
// surfaces at the first Command or Subscription, where `crashWith`
|
|
450
655
|
// does render the crash view. Flags that do need the Layer still
|
|
451
656
|
// fail here, and both causes are reported: the `Service not found`
|
|
452
657
|
// defect the empty context produced is useless on its own, and the
|
|
453
658
|
// build failure that explains it would be lost if it replaced the
|
|
454
|
-
//
|
|
659
|
+
// Flags cause outright. Combining them also keeps a Flags Effect
|
|
455
660
|
// that fails for its own unrelated reason visible instead of
|
|
456
661
|
// attributing its defect to the Layer. Interrupts propagate
|
|
457
662
|
// untouched on both sides, because dispose racing either the build
|
|
458
|
-
// or the
|
|
663
|
+
// or the Flags run is not a failure to recover from, and
|
|
459
664
|
// `Effect.catchCause` hands the handler interrupt causes too.
|
|
460
665
|
const provideResources = (effect) => Option.match(maybeAcquireResourceContext, {
|
|
461
666
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
@@ -471,23 +676,102 @@ const makeRuntime = ({ ports, Model, flags: maybeResolveFlags, init, update, vie
|
|
|
471
676
|
onSuccess: resourceContext => Effect.provideContext(effect, resourceContext),
|
|
472
677
|
}),
|
|
473
678
|
});
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
onNone: () =>
|
|
679
|
+
const maybeResolveFreshFlags = Option.orElse(Option.fromNullishOr(bootFlags), () => configuredFlags);
|
|
680
|
+
const resolveFreshFlags = Option.match(maybeResolveFreshFlags, {
|
|
681
|
+
onNone: () => isFlagsRequired
|
|
682
|
+
? Effect.die(new Error('[foldkit] This application declares Flags. Pass its ' +
|
|
683
|
+
'Flags Effect to Runtime.run or Runtime.embed.'))
|
|
684
|
+
: /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
685
|
+
Effect.succeed(undefined),
|
|
477
686
|
onSome: provideResources,
|
|
478
687
|
});
|
|
688
|
+
// Every hydration refusal that knows which root it was going to adopt
|
|
689
|
+
// contains that root first. The build id is one reason to refuse; a
|
|
690
|
+
// missing, duplicated, malformed, or Schema-incompatible Flags payload
|
|
691
|
+
// is another, and the page left behind is just as live in each case.
|
|
692
|
+
const refuseHydration = (root, message, cause) => {
|
|
693
|
+
containRefusedPage(root.ownerDocument);
|
|
694
|
+
return Effect.die(cause === undefined
|
|
695
|
+
? new Error(message)
|
|
696
|
+
: new Error(message, { cause }));
|
|
697
|
+
};
|
|
698
|
+
const decodeFlagsPayload = (payload, runtimeId, root) => Effect.try({
|
|
699
|
+
try: () => {
|
|
700
|
+
const parsedPayload = JSON.parse(payload);
|
|
701
|
+
return pipe(
|
|
702
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
703
|
+
Schema.toCodecJson(FlagsCodec), Schema.decodeUnknownSync, decode => decode(parsedPayload));
|
|
704
|
+
},
|
|
705
|
+
catch: cause => cause,
|
|
706
|
+
}).pipe(Effect.catch(cause => refuseHydration(root, '[foldkit] Runtime.hydrate could not decode the server ' +
|
|
707
|
+
`Flags payload for application "${runtimeId}". The HTML ` +
|
|
708
|
+
'and client bundle must use the same Flags Schema.', cause)));
|
|
709
|
+
const maybeRequestedHydration = bootMode === 'Hydrate'
|
|
710
|
+
? Option.fromNullishOr(hydration)
|
|
711
|
+
: Option.none();
|
|
712
|
+
if (bootMode === 'Hydrate' && Option.isNone(maybeRequestedHydration)) {
|
|
713
|
+
// A hydrating client that finds no stamped root will not adopt
|
|
714
|
+
// whatever the page holds, so the page is contained whether or not the
|
|
715
|
+
// caller named a container.
|
|
716
|
+
containRefusedPage(container === null ? document : container.ownerDocument);
|
|
717
|
+
return yield* Effect.die(new Error('[foldkit] Runtime.hydrate could not find a server-rendered ' +
|
|
718
|
+
`root stamped with \`${FOLDKIT_APP_ATTRIBUTE}\`. Use ` +
|
|
719
|
+
'Runtime.run for a fresh client boot.'));
|
|
720
|
+
}
|
|
721
|
+
// The build the served page came from is settled here, before the Flags
|
|
722
|
+
// payload text is accessed, parsed, or decoded, before `init` runs, and
|
|
723
|
+
// therefore before any Command, Subscription, ManagedResource, or port
|
|
724
|
+
// this boot would start. A page from another deployment carries that
|
|
725
|
+
// deployment's Flags, which the current Schema may well accept while
|
|
726
|
+
// every value in them means something else, so deferring the comparison
|
|
727
|
+
// to the DOM patch lets stale data reach new code that already acted on
|
|
728
|
+
// it.
|
|
729
|
+
if (Option.isSome(maybeRequestedHydration)) {
|
|
730
|
+
const skew = buildSkew(maybeRequestedHydration.value.root, buildId, maybeRequestedHydration.value.runtimeId);
|
|
731
|
+
if (skew !== undefined) {
|
|
732
|
+
containRefusedPage(maybeRequestedHydration.value.root.ownerDocument);
|
|
733
|
+
return yield* Effect.die(skew);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
// NOTE: an HMR-restored Model wins over DOM adoption because the
|
|
737
|
+
// server DOM reflects older code. The hydration handoff is still
|
|
738
|
+
// required, but the restored Model gets a fresh patch against its
|
|
739
|
+
// stamped root.
|
|
740
|
+
const maybeHydrationRoot = Predicate.isUndefined(hmrModel)
|
|
741
|
+
? Option.map(maybeRequestedHydration, requestedHydration => requestedHydration.root)
|
|
742
|
+
: Option.none();
|
|
743
|
+
const maybeHydrationFlags = yield* Option.match(maybeRequestedHydration, {
|
|
744
|
+
onNone: () => Effect.succeed(Option.none()),
|
|
745
|
+
onSome: requestedHydration => Effect.map(requestedHydration.isFlagsRequired
|
|
746
|
+
? Array.match(requestedHydration.flagsScripts, {
|
|
747
|
+
onEmpty: () => refuseHydration(requestedHydration.root, '[foldkit] Runtime.hydrate found application ' +
|
|
748
|
+
`"${requestedHydration.runtimeId}" but its ` +
|
|
749
|
+
'server Flags payload is missing.'),
|
|
750
|
+
onNonEmpty: ([payloadScript, ...remainingScripts]) => Array.isArrayNonEmpty(remainingScripts)
|
|
751
|
+
? refuseHydration(requestedHydration.root, '[foldkit] Runtime.hydrate found multiple ' +
|
|
752
|
+
'server Flags payloads for application ' +
|
|
753
|
+
`"${requestedHydration.runtimeId}".`)
|
|
754
|
+
: decodeFlagsPayload(payloadScript.textContent ?? '', requestedHydration.runtimeId, requestedHydration.root),
|
|
755
|
+
})
|
|
756
|
+
: /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
757
|
+
Effect.succeed(undefined), Option.some),
|
|
758
|
+
});
|
|
759
|
+
const resolveFlags = Option.match(maybeHydrationFlags, {
|
|
760
|
+
onNone: () => resolveFreshFlags,
|
|
761
|
+
onSome: Effect.succeed,
|
|
762
|
+
});
|
|
479
763
|
const ModelJsonCodec = Schema.toCodecJson(
|
|
480
764
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
481
765
|
Model);
|
|
482
766
|
const decodeHmrModel = Schema.decodeUnknownExit(ModelJsonCodec);
|
|
483
767
|
const encodeHmrModel = Schema.encodeUnknownSync(ModelJsonCodec);
|
|
484
768
|
const currentUrl = Option.fromNullishOr(routingConfig).pipe(Option.flatMap(() => urlFromString(window.location.href)));
|
|
485
|
-
// NOTE: a restored Model skips `init`, so resolving
|
|
769
|
+
// NOTE: a restored Model skips `init`, so resolving Flags on that
|
|
486
770
|
// path would build the `resources` Layer only to discard what it
|
|
487
771
|
// produced. Gating the resolution on the restore decision is what
|
|
488
772
|
// stops a reload from reconnecting whatever the Layer holds. It has
|
|
489
773
|
// to stay ahead of the preserve-scheduler and HMR finalizers: a
|
|
490
|
-
//
|
|
774
|
+
// Flags Effect that fails after those are registered tears down more
|
|
491
775
|
// than it used to, and their release defects would bury its cause.
|
|
492
776
|
const runInit = Effect.map(resolveFlags, flags => init(flags, Option.getOrUndefined(currentUrl)));
|
|
493
777
|
const [initModelRaw, initCommands] = yield* hmrModel !== undefined
|
|
@@ -637,6 +921,10 @@ const makeRuntime = ({ ports, Model, flags: maybeResolveFlags, init, update, vie
|
|
|
637
921
|
}
|
|
638
922
|
};
|
|
639
923
|
const vnodeSlot = { maybeCurrentVNode: Option.none() };
|
|
924
|
+
// NOTE: consumed by the first render only. Set when this boot found
|
|
925
|
+
// an adoptable server-rendered root; the first patch then goes
|
|
926
|
+
// through `__hydrateVNode` instead of replacing the container.
|
|
927
|
+
let pendingHydrationRoot = Option.getOrNull(maybeHydrationRoot);
|
|
640
928
|
// NOTE: registered before any perpetual fiber is forked so it runs
|
|
641
929
|
// after they are interrupted (scope finalizers are LIFO). Patching to
|
|
642
930
|
// an empty tree fires snabbdom destroy hooks, which is what releases
|
|
@@ -941,7 +1229,28 @@ const makeRuntime = ({ ports, Model, flags: maybeResolveFlags, init, update, vie
|
|
|
941
1229
|
thresholdMs,
|
|
942
1230
|
}));
|
|
943
1231
|
const maybeCurrentVNode = vnodeSlot.maybeCurrentVNode;
|
|
944
|
-
const [patchedVNode, maybePatchDuration] = yield* Effect.sync(() => measureSlowPhase(maybeLiveSlowPatch, () =>
|
|
1232
|
+
const [patchedVNode, maybePatchDuration] = yield* Effect.sync(() => measureSlowPhase(maybeLiveSlowPatch, () => {
|
|
1233
|
+
if (Option.isNone(maybeCurrentVNode) &&
|
|
1234
|
+
pendingHydrationRoot !== null) {
|
|
1235
|
+
const hydrationRoot = pendingHydrationRoot;
|
|
1236
|
+
pendingHydrationRoot = null;
|
|
1237
|
+
// NOTE: strip the stamp before the patch, not after, so the
|
|
1238
|
+
// patch is the sole owner of the root's attributes. It has
|
|
1239
|
+
// already served its purpose of locating the root, and
|
|
1240
|
+
// removing it after would delete a `data-foldkit-app` the view
|
|
1241
|
+
// itself declares, which a later equal-vnode patch would not
|
|
1242
|
+
// restore. Removing it here also stops a later boot on the same
|
|
1243
|
+
// container (a dispose-then-embed remount) from re-detecting
|
|
1244
|
+
// this now-consumed root as hydratable.
|
|
1245
|
+
hydrationRoot.removeAttribute(FOLDKIT_APP_ATTRIBUTE);
|
|
1246
|
+
// An empty id reaches the adoption step's own check as a
|
|
1247
|
+
// value that matches nothing. Boot already refused a
|
|
1248
|
+
// hydration without an id, so this stands in only for a
|
|
1249
|
+
// caller that reached here another way.
|
|
1250
|
+
return __hydrateVNode(hydrationRoot, nextVNode, boundaryRegistry.dedupeSeen, buildId ?? '');
|
|
1251
|
+
}
|
|
1252
|
+
return __patchVNode(maybeCurrentVNode, nextVNode, container, boundaryRegistry.dedupeSeen);
|
|
1253
|
+
}));
|
|
945
1254
|
vnodeSlot.maybeCurrentVNode = Option.some(patchedVNode);
|
|
946
1255
|
reportSlowPhase(maybeLiveSlowPatch, maybePatchDuration, (durationMs, thresholdMs) => ({
|
|
947
1256
|
_tag: 'Patch',
|
|
@@ -1372,10 +1681,15 @@ const makeRuntime = ({ ports, Model, flags: maybeResolveFlags, init, update, vie
|
|
|
1372
1681
|
yield* Effect.never;
|
|
1373
1682
|
})).pipe(Effect.provideService(RenderCommit, commitNotifier.service));
|
|
1374
1683
|
};
|
|
1375
|
-
const start = (hmrModel) => startWith(Option.none(), hmrModel);
|
|
1376
|
-
const program = {
|
|
1684
|
+
const start = (hmrModel) => startWith(Option.none(), hmrModel, 'Fresh');
|
|
1685
|
+
const program = {
|
|
1686
|
+
runtimeId,
|
|
1687
|
+
start,
|
|
1688
|
+
ports,
|
|
1689
|
+
};
|
|
1377
1690
|
runtimeInternals.set(program, {
|
|
1378
|
-
startWith,
|
|
1691
|
+
startWith: (maybeConnector, hmrModel, bootMode, flags, buildId) => startWith(maybeConnector, hmrModel, bootMode, flags, buildId),
|
|
1692
|
+
kind,
|
|
1379
1693
|
isEmbedActive: false,
|
|
1380
1694
|
maybeActiveFiber: Option.none(),
|
|
1381
1695
|
});
|
|
@@ -1390,11 +1704,6 @@ const currentLocationUrl = () => {
|
|
|
1390
1704
|
const { origin, pathname, search } = window.location;
|
|
1391
1705
|
return `${origin}${pathname}${search}`;
|
|
1392
1706
|
};
|
|
1393
|
-
const textDirectionForHtmlElement = {
|
|
1394
|
-
Ltr: 'ltr',
|
|
1395
|
-
Rtl: 'rtl',
|
|
1396
|
-
Auto: 'auto',
|
|
1397
|
-
};
|
|
1398
1707
|
const documentMetadataElements = new WeakMap();
|
|
1399
1708
|
const metadataElementsForDocument = () => {
|
|
1400
1709
|
let elements = documentMetadataElements.get(document);
|
|
@@ -1431,7 +1740,7 @@ const applyDocumentMetadata = (nextDocument, mountedRoot) => {
|
|
|
1431
1740
|
documentElement.lang = nextDocument.lang;
|
|
1432
1741
|
}
|
|
1433
1742
|
if (nextDocument.dir !== undefined) {
|
|
1434
|
-
const dir =
|
|
1743
|
+
const dir = textDirectionToAttribute(nextDocument.dir);
|
|
1435
1744
|
if (documentElement.dir !== dir) {
|
|
1436
1745
|
documentElement.dir = dir;
|
|
1437
1746
|
}
|
|
@@ -1503,24 +1812,37 @@ const renderCrashView = (context, crash, container, vnodeSlot, manageDocument) =
|
|
|
1503
1812
|
};
|
|
1504
1813
|
export function makeApplication(config) {
|
|
1505
1814
|
const { container } = config;
|
|
1506
|
-
|
|
1815
|
+
const hasRouting = 'routing' in config;
|
|
1816
|
+
const hasFlags = 'Flags' in config;
|
|
1817
|
+
const hydration = findDocumentHydration(container, hasFlags);
|
|
1818
|
+
const resolvedContainer = hydration?.root ?? container;
|
|
1819
|
+
if (resolvedContainer === null) {
|
|
1820
|
+
// A server-rendered page whose root lost its stamp reaches exactly here:
|
|
1821
|
+
// template injection put the render where the placeholder was, so
|
|
1822
|
+
// `getElementById` finds nothing and the stamp that would have named the
|
|
1823
|
+
// root is gone. There is no handoff to refuse further along, and the markup
|
|
1824
|
+
// is as live as any other refused page, so it is contained here.
|
|
1825
|
+
if (hasServerRenderedMarkup(document)) {
|
|
1826
|
+
containRefusedPage(document);
|
|
1827
|
+
}
|
|
1507
1828
|
throw new Error('[foldkit] Container is null. Make sure the element exists in the DOM ' +
|
|
1508
1829
|
'before calling makeApplication (e.g. that your <div id="root"></div> has ' +
|
|
1509
|
-
'rendered, and your script runs after it).'
|
|
1830
|
+
'rendered, and your script runs after it). On a server-rendered page ' +
|
|
1831
|
+
'the runtime instead finds the root by its `data-foldkit-app` stamp.');
|
|
1510
1832
|
}
|
|
1511
|
-
const hasRouting = 'routing' in config;
|
|
1512
|
-
const hasFlags = 'Flags' in config;
|
|
1513
1833
|
const currentUrl = hasRouting
|
|
1514
1834
|
? Option.getOrThrow(urlFromString(window.location.href))
|
|
1515
1835
|
: undefined;
|
|
1516
1836
|
const baseConfig = {
|
|
1837
|
+
kind: 'Application',
|
|
1517
1838
|
Model: config.Model,
|
|
1518
1839
|
update: config.update,
|
|
1519
1840
|
view: config.view,
|
|
1520
1841
|
manageDocument: true,
|
|
1521
1842
|
ports: config.ports,
|
|
1522
1843
|
...(config.subscriptions && { subscriptions: config.subscriptions }),
|
|
1523
|
-
container,
|
|
1844
|
+
container: resolvedContainer,
|
|
1845
|
+
...(hydration && { hydration }),
|
|
1524
1846
|
...(hasRouting && { routing: config.routing }),
|
|
1525
1847
|
...(config.crash && { crash: config.crash }),
|
|
1526
1848
|
...(Predicate.isNotUndefined(config.slow) && {
|
|
@@ -1548,7 +1870,8 @@ export function makeApplication(config) {
|
|
|
1548
1870
|
return makeRuntime({
|
|
1549
1871
|
...baseConfig,
|
|
1550
1872
|
Flags: config.Flags,
|
|
1551
|
-
|
|
1873
|
+
configuredFlags: Option.none(),
|
|
1874
|
+
isFlagsRequired: true,
|
|
1552
1875
|
init: (flags, url) => config.init(flags, url ?? currentUrl),
|
|
1553
1876
|
});
|
|
1554
1877
|
}
|
|
@@ -1556,7 +1879,8 @@ export function makeApplication(config) {
|
|
|
1556
1879
|
return makeRuntime({
|
|
1557
1880
|
...baseConfig,
|
|
1558
1881
|
Flags: Schema.Void,
|
|
1559
|
-
|
|
1882
|
+
configuredFlags: Option.none(),
|
|
1883
|
+
isFlagsRequired: false,
|
|
1560
1884
|
init: (_flags, url) => config.init(url ?? currentUrl),
|
|
1561
1885
|
});
|
|
1562
1886
|
}
|
|
@@ -1564,7 +1888,8 @@ export function makeApplication(config) {
|
|
|
1564
1888
|
return makeRuntime({
|
|
1565
1889
|
...baseConfig,
|
|
1566
1890
|
Flags: config.Flags,
|
|
1567
|
-
|
|
1891
|
+
configuredFlags: Option.none(),
|
|
1892
|
+
isFlagsRequired: true,
|
|
1568
1893
|
init: (flags) => config.init(flags),
|
|
1569
1894
|
});
|
|
1570
1895
|
}
|
|
@@ -1572,7 +1897,8 @@ export function makeApplication(config) {
|
|
|
1572
1897
|
return makeRuntime({
|
|
1573
1898
|
...baseConfig,
|
|
1574
1899
|
Flags: Schema.Void,
|
|
1575
|
-
|
|
1900
|
+
configuredFlags: Option.none(),
|
|
1901
|
+
isFlagsRequired: false,
|
|
1576
1902
|
init: () => config.init(),
|
|
1577
1903
|
});
|
|
1578
1904
|
}
|
|
@@ -1610,6 +1936,7 @@ export function makeElement(config) {
|
|
|
1610
1936
|
});
|
|
1611
1937
|
const crash = toCrashConfig(config.crash);
|
|
1612
1938
|
const baseConfig = {
|
|
1939
|
+
kind: 'Element',
|
|
1613
1940
|
Model: config.Model,
|
|
1614
1941
|
update: config.update,
|
|
1615
1942
|
view,
|
|
@@ -1640,7 +1967,8 @@ export function makeElement(config) {
|
|
|
1640
1967
|
return makeRuntime({
|
|
1641
1968
|
...baseConfig,
|
|
1642
1969
|
Flags: config.Flags,
|
|
1643
|
-
|
|
1970
|
+
configuredFlags: Option.some(config.flags),
|
|
1971
|
+
isFlagsRequired: true,
|
|
1644
1972
|
init: (flags) => config.init(flags),
|
|
1645
1973
|
});
|
|
1646
1974
|
}
|
|
@@ -1648,7 +1976,8 @@ export function makeElement(config) {
|
|
|
1648
1976
|
return makeRuntime({
|
|
1649
1977
|
...baseConfig,
|
|
1650
1978
|
Flags: Schema.Void,
|
|
1651
|
-
|
|
1979
|
+
configuredFlags: Option.none(),
|
|
1980
|
+
isFlagsRequired: false,
|
|
1652
1981
|
init: () => config.init(),
|
|
1653
1982
|
});
|
|
1654
1983
|
}
|
|
@@ -1716,6 +2045,20 @@ const resolveHmrModel = (runtimeId) => {
|
|
|
1716
2045
|
return Effect.succeed(undefined);
|
|
1717
2046
|
}));
|
|
1718
2047
|
};
|
|
2048
|
+
/** Starts a program Effect with explicit boot inputs for runtime tests.
|
|
2049
|
+
* @internal */
|
|
2050
|
+
export const __startProgram = (program, hmrModel, bootMode, flags, buildId) => {
|
|
2051
|
+
const internals = runtimeInternals.get(program);
|
|
2052
|
+
if (Predicate.isUndefined(internals)) {
|
|
2053
|
+
return Effect.die(new Error('[foldkit] Runtime boot expects a program created by ' +
|
|
2054
|
+
'makeApplication or makeElement.'));
|
|
2055
|
+
}
|
|
2056
|
+
if (bootMode === 'Hydrate' && internals.kind !== 'Application') {
|
|
2057
|
+
return Effect.die(new Error('[foldkit] Runtime.hydrate expects a program created by ' +
|
|
2058
|
+
'makeApplication.'));
|
|
2059
|
+
}
|
|
2060
|
+
return internals.startWith(Option.none(), hmrModel, bootMode, flags, buildId);
|
|
2061
|
+
};
|
|
1719
2062
|
// NOTE: deliberately not `BrowserRuntime.runMain`, which interrupts the
|
|
1720
2063
|
// runtime on `beforeunload`. `beforeunload` is a question, not a commitment:
|
|
1721
2064
|
// the browser also fires it for a click on a download link, for a navigation
|
|
@@ -1727,11 +2070,34 @@ const resolveHmrModel = (runtimeId) => {
|
|
|
1727
2070
|
// interrupt at all and lets the document take the runtime with it. Error
|
|
1728
2071
|
// reporting and the keep-alive interval come from `makeRunMain` either way.
|
|
1729
2072
|
const runMainWithoutUnloadInterrupt = Runtime.makeRunMain(Function.constVoid);
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
export
|
|
1734
|
-
|
|
2073
|
+
const startProgram = (program, bootMode, flags, buildId) => {
|
|
2074
|
+
runMainWithoutUnloadInterrupt(provideBrowserScheduler(Effect.flatMap(resolveHmrModel(program.runtimeId), hmrModel => __startProgram(program, hmrModel, bootMode, flags, buildId))));
|
|
2075
|
+
};
|
|
2076
|
+
export function run(program, options) {
|
|
2077
|
+
startProgram(program, 'Fresh', options?.flags);
|
|
2078
|
+
}
|
|
2079
|
+
/** Starts a Foldkit runtime by adopting a server-rendered DOM in place instead
|
|
2080
|
+
* of building it fresh. Use this as the client entry for a page served by
|
|
2081
|
+
* `renderToString`: the first render attaches to the stamped root, keeps the
|
|
2082
|
+
* existing nodes, and reconstructs the Model from the Flags the server
|
|
2083
|
+
* embedded. The handoff is strict: a missing server root, a root stamped more
|
|
2084
|
+
* than once, more than one root with no container to choose between them, a
|
|
2085
|
+
* missing Flags payload, an undecodable payload, or a page from another
|
|
2086
|
+
* deployment terminates startup. Every one of those contains the page first:
|
|
2087
|
+
* the document's body is marked `inert` and a nondismissable modal shield is
|
|
2088
|
+
* opened above existing top-layer content, so native links, forms, controls,
|
|
2089
|
+
* and focus targets stop responding. Containment leaves author-owned dialogs
|
|
2090
|
+
* open without calling `close` or dispatching `cancel`. Nothing is moved, so
|
|
2091
|
+
* no custom element reconnects and no frame reloads. This is not a script or
|
|
2092
|
+
* event sandbox: existing capture-phase handlers, browser-generated top-layer
|
|
2093
|
+
* events, timers, and other stale scripts can still run and can open newer
|
|
2094
|
+
* top-layer UI. Use `run` in a separate client-only entry when the page should
|
|
2095
|
+
* boot without server output.
|
|
2096
|
+
*
|
|
2097
|
+
* @experimental Server rendering and hydration are experimental while their
|
|
2098
|
+
* contracts settle. */
|
|
2099
|
+
export const hydrate = (program, options) => {
|
|
2100
|
+
startProgram(program, 'Hydrate', undefined, options.buildId);
|
|
1735
2101
|
};
|
|
1736
2102
|
const buildPortHandles = (ports, connector) => {
|
|
1737
2103
|
const handles = {};
|
|
@@ -1750,32 +2116,7 @@ const buildPortHandles = (ports, connector) => {
|
|
|
1750
2116
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
1751
2117
|
return handles;
|
|
1752
2118
|
};
|
|
1753
|
-
|
|
1754
|
-
* Starts a Foldkit runtime under a host-controlled lifecycle and returns an
|
|
1755
|
-
* `EmbedHandle`. This is the entry point for embedding a Foldkit app inside
|
|
1756
|
-
* another application: the host pushes values in through the handle's inbound
|
|
1757
|
-
* Ports, listens to outbound Ports, and calls `dispose` when it unmounts the
|
|
1758
|
-
* app. The host never touches the Model or dispatches Messages directly; the
|
|
1759
|
-
* Schema-typed Ports are the whole boundary.
|
|
1760
|
-
*
|
|
1761
|
-
* Works with programs from both `makeApplication` and `makeElement`; for a
|
|
1762
|
-
* widget on a page the host owns, `makeElement` is the natural fit.
|
|
1763
|
-
*
|
|
1764
|
-
* A program can be embedded once at a time (it owns one container). After
|
|
1765
|
-
* `dispose`, the same container can be embedded again with a fresh program.
|
|
1766
|
-
*
|
|
1767
|
-
* ```ts
|
|
1768
|
-
* const handle = Runtime.embed(element)
|
|
1769
|
-
*
|
|
1770
|
-
* handle.ports.stepChanged.send(5)
|
|
1771
|
-
* const unsubscribe = handle.ports.countChanged.subscribe(count => {
|
|
1772
|
-
* console.log(count)
|
|
1773
|
-
* })
|
|
1774
|
-
*
|
|
1775
|
-
* handle.dispose()
|
|
1776
|
-
* ```
|
|
1777
|
-
*/
|
|
1778
|
-
export const embed = (program) => {
|
|
2119
|
+
export function embed(program, options) {
|
|
1779
2120
|
const internals = runtimeInternals.get(program);
|
|
1780
2121
|
if (Predicate.isUndefined(internals)) {
|
|
1781
2122
|
throw new Error('[foldkit] embed expects a program created by makeApplication or makeElement.');
|
|
@@ -1795,7 +2136,7 @@ export const embed = (program) => {
|
|
|
1795
2136
|
const startEffect = pipe(Option.match(internals.maybeActiveFiber, {
|
|
1796
2137
|
onNone: () => Effect.void,
|
|
1797
2138
|
onSome: previousFiber => Effect.asVoid(Fiber.await(previousFiber)),
|
|
1798
|
-
}), Effect.andThen(resolveHmrModel(program.runtimeId)), Effect.flatMap(hmrModel => internals.startWith(Option.some(connector), hmrModel)));
|
|
2139
|
+
}), Effect.andThen(resolveHmrModel(program.runtimeId)), Effect.flatMap(hmrModel => internals.startWith(Option.some(connector), hmrModel, 'Fresh', options?.flags)));
|
|
1799
2140
|
const fiber = Effect.runFork(provideBrowserScheduler(startEffect));
|
|
1800
2141
|
internals.maybeActiveFiber = Option.some(fiber);
|
|
1801
2142
|
let isHandleDisposed = false;
|
|
@@ -1810,4 +2151,4 @@ export const embed = (program) => {
|
|
|
1810
2151
|
};
|
|
1811
2152
|
const ports = buildPortHandles(program.ports, connector);
|
|
1812
2153
|
return { ports, dispose };
|
|
1813
|
-
}
|
|
2154
|
+
}
|