foldkit 0.106.0 → 0.108.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 -4
- package/dist/devTools/overlay.d.ts +1 -1
- package/dist/devTools/overlay.d.ts.map +1 -1
- package/dist/devTools/overlay.js +11 -12
- package/dist/devTools/webSocketBridge.d.ts +2 -2
- package/dist/devTools/webSocketBridge.d.ts.map +1 -1
- package/dist/devTools/webSocketBridge.js +9 -0
- package/dist/html/index.d.ts +5 -1
- package/dist/html/index.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/port/index.d.ts +2 -0
- package/dist/port/index.d.ts.map +1 -0
- package/dist/port/index.js +1 -0
- package/dist/port/port.d.ts +143 -0
- package/dist/port/port.d.ts.map +1 -0
- package/dist/port/port.js +156 -0
- package/dist/port/public.d.ts +3 -0
- package/dist/port/public.d.ts.map +1 -0
- package/dist/port/public.js +1 -0
- package/dist/runtime/browserListeners.d.ts +3 -3
- package/dist/runtime/browserListeners.d.ts.map +1 -1
- package/dist/runtime/browserListeners.js +23 -5
- package/dist/runtime/crashUI.d.ts.map +1 -1
- package/dist/runtime/crashUI.js +1 -3
- 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 +172 -28
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +462 -62
- package/dist/runtime/subscription.d.ts +6 -1
- package/dist/runtime/subscription.d.ts.map +1 -1
- package/dist/ui/dragAndDrop/index.d.ts +212 -246
- package/dist/ui/dragAndDrop/index.d.ts.map +1 -1
- package/dist/ui/slider/index.d.ts +124 -179
- package/dist/ui/slider/index.d.ts.map +1 -1
- package/dist/ui/virtualList/index.d.ts +28 -38
- package/dist/ui/virtualList/index.d.ts.map +1 -1
- package/package.json +5 -1
package/dist/runtime/runtime.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { BrowserRuntime } from '@effect/platform-browser';
|
|
2
|
-
import { Array, Cause, Context, Duration, Effect, Exit, Function, Layer, Match, Option, Predicate, PubSub, Queue, Record, Ref, Scheduler, Schema, Stream, SubscriptionRef, pipe, } from 'effect';
|
|
2
|
+
import { Array, Cause, Context, Duration, Effect, Exit, Fiber, Function, Layer, Match, Option, Predicate, PubSub, Queue, Record, Ref, Scheduler, Schema, Stream, SubscriptionRef, pipe, } from 'effect';
|
|
3
3
|
import { h } from 'snabbdom';
|
|
4
4
|
import { createOverlay } from '../devTools/overlay.js';
|
|
5
5
|
import { createDevToolsStore, } from '../devTools/store.js';
|
|
6
6
|
import { startWebSocketBridge } from '../devTools/webSocketBridge.js';
|
|
7
7
|
import { __beginRender as beginHtmlRender, __clearRuntime as clearHtmlRuntime, __createBoundaryRegistry as createHtmlBoundaryRegistry, __setRuntime as setHtmlRuntime, } from '../html/index.js';
|
|
8
8
|
import { MountTracker } from '../mount/index.js';
|
|
9
|
+
import { __CurrentPortChannels, __makeInboundChannel, } from '../port/index.js';
|
|
9
10
|
import { fromString as urlFromString } from '../url/index.js';
|
|
10
11
|
import { dedupeSharedVNodes, patch, toVNode } from '../vdom.js';
|
|
11
12
|
import { addBfcacheRestoreListener, addNavigationEventListeners, } from './browserListeners.js';
|
|
@@ -48,7 +49,130 @@ const defaultSlowViewCallback = (context) => {
|
|
|
48
49
|
/** Effect service tag that provides message dispatching to the view layer. */
|
|
49
50
|
export class Dispatch extends Context.Service()('@foldkit/Dispatch') {
|
|
50
51
|
}
|
|
51
|
-
const
|
|
52
|
+
const makeHostConnector = () => {
|
|
53
|
+
let isDisposed = false;
|
|
54
|
+
let maybeDeliverInbound = Option.none();
|
|
55
|
+
const pendingInboundSends = [];
|
|
56
|
+
const listenersByPort = new Map();
|
|
57
|
+
const sendInbound = (portName, port, value) => {
|
|
58
|
+
if (isDisposed) {
|
|
59
|
+
return Exit.void;
|
|
60
|
+
}
|
|
61
|
+
const decodeExit = Schema.decodeUnknownExit(port.schema)(value);
|
|
62
|
+
Exit.match(decodeExit, {
|
|
63
|
+
onFailure: cause => {
|
|
64
|
+
console.error(`[foldkit] Inbound port "${portName}" rejected a value:`, Cause.squash(cause));
|
|
65
|
+
},
|
|
66
|
+
onSuccess: decodedValue => {
|
|
67
|
+
Option.match(maybeDeliverInbound, {
|
|
68
|
+
onNone: () => {
|
|
69
|
+
pendingInboundSends.push({ port, value: decodedValue });
|
|
70
|
+
},
|
|
71
|
+
onSome: deliverInbound => deliverInbound(port, decodedValue),
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
return Exit.asVoid(decodeExit);
|
|
76
|
+
};
|
|
77
|
+
const addListener = (port, listener) => {
|
|
78
|
+
if (isDisposed) {
|
|
79
|
+
return Function.constVoid;
|
|
80
|
+
}
|
|
81
|
+
const listeners = listenersByPort.get(port) ?? new Set();
|
|
82
|
+
listenersByPort.set(port, listeners);
|
|
83
|
+
listeners.add(listener);
|
|
84
|
+
return () => {
|
|
85
|
+
listeners.delete(listener);
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
// NOTE: delivery is deferred to a microtask so a host listener never runs
|
|
89
|
+
// inside the runtime's Command fiber (a listener that synchronously calls
|
|
90
|
+
// send or dispose must not re-enter the runtime), and so a host that
|
|
91
|
+
// subscribes synchronously right after embed() returns still receives
|
|
92
|
+
// emissions from init Commands.
|
|
93
|
+
const deliverOutbound = (port, encodedValue) => {
|
|
94
|
+
if (isDisposed) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
queueMicrotask(() => {
|
|
98
|
+
if (isDisposed) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const listeners = listenersByPort.get(port) ?? new Set();
|
|
102
|
+
listeners.forEach(listener => {
|
|
103
|
+
try {
|
|
104
|
+
listener(encodedValue);
|
|
105
|
+
}
|
|
106
|
+
catch (listenerError) {
|
|
107
|
+
console.error('[foldkit] An outbound port listener threw:', listenerError);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
const bind = (deliverInbound) => {
|
|
113
|
+
maybeDeliverInbound = Option.some(deliverInbound);
|
|
114
|
+
const flushedSends = pendingInboundSends.splice(0);
|
|
115
|
+
flushedSends.forEach(({ port, value }) => deliverInbound(port, value));
|
|
116
|
+
};
|
|
117
|
+
const unbind = () => {
|
|
118
|
+
maybeDeliverInbound = Option.none();
|
|
119
|
+
};
|
|
120
|
+
const dispose = () => {
|
|
121
|
+
isDisposed = true;
|
|
122
|
+
pendingInboundSends.length = 0;
|
|
123
|
+
listenersByPort.forEach(listeners => listeners.clear());
|
|
124
|
+
listenersByPort.clear();
|
|
125
|
+
};
|
|
126
|
+
return { sendInbound, addListener, deliverOutbound, bind, unbind, dispose };
|
|
127
|
+
};
|
|
128
|
+
const makePortChannels = (ports, maybeConnector) => {
|
|
129
|
+
const inboundChannelsByPort = new Map();
|
|
130
|
+
Object.values(ports.inbound ?? {}).forEach(port => {
|
|
131
|
+
inboundChannelsByPort.set(port, __makeInboundChannel());
|
|
132
|
+
});
|
|
133
|
+
const outboundPorts = new Set(Object.values(ports.outbound ?? {}));
|
|
134
|
+
const channels = {
|
|
135
|
+
isConfigured: true,
|
|
136
|
+
lookupInbound: port => Option.fromNullishOr(inboundChannelsByPort.get(port)),
|
|
137
|
+
lookupOutbound: port => outboundPorts.has(port)
|
|
138
|
+
? Option.some(encodedValue => Option.match(maybeConnector, {
|
|
139
|
+
onNone: Function.constVoid,
|
|
140
|
+
onSome: connector => connector.deliverOutbound(port, encodedValue),
|
|
141
|
+
}))
|
|
142
|
+
: Option.none(),
|
|
143
|
+
};
|
|
144
|
+
const deliverInbound = (port, value) => {
|
|
145
|
+
Option.match(Option.fromNullishOr(inboundChannelsByPort.get(port)), {
|
|
146
|
+
onNone: Function.constVoid,
|
|
147
|
+
onSome: channel => channel.deliver(value),
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
return { channels, deliverInbound };
|
|
151
|
+
};
|
|
152
|
+
const validatePorts = (ports) => {
|
|
153
|
+
const inboundEntries = Object.entries(ports.inbound ?? {});
|
|
154
|
+
const outboundEntries = Object.entries(ports.outbound ?? {});
|
|
155
|
+
const inboundNames = new Set(inboundEntries.map(([name]) => name));
|
|
156
|
+
outboundEntries.forEach(([name]) => {
|
|
157
|
+
if (inboundNames.has(name)) {
|
|
158
|
+
throw new Error(`[foldkit] Port name "${name}" appears in both inbound and outbound. ` +
|
|
159
|
+
'Port names share one namespace on the EmbedHandle, so each name ' +
|
|
160
|
+
'must be unique across both records.');
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
const seenPorts = new Set();
|
|
164
|
+
const allEntries = [...inboundEntries, ...outboundEntries];
|
|
165
|
+
allEntries.forEach(([name, port]) => {
|
|
166
|
+
if (seenPorts.has(port)) {
|
|
167
|
+
throw new Error(`[foldkit] The Port registered as "${name}" is also registered under ` +
|
|
168
|
+
'another name. Each entry in the ports record needs its own ' +
|
|
169
|
+
'Port.inbound or Port.outbound value.');
|
|
170
|
+
}
|
|
171
|
+
seenPorts.add(port);
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
const runtimeInternals = new WeakMap();
|
|
175
|
+
const makeRuntime = ({ ports, Model, flags: resolveFlags, init, update, view, manageDocument, subscriptions, container, routing: routingConfig, crash, slowView, freezeModel, resources, managedResources, devTools, }) => {
|
|
52
176
|
const resolvedSlowView = pipe(slowView ?? {}, Option.liftPredicate(config => config !== false), Option.filter(config => Match.value(config.show ?? DEFAULT_SLOW_VIEW_SHOW).pipe(Match.when('Always', () => true), Match.when('Development', () => !!import.meta.hot), Match.exhaustive)), Option.map(config => ({
|
|
53
177
|
thresholdMs: config.thresholdMs ?? DEFAULT_SLOW_VIEW_THRESHOLD_MS,
|
|
54
178
|
onSlowView: config.onSlowView ?? defaultSlowViewCallback,
|
|
@@ -60,6 +184,9 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
60
184
|
}));
|
|
61
185
|
const devToolsMaxEntries = pipe(devTools ?? {}, Option.liftPredicate(config => config !== false), Option.flatMapNullishOr(config => config.maxEntries), Option.map(value => Math.max(DEV_TOOLS_MAX_ENTRIES_MIN, Math.min(DEV_TOOLS_MAX_ENTRIES_MAX, value))), Option.getOrUndefined);
|
|
62
186
|
const maybeFreezeModel = (model) => isFreezeModelActive ? deepFreeze(model) : model;
|
|
187
|
+
if (Predicate.isNotUndefined(ports)) {
|
|
188
|
+
validatePorts(ports);
|
|
189
|
+
}
|
|
63
190
|
const runtimeId = container?.id ?? '';
|
|
64
191
|
// NOTE: When the message queue drains a chain of dispatches (e.g. recursive
|
|
65
192
|
// Commands, websocket bursts), processing all of them inside one macrotask
|
|
@@ -77,11 +204,17 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
77
204
|
const handle = requestAnimationFrame(() => resume(Effect.void));
|
|
78
205
|
return Effect.sync(() => cancelAnimationFrame(handle));
|
|
79
206
|
});
|
|
80
|
-
const
|
|
207
|
+
const startWith = (maybeConnector, hmrModel) => Effect.scoped(Effect.gen(function* () {
|
|
81
208
|
if (runtimeId === '') {
|
|
82
209
|
return yield* Effect.die(new Error('[foldkit] Runtime container must have an `id` for HMR model preservation. ' +
|
|
83
|
-
'Set `container.id = "app"` (or any unique string) before passing it to
|
|
210
|
+
'Set `container.id = "app"` (or any unique string) before passing it to makeApplication or makeElement.'));
|
|
84
211
|
}
|
|
212
|
+
// NOTE: every perpetual fiber (render loop, Subscription streams,
|
|
213
|
+
// ManagedResource lifecycles) and every Command fiber forks into the
|
|
214
|
+
// runtime scope, so interrupting the runtime fiber (what dispose
|
|
215
|
+
// does) interrupts them all and runs their finalizers. A detached
|
|
216
|
+
// fork would outlive the runtime.
|
|
217
|
+
const runtimeScope = yield* Effect.scope;
|
|
85
218
|
// NOTE: one persistent MessageChannel for the runtime lifetime,
|
|
86
219
|
// shared by every burst-budget yield. The queue-drain fiber is the
|
|
87
220
|
// sole consumer, so a single `pendingYieldResume` slot is sufficient.
|
|
@@ -106,7 +239,29 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
106
239
|
}
|
|
107
240
|
});
|
|
108
241
|
});
|
|
109
|
-
|
|
242
|
+
// NOTE: `Effect.provide(effect, layer)` builds the Layer into a
|
|
243
|
+
// scope that closes when the provided effect ends, so providing the
|
|
244
|
+
// Layer per Command would construct and tear down every resource on
|
|
245
|
+
// each invocation. Building once into `runtimeScope` through a
|
|
246
|
+
// cached Effect is what makes `resources` long-lived: the first
|
|
247
|
+
// Command or Subscription that runs triggers construction, every
|
|
248
|
+
// later one shares the same built services, and release happens at
|
|
249
|
+
// runtime teardown. The build is uninterruptible because
|
|
250
|
+
// `Effect.cached` caches whatever Exit the first run produces:
|
|
251
|
+
// dispose racing an in-flight build would otherwise cache an
|
|
252
|
+
// interrupt, which every waiter would then surface as a crash.
|
|
253
|
+
const maybeAcquireResourceContext = yield* Option.match(Option.fromNullishOr(resources), {
|
|
254
|
+
onNone: () => Effect.succeed(Option.none()),
|
|
255
|
+
onSome: resourceLayer => Effect.map(Effect.cached(Effect.uninterruptible(Layer.buildWithScope(resourceLayer, runtimeScope))), Option.some),
|
|
256
|
+
});
|
|
257
|
+
const maybePortChannels = pipe(Option.fromNullishOr(ports), Option.map(portsConfig => makePortChannels(portsConfig, maybeConnector)));
|
|
258
|
+
yield* Option.match(Option.all({
|
|
259
|
+
connector: maybeConnector,
|
|
260
|
+
portChannels: maybePortChannels,
|
|
261
|
+
}), {
|
|
262
|
+
onNone: () => Effect.void,
|
|
263
|
+
onSome: ({ connector, portChannels }) => Effect.acquireRelease(Effect.sync(() => connector.bind(portChannels.deliverInbound)), () => Effect.sync(() => connector.unbind())),
|
|
264
|
+
});
|
|
110
265
|
// NOTE: One boundary registry per runtime instance, shared
|
|
111
266
|
// across renders so Submodel wrap descriptors registered by
|
|
112
267
|
// h.submodel persist between renders. The render function calls
|
|
@@ -130,17 +285,21 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
130
285
|
Layer.empty, mergeResourceIntoLayer)),
|
|
131
286
|
});
|
|
132
287
|
const provideAllResources = (effect) => {
|
|
133
|
-
const withResources = Option.match(
|
|
288
|
+
const withResources = Option.match(maybeAcquireResourceContext, {
|
|
134
289
|
onNone: () => effect,
|
|
135
|
-
onSome:
|
|
290
|
+
onSome: acquireResourceContext => Effect.flatMap(acquireResourceContext, resourceContext => Effect.provideContext(effect, resourceContext)),
|
|
136
291
|
});
|
|
137
|
-
|
|
292
|
+
const withManagedResources = Option.match(maybeManagedResourceLayer, {
|
|
138
293
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
139
294
|
onNone: () => withResources,
|
|
140
295
|
onSome: managedLayer =>
|
|
141
296
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
142
297
|
Effect.provide(withResources, managedLayer),
|
|
143
298
|
});
|
|
299
|
+
return Option.match(maybePortChannels, {
|
|
300
|
+
onNone: () => withManagedResources,
|
|
301
|
+
onSome: portChannels => Effect.provideService(withManagedResources, __CurrentPortChannels, portChannels.channels),
|
|
302
|
+
});
|
|
144
303
|
};
|
|
145
304
|
const flags = yield* resolveFlags;
|
|
146
305
|
const ModelJsonCodec = Schema.toCodecJson(
|
|
@@ -198,26 +357,62 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
198
357
|
: init(flags, Option.getOrUndefined(currentUrl));
|
|
199
358
|
const initModel = maybeFreezeModel(initModelRaw);
|
|
200
359
|
const modelPubSub = yield* PubSub.unbounded();
|
|
201
|
-
yield* Effect.forEach(
|
|
202
|
-
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
203
|
-
initCommands, command => Effect.forkDetach(command.effect.pipe(Effect.withSpan(command.name, {
|
|
204
|
-
attributes: command.args ?? {},
|
|
205
|
-
}), provideAllResources, Effect.flatMap(enqueueNormal))));
|
|
206
360
|
if (routingConfig) {
|
|
207
|
-
addNavigationEventListeners(enqueueHighUnsafe, routingConfig);
|
|
361
|
+
yield* Effect.acquireRelease(Effect.sync(() => addNavigationEventListeners(enqueueHighUnsafe, routingConfig)), removeNavigationEventListeners => Effect.sync(() => removeNavigationEventListeners()));
|
|
208
362
|
}
|
|
209
363
|
const modelRef = yield* Ref.make(initModel);
|
|
210
364
|
const maybeCurrentVNodeRef = yield* Ref.make(Option.none());
|
|
211
|
-
// NOTE:
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
365
|
+
// NOTE: registered before any perpetual fiber is forked so it runs
|
|
366
|
+
// after they are interrupted (scope finalizers are LIFO). Patching to
|
|
367
|
+
// an empty tree fires snabbdom destroy hooks, which is what releases
|
|
368
|
+
// Mounts; swapping the placeholder for the original container leaves
|
|
369
|
+
// the host DOM as it was before the first render, ready for a fresh
|
|
370
|
+
// embed of the same container. Gated on interruption: that is the
|
|
371
|
+
// dispose path. A runtime that stops because it crashed completes
|
|
372
|
+
// normally after rendering the crash view, and the crash view must
|
|
373
|
+
// stay visible.
|
|
374
|
+
yield* Effect.addFinalizer(exit => Effect.gen(function* () {
|
|
375
|
+
if (!Exit.hasInterrupts(exit)) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const maybeCurrentVNode = yield* Ref.get(maybeCurrentVNodeRef);
|
|
379
|
+
yield* Option.match(maybeCurrentVNode, {
|
|
380
|
+
onNone: () => Effect.void,
|
|
381
|
+
onSome: currentVNode => Effect.sync(() => {
|
|
382
|
+
const placeholderNode = patchVNode(Option.some(currentVNode), null, container).elm;
|
|
383
|
+
if (placeholderNode && placeholderNode.parentNode) {
|
|
384
|
+
placeholderNode.parentNode.replaceChild(container, placeholderNode);
|
|
385
|
+
container.replaceChildren();
|
|
386
|
+
}
|
|
387
|
+
}),
|
|
388
|
+
});
|
|
389
|
+
}));
|
|
390
|
+
const isCrashedRef = yield* Ref.make(false);
|
|
391
|
+
// NOTE: shared by every fiber's crash path: init render, render
|
|
392
|
+
// loop, message drain, and the Command and Subscription forks (a
|
|
393
|
+
// Command's Effect and a Subscription's Stream are typed with a
|
|
394
|
+
// `never` error channel, so a cause escaping one can only be a
|
|
395
|
+
// `resources` Layer build failure or an escaped defect, both
|
|
396
|
+
// unrecoverable). Each fiber catches its own cause so
|
|
397
|
+
// a failure surfaces as the crash view instead of dying silently
|
|
398
|
+
// and leaving the DOM frozen at the last successful render. The
|
|
399
|
+
// first crash wins: concurrent Command fibers can fail on the same
|
|
400
|
+
// broken Layer, and only one should report and render.
|
|
215
401
|
const crashWith = (cause, maybeMessage) => Effect.gen(function* () {
|
|
402
|
+
const wasCrashed = yield* Ref.getAndSet(isCrashedRef, true);
|
|
403
|
+
if (wasCrashed) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
216
406
|
const model = yield* Ref.get(modelRef);
|
|
217
407
|
const squashed = Cause.squash(cause);
|
|
218
408
|
const error = squashed instanceof Error ? squashed : new Error(String(squashed));
|
|
219
|
-
renderCrashView({ error, model, message: maybeMessage }, crash, container, maybeCurrentVNodeRef);
|
|
409
|
+
renderCrashView({ error, model, message: maybeMessage }, crash, container, maybeCurrentVNodeRef, manageDocument);
|
|
220
410
|
});
|
|
411
|
+
yield* Effect.forEach(
|
|
412
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
413
|
+
initCommands, command => Effect.forkIn(runtimeScope)(command.effect.pipe(Effect.withSpan(command.name, {
|
|
414
|
+
attributes: command.args ?? {},
|
|
415
|
+
}), provideAllResources, Effect.flatMap(enqueueNormal), Effect.catchCause(cause => crashWith(cause, Option.none())))));
|
|
221
416
|
// NOTE: queue-drain-fiber-local state. Kept as plain closure
|
|
222
417
|
// variables instead of `Ref`s because nothing else reads or writes
|
|
223
418
|
// them concurrently, and JS's single-threaded model already orders
|
|
@@ -276,9 +471,9 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
276
471
|
if (!Array.isReadonlyArrayEmpty(commands)) {
|
|
277
472
|
yield* Effect.forEach(
|
|
278
473
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
279
|
-
commands, command => Effect.
|
|
474
|
+
commands, command => Effect.forkIn(runtimeScope)(command.effect.pipe(Effect.withSpan(command.name, {
|
|
280
475
|
attributes: command.args ?? {},
|
|
281
|
-
}), provideAllResources, Effect.flatMap(enqueueNormal))));
|
|
476
|
+
}), provideAllResources, Effect.flatMap(enqueueNormal), Effect.catchCause(cause => crashWith(cause, Option.some(message))))));
|
|
282
477
|
}
|
|
283
478
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
284
479
|
const messageTag = message._tag;
|
|
@@ -336,7 +531,9 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
336
531
|
const maybeCurrentVNode = yield* Ref.get(maybeCurrentVNodeRef);
|
|
337
532
|
const patchedVNode = yield* Effect.sync(() => patchVNode(maybeCurrentVNode, nextVNode, container));
|
|
338
533
|
yield* Ref.set(maybeCurrentVNodeRef, Option.some(patchedVNode));
|
|
339
|
-
|
|
534
|
+
if (manageDocument) {
|
|
535
|
+
yield* Effect.sync(() => applyDocumentMetadata(nextDocument, patchedVNode.elm));
|
|
536
|
+
}
|
|
340
537
|
}).pipe(Effect.provideService(Dispatch, dispatchService), Effect.provideService(MountTracker, mountTracker));
|
|
341
538
|
const isInIframe = window.self !== window.top;
|
|
342
539
|
const resolvedDevTools = pipe(devTools ?? {}, Option.liftPredicate(config => config !== false), Option.filter(config => Match.value(config.show ?? DEFAULT_DEV_TOOLS_SHOW).pipe(Match.when('Always', () => true), Match.when('Development', () => !!import.meta.hot && !isInIframe), Match.exhaustive)), Option.map(config => ({
|
|
@@ -405,9 +602,22 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
405
602
|
maybeMessageSchema);
|
|
406
603
|
}
|
|
407
604
|
}
|
|
605
|
+
// NOTE: a fast-failing init Command (a `resources` Layer that
|
|
606
|
+
// throws synchronously) can render the crash view before this
|
|
607
|
+
// point. Rendering the init view would paint over it, so a crashed
|
|
608
|
+
// runtime suspends here instead, exactly like the failing-init-
|
|
609
|
+
// render path below.
|
|
610
|
+
const isCrashedBeforeInitRender = yield* Ref.get(isCrashedRef);
|
|
611
|
+
if (isCrashedBeforeInitRender) {
|
|
612
|
+
return yield* Effect.never;
|
|
613
|
+
}
|
|
408
614
|
const initRenderExit = yield* Effect.exit(render(initModel, Option.none()));
|
|
409
615
|
if (Exit.isFailure(initRenderExit)) {
|
|
410
|
-
|
|
616
|
+
yield* crashWith(initRenderExit.cause, Option.none());
|
|
617
|
+
// NOTE: suspend instead of returning. Completing would close the
|
|
618
|
+
// runtime scope and tear down the crash view; the scope must stay
|
|
619
|
+
// open until the runtime is interrupted (dispose, or page unload).
|
|
620
|
+
return yield* Effect.never;
|
|
411
621
|
}
|
|
412
622
|
const initMountEvents = drainMountEvents();
|
|
413
623
|
yield* Option.match(maybeDevToolsStore, {
|
|
@@ -425,6 +635,14 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
425
635
|
awaitNextFrame,
|
|
426
636
|
isPaused: isPausedEffect,
|
|
427
637
|
render: Effect.gen(function* () {
|
|
638
|
+
// NOTE: a Message that dirtied the model can also be the one
|
|
639
|
+
// whose Command crashed the runtime. Without this guard the
|
|
640
|
+
// next animation frame would render the live view over the
|
|
641
|
+
// crash view.
|
|
642
|
+
const isCrashed = yield* Ref.get(isCrashedRef);
|
|
643
|
+
if (isCrashed) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
428
646
|
const model = yield* Ref.get(modelRef);
|
|
429
647
|
const maybeMessage = yield* Ref.get(maybeLastDirtyMessageRef);
|
|
430
648
|
yield* render(model, maybeMessage);
|
|
@@ -435,18 +653,23 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
435
653
|
});
|
|
436
654
|
}),
|
|
437
655
|
});
|
|
438
|
-
yield* Effect.
|
|
656
|
+
yield* Effect.forkIn(runtimeScope)(renderLoop.pipe(Effect.catchCause(cause => Effect.gen(function* () {
|
|
439
657
|
const maybeMessage = yield* Ref.get(maybeLastDirtyMessageRef);
|
|
440
658
|
yield* crashWith(cause, maybeMessage);
|
|
441
659
|
}))));
|
|
442
|
-
|
|
660
|
+
// NOTE: reloading on bfcache restore is a page-level decision, so
|
|
661
|
+
// only a page-owning runtime installs the listener. An embedded app
|
|
662
|
+
// must never force the host page to reload.
|
|
663
|
+
if (manageDocument) {
|
|
664
|
+
yield* Effect.acquireRelease(Effect.sync(() => addBfcacheRestoreListener()), removeBfcacheRestoreListener => Effect.sync(() => removeBfcacheRestoreListener()));
|
|
665
|
+
}
|
|
443
666
|
if (subscriptions) {
|
|
444
667
|
yield* pipe(subscriptions, Record.toEntries, Effect.forEach(([_key, { dependenciesSchema, modelToDependencies, keepAliveEquivalence, dependenciesToStream, },]) => Effect.gen(function* () {
|
|
445
668
|
const latestDependenciesRef = yield* Ref.make(modelToDependencies(initModel));
|
|
446
669
|
const equivalence = keepAliveEquivalence ??
|
|
447
670
|
Schema.toEquivalence(dependenciesSchema);
|
|
448
671
|
const modelStream = Stream.concat(Stream.make(initModel), Stream.fromPubSub(modelPubSub));
|
|
449
|
-
yield* Effect.
|
|
672
|
+
yield* Effect.forkIn(runtimeScope)(modelStream.pipe(
|
|
450
673
|
// NOTE: Ref.set runs upstream of Stream.changesWith on
|
|
451
674
|
// every model change, so readDependencies() returns
|
|
452
675
|
// current values even when the equivalence filter
|
|
@@ -460,7 +683,7 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
460
683
|
return dependencies;
|
|
461
684
|
})), Stream.changesWith(equivalence), Stream.switchMap(dependencies => dependenciesToStream(dependencies, () => Ref.getUnsafe(latestDependenciesRef))), Stream.runForEach(message =>
|
|
462
685
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
463
|
-
enqueueHigh(message)), provideAllResources));
|
|
686
|
+
enqueueHigh(message)), provideAllResources, Effect.catchCause(cause => crashWith(cause, Option.none()))));
|
|
464
687
|
}), {
|
|
465
688
|
concurrency: 'unbounded',
|
|
466
689
|
discard: true,
|
|
@@ -489,7 +712,7 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
489
712
|
const forkManagedResourceLifecycle = ({ config, ref: resourceRef, }) => Effect.gen(function* () {
|
|
490
713
|
const modelStream = Stream.concat(Stream.make(initModel), Stream.fromPubSub(modelPubSub));
|
|
491
714
|
const equivalence = Schema.toEquivalence(config.schema);
|
|
492
|
-
yield* Effect.
|
|
715
|
+
yield* Effect.forkIn(runtimeScope)(modelStream.pipe(Stream.map(config.modelToMaybeRequirements), Stream.changesWith(equivalence), Stream.switchMap(maybeRequirementsToLifecycle(config, resourceRef)), Stream.runForEach(Effect.flatMap(enqueueHigh))));
|
|
493
716
|
});
|
|
494
717
|
yield* Effect.forEach(managedResourceRefs, forkManagedResourceLifecycle, {
|
|
495
718
|
concurrency: 'unbounded',
|
|
@@ -551,8 +774,20 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
551
774
|
yield* processBatch(Array.prepend(rest, first));
|
|
552
775
|
yield* drainQueue;
|
|
553
776
|
})), Effect.catchCause(cause => crashWith(cause, currentMessage)));
|
|
777
|
+
// NOTE: reached only after the drain loop crashed and the crash view
|
|
778
|
+
// rendered. Suspending keeps the runtime scope open so the crash view
|
|
779
|
+
// and the DevTools overlay stay up for inspection; interruption
|
|
780
|
+
// (dispose, or page unload) still tears everything down.
|
|
781
|
+
yield* Effect.never;
|
|
554
782
|
}));
|
|
555
|
-
|
|
783
|
+
const start = (hmrModel) => startWith(Option.none(), hmrModel);
|
|
784
|
+
const program = { runtimeId, start, ports };
|
|
785
|
+
runtimeInternals.set(program, {
|
|
786
|
+
startWith,
|
|
787
|
+
isEmbedActive: false,
|
|
788
|
+
maybeActiveFiber: Option.none(),
|
|
789
|
+
});
|
|
790
|
+
return program;
|
|
556
791
|
};
|
|
557
792
|
// NOTE: exported for `patchVNode.test.ts` to assert the dedupeSharedVNodes
|
|
558
793
|
// wiring; not part of the public surface (`runtime/public.ts` is curated).
|
|
@@ -594,7 +829,7 @@ const applyDocumentMetadata = (nextDocument, mountedRoot) => {
|
|
|
594
829
|
content: ogUrl,
|
|
595
830
|
});
|
|
596
831
|
};
|
|
597
|
-
const renderCrashView = (context, crash, container, maybeCurrentVNodeRef) => {
|
|
832
|
+
const renderCrashView = (context, crash, container, maybeCurrentVNodeRef, manageDocument) => {
|
|
598
833
|
console.error('[foldkit] Application crash:', context.error);
|
|
599
834
|
if (crash?.report) {
|
|
600
835
|
try {
|
|
@@ -621,7 +856,10 @@ const renderCrashView = (context, crash, container, maybeCurrentVNodeRef) => {
|
|
|
621
856
|
}
|
|
622
857
|
const maybeCurrentVNode = Effect.runSync(Ref.get(maybeCurrentVNodeRef));
|
|
623
858
|
const patchedVNode = patchVNode(maybeCurrentVNode, crashDocument.body, container);
|
|
624
|
-
|
|
859
|
+
Effect.runSync(Ref.set(maybeCurrentVNodeRef, Option.some(patchedVNode)));
|
|
860
|
+
if (manageDocument) {
|
|
861
|
+
applyDocumentMetadata(crashDocument, patchedVNode.elm);
|
|
862
|
+
}
|
|
625
863
|
}
|
|
626
864
|
catch (viewError) {
|
|
627
865
|
console.error('[foldkit] crash.view failed:', viewError);
|
|
@@ -636,14 +874,17 @@ const renderCrashView = (context, crash, container, maybeCurrentVNodeRef) => {
|
|
|
636
874
|
}
|
|
637
875
|
const maybeCurrentVNode = Effect.runSync(Ref.get(maybeCurrentVNodeRef));
|
|
638
876
|
const patchedVNode = patchVNode(maybeCurrentVNode, fallbackDocument.body, container);
|
|
639
|
-
|
|
877
|
+
Effect.runSync(Ref.set(maybeCurrentVNodeRef, Option.some(patchedVNode)));
|
|
878
|
+
if (manageDocument) {
|
|
879
|
+
applyDocumentMetadata(fallbackDocument, patchedVNode.elm);
|
|
880
|
+
}
|
|
640
881
|
}
|
|
641
882
|
};
|
|
642
|
-
export function
|
|
883
|
+
export function makeApplication(config) {
|
|
643
884
|
const { container } = config;
|
|
644
885
|
if (container === null) {
|
|
645
886
|
throw new Error('[foldkit] Container is null. Make sure the element exists in the DOM ' +
|
|
646
|
-
'before calling
|
|
887
|
+
'before calling makeApplication (e.g. that your <div id="root"></div> has ' +
|
|
647
888
|
'rendered, and your script runs after it).');
|
|
648
889
|
}
|
|
649
890
|
const hasRouting = 'routing' in config;
|
|
@@ -655,6 +896,8 @@ export function makeProgram(config) {
|
|
|
655
896
|
Model: config.Model,
|
|
656
897
|
update: config.update,
|
|
657
898
|
view: config.view,
|
|
899
|
+
manageDocument: true,
|
|
900
|
+
ports: config.ports,
|
|
658
901
|
...(config.subscriptions && { subscriptions: config.subscriptions }),
|
|
659
902
|
container,
|
|
660
903
|
...(hasRouting && { routing: config.routing }),
|
|
@@ -708,6 +951,79 @@ export function makeProgram(config) {
|
|
|
708
951
|
}
|
|
709
952
|
/* eslint-enable @typescript-eslint/consistent-type-assertions */
|
|
710
953
|
}
|
|
954
|
+
const toCrashConfig = (nullableCrash) => {
|
|
955
|
+
if (Predicate.isUndefined(nullableCrash)) {
|
|
956
|
+
return undefined;
|
|
957
|
+
}
|
|
958
|
+
const elementCrashView = nullableCrash.view;
|
|
959
|
+
return {
|
|
960
|
+
...(Predicate.isNotUndefined(elementCrashView) && {
|
|
961
|
+
view: (context) => ({
|
|
962
|
+
title: '',
|
|
963
|
+
body: elementCrashView(context),
|
|
964
|
+
}),
|
|
965
|
+
}),
|
|
966
|
+
...(Predicate.isNotUndefined(nullableCrash.report) && {
|
|
967
|
+
report: nullableCrash.report,
|
|
968
|
+
}),
|
|
969
|
+
};
|
|
970
|
+
};
|
|
971
|
+
export function makeElement(config) {
|
|
972
|
+
const { container } = config;
|
|
973
|
+
if (container === null) {
|
|
974
|
+
throw new Error('[foldkit] Container is null. Make sure the element exists in the DOM ' +
|
|
975
|
+
'before calling makeElement (e.g. that your <div id="root"></div> has ' +
|
|
976
|
+
'rendered, and your script runs after it).');
|
|
977
|
+
}
|
|
978
|
+
const hasFlags = 'Flags' in config;
|
|
979
|
+
const elementView = config.view;
|
|
980
|
+
const view = (model) => ({
|
|
981
|
+
title: '',
|
|
982
|
+
body: elementView(model),
|
|
983
|
+
});
|
|
984
|
+
const nullableCrash = toCrashConfig(config.crash);
|
|
985
|
+
const baseConfig = {
|
|
986
|
+
Model: config.Model,
|
|
987
|
+
update: config.update,
|
|
988
|
+
view,
|
|
989
|
+
manageDocument: false,
|
|
990
|
+
ports: config.ports,
|
|
991
|
+
...(config.subscriptions && { subscriptions: config.subscriptions }),
|
|
992
|
+
container,
|
|
993
|
+
...(Predicate.isNotUndefined(nullableCrash) && { crash: nullableCrash }),
|
|
994
|
+
...(Predicate.isNotUndefined(config.slowView) && {
|
|
995
|
+
slowView: config.slowView,
|
|
996
|
+
}),
|
|
997
|
+
...(Predicate.isNotUndefined(config.freezeModel) && {
|
|
998
|
+
freezeModel: config.freezeModel,
|
|
999
|
+
}),
|
|
1000
|
+
...(config.resources && { resources: config.resources }),
|
|
1001
|
+
...(config.managedResources && {
|
|
1002
|
+
managedResources: config.managedResources,
|
|
1003
|
+
}),
|
|
1004
|
+
...(Predicate.isNotUndefined(config.devTools) && {
|
|
1005
|
+
devTools: config.devTools,
|
|
1006
|
+
}),
|
|
1007
|
+
};
|
|
1008
|
+
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
|
1009
|
+
if (hasFlags) {
|
|
1010
|
+
return makeRuntime({
|
|
1011
|
+
...baseConfig,
|
|
1012
|
+
Flags: config.Flags,
|
|
1013
|
+
flags: config.flags,
|
|
1014
|
+
init: (flags) => config.init(flags),
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
else {
|
|
1018
|
+
return makeRuntime({
|
|
1019
|
+
...baseConfig,
|
|
1020
|
+
Flags: Schema.Void,
|
|
1021
|
+
flags: Effect.succeed(undefined),
|
|
1022
|
+
init: () => config.init(),
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
/* eslint-enable @typescript-eslint/consistent-type-assertions */
|
|
1026
|
+
}
|
|
711
1027
|
const encodePreserveModelMessage = Schema.encodeUnknownSync(PreserveModelMessage);
|
|
712
1028
|
const encodeRequestModelMessage = Schema.encodeUnknownSync(RequestModelMessage);
|
|
713
1029
|
const decodeRestoreModelMessage = Schema.decodeUnknownExit(RestoreModelMessage);
|
|
@@ -735,36 +1051,120 @@ const microtaskSetImmediate = (callback) => {
|
|
|
735
1051
|
};
|
|
736
1052
|
const browserScheduler = new Scheduler.MixedScheduler('async', microtaskSetImmediate);
|
|
737
1053
|
const provideBrowserScheduler = (effect) => Effect.provide(effect, Layer.succeed(Scheduler.Scheduler, browserScheduler));
|
|
738
|
-
|
|
1054
|
+
// NOTE: asks @foldkit/vite-plugin for a model preserved across the last HMR
|
|
1055
|
+
// reload. The plugin only serves a model whose preservation was flushed by a
|
|
1056
|
+
// reload, so a host-driven dispose-then-embed remount initializes fresh while
|
|
1057
|
+
// a code reload restores state.
|
|
1058
|
+
const resolveHmrModel = (runtimeId) => {
|
|
1059
|
+
const hot = import.meta.hot;
|
|
1060
|
+
if (!hot) {
|
|
1061
|
+
return Effect.succeed(undefined);
|
|
1062
|
+
}
|
|
1063
|
+
return pipe(Effect.callback(resume => {
|
|
1064
|
+
const handler = (message) => {
|
|
1065
|
+
Exit.match(decodeRestoreModelMessage(message), {
|
|
1066
|
+
onFailure: Function.constVoid,
|
|
1067
|
+
onSuccess: ({ id, model }) => {
|
|
1068
|
+
if (id === runtimeId) {
|
|
1069
|
+
hot.off('foldkit:restore-model', handler);
|
|
1070
|
+
resume(Effect.succeed(model));
|
|
1071
|
+
}
|
|
1072
|
+
},
|
|
1073
|
+
});
|
|
1074
|
+
};
|
|
1075
|
+
hot.on('foldkit:restore-model', handler);
|
|
1076
|
+
hot.send('foldkit:request-model', encodeRequestModelMessage(RequestModelMessage.make({ id: runtimeId })));
|
|
1077
|
+
return Effect.sync(() => hot.off('foldkit:restore-model', handler));
|
|
1078
|
+
}), Effect.timeout(PLUGIN_RESPONSE_TIMEOUT_MS), Effect.catchTag('TimeoutError', () => {
|
|
1079
|
+
console.warn('[foldkit] No response from @foldkit/vite-plugin. Add it to your vite.config.ts for HMR model preservation:\n\n' +
|
|
1080
|
+
" import { foldkit } from '@foldkit/vite-plugin'\n\n" +
|
|
1081
|
+
' export default defineConfig({ plugins: [foldkit()] })\n\n' +
|
|
1082
|
+
'Starting without HMR support.');
|
|
1083
|
+
return Effect.succeed(undefined);
|
|
1084
|
+
}));
|
|
1085
|
+
};
|
|
1086
|
+
/** Starts a Foldkit runtime that owns the page for the page's whole lifetime,
|
|
1087
|
+
* with HMR support for development. To start a runtime under a
|
|
1088
|
+
* host-controlled lifecycle instead, use `embed`. */
|
|
739
1089
|
export const run = (program) => {
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
if (id === runtimeId) {
|
|
749
|
-
hot.off('foldkit:restore-model', handler);
|
|
750
|
-
resume(Effect.succeed(model));
|
|
751
|
-
}
|
|
752
|
-
},
|
|
753
|
-
});
|
|
1090
|
+
BrowserRuntime.runMain(provideBrowserScheduler(Effect.flatMap(resolveHmrModel(program.runtimeId), program.start)));
|
|
1091
|
+
};
|
|
1092
|
+
const buildPortHandles = (ports, connector) => {
|
|
1093
|
+
const handles = {};
|
|
1094
|
+
if (Predicate.isNotUndefined(ports)) {
|
|
1095
|
+
Object.entries(ports.inbound ?? {}).forEach(([portName, port]) => {
|
|
1096
|
+
handles[portName] = {
|
|
1097
|
+
send: (value) => connector.sendInbound(portName, port, value),
|
|
754
1098
|
};
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
' export default defineConfig({ plugins: [foldkit()] })\n\n' +
|
|
762
|
-
'Starting without HMR support.');
|
|
763
|
-
return Effect.succeed(undefined);
|
|
764
|
-
}), Effect.flatMap(start));
|
|
765
|
-
BrowserRuntime.runMain(provideBrowserScheduler(requestPreservedModel));
|
|
1099
|
+
});
|
|
1100
|
+
Object.entries(ports.outbound ?? {}).forEach(([portName, port]) => {
|
|
1101
|
+
handles[portName] = {
|
|
1102
|
+
subscribe: (listener) => connector.addListener(port, listener),
|
|
1103
|
+
};
|
|
1104
|
+
});
|
|
766
1105
|
}
|
|
767
|
-
|
|
768
|
-
|
|
1106
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
1107
|
+
return handles;
|
|
1108
|
+
};
|
|
1109
|
+
/**
|
|
1110
|
+
* Starts a Foldkit runtime under a host-controlled lifecycle and returns an
|
|
1111
|
+
* `EmbedHandle`. This is the entry point for embedding a Foldkit app inside
|
|
1112
|
+
* another application: the host pushes values in through the handle's inbound
|
|
1113
|
+
* Ports, listens to outbound Ports, and calls `dispose` when it unmounts the
|
|
1114
|
+
* app. The host never touches the Model or dispatches Messages directly; the
|
|
1115
|
+
* Schema-typed Ports are the whole boundary.
|
|
1116
|
+
*
|
|
1117
|
+
* Works with programs from both `makeApplication` and `makeElement`; for a
|
|
1118
|
+
* widget on a page the host owns, `makeElement` is the natural fit.
|
|
1119
|
+
*
|
|
1120
|
+
* A program can be embedded once at a time (it owns one container). After
|
|
1121
|
+
* `dispose`, the same container can be embedded again with a fresh program.
|
|
1122
|
+
*
|
|
1123
|
+
* ```ts
|
|
1124
|
+
* const handle = Runtime.embed(element)
|
|
1125
|
+
*
|
|
1126
|
+
* handle.ports.stepChanged.send(5)
|
|
1127
|
+
* const unsubscribe = handle.ports.countChanged.subscribe(count => {
|
|
1128
|
+
* console.log(count)
|
|
1129
|
+
* })
|
|
1130
|
+
*
|
|
1131
|
+
* handle.dispose()
|
|
1132
|
+
* ```
|
|
1133
|
+
*/
|
|
1134
|
+
export const embed = (program) => {
|
|
1135
|
+
const nullableInternals = runtimeInternals.get(program);
|
|
1136
|
+
if (Predicate.isUndefined(nullableInternals)) {
|
|
1137
|
+
throw new Error('[foldkit] embed expects a program created by makeApplication or makeElement.');
|
|
1138
|
+
}
|
|
1139
|
+
const internals = nullableInternals;
|
|
1140
|
+
if (internals.isEmbedActive) {
|
|
1141
|
+
throw new Error('[foldkit] This program is already embedded. Dispose the existing ' +
|
|
1142
|
+
'handle first, or create a separate program: each program owns one ' +
|
|
1143
|
+
'container.');
|
|
769
1144
|
}
|
|
1145
|
+
internals.isEmbedActive = true;
|
|
1146
|
+
const connector = makeHostConnector();
|
|
1147
|
+
// NOTE: a dispose immediately followed by a fresh embed (React strict mode
|
|
1148
|
+
// runs effects exactly that way) must not start the new runtime while the
|
|
1149
|
+
// old one is still tearing down: the teardown finalizer is what puts the
|
|
1150
|
+
// container element back in the DOM. Awaiting the previous fiber's exit
|
|
1151
|
+
// sequences the two.
|
|
1152
|
+
const startEffect = pipe(Option.match(internals.maybeActiveFiber, {
|
|
1153
|
+
onNone: () => Effect.void,
|
|
1154
|
+
onSome: previousFiber => Effect.asVoid(Fiber.await(previousFiber)),
|
|
1155
|
+
}), Effect.andThen(resolveHmrModel(program.runtimeId)), Effect.flatMap(hmrModel => internals.startWith(Option.some(connector), hmrModel)));
|
|
1156
|
+
const fiber = Effect.runFork(provideBrowserScheduler(startEffect));
|
|
1157
|
+
internals.maybeActiveFiber = Option.some(fiber);
|
|
1158
|
+
let isHandleDisposed = false;
|
|
1159
|
+
const dispose = () => {
|
|
1160
|
+
if (isHandleDisposed) {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
isHandleDisposed = true;
|
|
1164
|
+
connector.dispose();
|
|
1165
|
+
internals.isEmbedActive = false;
|
|
1166
|
+
Effect.runFork(Fiber.interrupt(fiber));
|
|
1167
|
+
};
|
|
1168
|
+
const ports = buildPortHandles(program.ports, connector);
|
|
1169
|
+
return { ports, dispose };
|
|
770
1170
|
};
|