foldkit 0.157.0 → 0.158.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.
Files changed (82) hide show
  1. package/dist/devTools/host.d.ts +2 -2
  2. package/dist/devTools/host.d.ts.map +1 -1
  3. package/dist/devTools/host.js +1 -1
  4. package/dist/experimental/machine/machine.d.ts +278 -84
  5. package/dist/experimental/machine/machine.d.ts.map +1 -1
  6. package/dist/experimental/machine/machine.js +362 -212
  7. package/dist/runtime/browserListeners.d.ts +7 -1
  8. package/dist/runtime/browserListeners.d.ts.map +1 -1
  9. package/dist/runtime/browserScheduler.d.ts +3 -0
  10. package/dist/runtime/browserScheduler.d.ts.map +1 -0
  11. package/dist/runtime/browserScheduler.js +22 -0
  12. package/dist/runtime/crashUI.d.ts +36 -5
  13. package/dist/runtime/crashUI.d.ts.map +1 -1
  14. package/dist/runtime/crashUI.js +55 -2
  15. package/dist/runtime/devToolsConfig.d.ts +85 -0
  16. package/dist/runtime/devToolsConfig.d.ts.map +1 -0
  17. package/dist/runtime/devToolsConfig.js +49 -0
  18. package/dist/runtime/devToolsIntegration.d.ts +56 -0
  19. package/dist/runtime/devToolsIntegration.d.ts.map +1 -0
  20. package/dist/runtime/devToolsIntegration.js +186 -0
  21. package/dist/runtime/dispatch.d.ts +10 -0
  22. package/dist/runtime/dispatch.d.ts.map +1 -0
  23. package/dist/runtime/dispatch.js +4 -0
  24. package/dist/runtime/documentMetadata.d.ts +3 -0
  25. package/dist/runtime/documentMetadata.d.ts.map +1 -0
  26. package/dist/runtime/documentMetadata.js +148 -0
  27. package/dist/runtime/duplicateIdScanner.d.ts +16 -0
  28. package/dist/runtime/duplicateIdScanner.d.ts.map +1 -0
  29. package/dist/runtime/duplicateIdScanner.js +70 -0
  30. package/dist/runtime/hmrModelBridge.d.ts +4 -0
  31. package/dist/runtime/hmrModelBridge.d.ts.map +1 -0
  32. package/dist/runtime/hmrModelBridge.js +43 -0
  33. package/dist/runtime/hostConnector.d.ts +61 -0
  34. package/dist/runtime/hostConnector.d.ts.map +1 -0
  35. package/dist/runtime/hostConnector.js +141 -0
  36. package/dist/runtime/hydrationHandoff.d.ts +44 -0
  37. package/dist/runtime/hydrationHandoff.d.ts.map +1 -0
  38. package/dist/runtime/hydrationHandoff.js +395 -0
  39. package/dist/runtime/index.d.ts +16 -1
  40. package/dist/runtime/index.d.ts.map +1 -1
  41. package/dist/runtime/index.js +5 -1
  42. package/dist/runtime/makeApplication.d.ts +71 -0
  43. package/dist/runtime/makeApplication.d.ts.map +1 -0
  44. package/dist/runtime/makeApplication.js +98 -0
  45. package/dist/runtime/makeElement.d.ts +69 -0
  46. package/dist/runtime/makeElement.d.ts.map +1 -0
  47. package/dist/runtime/makeElement.js +81 -0
  48. package/dist/runtime/managedResourceFibers.d.ts +18 -0
  49. package/dist/runtime/managedResourceFibers.d.ts.map +1 -0
  50. package/dist/runtime/managedResourceFibers.js +47 -0
  51. package/dist/runtime/messageQueue.d.ts +27 -0
  52. package/dist/runtime/messageQueue.d.ts.map +1 -0
  53. package/dist/runtime/messageQueue.js +133 -0
  54. package/dist/runtime/public.d.ts +2 -3
  55. package/dist/runtime/public.d.ts.map +1 -1
  56. package/dist/runtime/public.js +1 -1
  57. package/dist/runtime/renderer.d.ts +69 -0
  58. package/dist/runtime/renderer.d.ts.map +1 -0
  59. package/dist/runtime/renderer.js +512 -0
  60. package/dist/runtime/resourceProvider.d.ts +33 -0
  61. package/dist/runtime/resourceProvider.d.ts.map +1 -0
  62. package/dist/runtime/resourceProvider.js +98 -0
  63. package/dist/runtime/runtime.d.ts +133 -412
  64. package/dist/runtime/runtime.d.ts.map +1 -1
  65. package/dist/runtime/runtime.js +118 -2184
  66. package/dist/runtime/runtimeStatus.d.ts +15 -0
  67. package/dist/runtime/runtimeStatus.d.ts.map +1 -0
  68. package/dist/runtime/runtimeStatus.js +6 -0
  69. package/dist/runtime/slowPhase.d.ts +82 -0
  70. package/dist/runtime/slowPhase.d.ts.map +1 -0
  71. package/dist/runtime/slowPhase.js +83 -0
  72. package/dist/runtime/start.d.ts +96 -0
  73. package/dist/runtime/start.d.ts.map +1 -0
  74. package/dist/runtime/start.js +99 -0
  75. package/dist/runtime/subscriptionFibers.d.ts +21 -0
  76. package/dist/runtime/subscriptionFibers.d.ts.map +1 -0
  77. package/dist/runtime/subscriptionFibers.js +47 -0
  78. package/dist/runtime/visibility.d.ts +8 -0
  79. package/dist/runtime/visibility.d.ts.map +1 -0
  80. package/dist/runtime/visibility.js +6 -0
  81. package/dist/test/scene.js +1 -1
  82. package/package.json +1 -1
@@ -0,0 +1,148 @@
1
+ import { Array } from 'effect';
2
+ import { textDirectionToAttribute } from '../html/index.js';
3
+ const currentLocationUrl = () => {
4
+ const { origin, pathname, search } = window.location;
5
+ return `${origin}${pathname}${search}`;
6
+ };
7
+ const documentMetadataStates = new WeakMap();
8
+ const observeDocumentMetadataMutations = (observer) => {
9
+ const observedHead = document.head;
10
+ observer.observe(observedHead, {
11
+ attributes: true,
12
+ attributeFilter: ['rel', 'href', 'property', 'content'],
13
+ childList: true,
14
+ characterData: true,
15
+ subtree: true,
16
+ });
17
+ observer.observe(document.documentElement, {
18
+ attributes: true,
19
+ attributeFilter: ['lang', 'dir'],
20
+ });
21
+ return observedHead;
22
+ };
23
+ const getOrCreateDocumentMetadataState = () => {
24
+ const existingState = documentMetadataStates.get(document);
25
+ if (existingState !== undefined) {
26
+ return existingState;
27
+ }
28
+ const invalidation = { isInvalidated: true };
29
+ const observer = new MutationObserver(() => {
30
+ invalidation.isInvalidated = true;
31
+ });
32
+ const metadataState = {
33
+ elements: {},
34
+ observer,
35
+ invalidation,
36
+ observedHead: observeDocumentMetadataMutations(observer),
37
+ };
38
+ documentMetadataStates.set(document, metadataState);
39
+ return metadataState;
40
+ };
41
+ const findOrCreateDocumentMetadataElements = (metadataState) => {
42
+ const { elements } = metadataState;
43
+ let canonical = elements.canonical;
44
+ if (canonical === undefined || canonical.parentNode !== document.head) {
45
+ canonical =
46
+ document.head.querySelector('link[rel="canonical"]') ??
47
+ document.head.appendChild(document.createElement('link'));
48
+ elements.canonical = canonical;
49
+ }
50
+ let ogUrl = elements.ogUrl;
51
+ if (ogUrl === undefined || ogUrl.parentNode !== document.head) {
52
+ ogUrl =
53
+ document.head.querySelector('meta[property="og:url"]') ??
54
+ document.head.appendChild(document.createElement('meta'));
55
+ elements.ogUrl = ogUrl;
56
+ }
57
+ return { canonical, ogUrl };
58
+ };
59
+ const readOrCacheCurrentLocationUrl = (metadataState) => {
60
+ const currentLocationHref = window.location.href;
61
+ if (metadataState.cachedLocationHref === currentLocationHref &&
62
+ metadataState.cachedLocationCanonical !== undefined) {
63
+ return metadataState.cachedLocationCanonical;
64
+ }
65
+ const canonicalUrl = currentLocationUrl();
66
+ metadataState.cachedLocationHref = currentLocationHref;
67
+ metadataState.cachedLocationCanonical = canonicalUrl;
68
+ return canonicalUrl;
69
+ };
70
+ const rebindDocumentMetadataObserverToCurrentHead = (metadataState) => {
71
+ // NOTE: a MutationObserver remains attached to a detached head after
72
+ // document.head is replaced. Rebind it and invalidate the metadata snapshot
73
+ // before the next unchanged guard.
74
+ metadataState.observer.disconnect();
75
+ metadataState.observedHead = observeDocumentMetadataMutations(metadataState.observer);
76
+ metadataState.invalidation.isInvalidated = true;
77
+ };
78
+ const reconcileDocumentMetadata = (metadataState, nextMetadata) => {
79
+ if (document.title !== nextMetadata.title) {
80
+ document.title = nextMetadata.title;
81
+ }
82
+ const { documentElement } = document;
83
+ if (nextMetadata.lang !== undefined &&
84
+ documentElement.lang !== nextMetadata.lang) {
85
+ documentElement.lang = nextMetadata.lang;
86
+ }
87
+ if (nextMetadata.dirAttribute !== undefined &&
88
+ documentElement.dir !== nextMetadata.dirAttribute) {
89
+ documentElement.dir = nextMetadata.dirAttribute;
90
+ }
91
+ const metadataElements = findOrCreateDocumentMetadataElements(metadataState);
92
+ if (metadataElements.canonical.getAttribute('rel') !== 'canonical') {
93
+ metadataElements.canonical.setAttribute('rel', 'canonical');
94
+ }
95
+ if (metadataElements.canonical.getAttribute('href') !==
96
+ nextMetadata.canonicalUrl) {
97
+ metadataElements.canonical.setAttribute('href', nextMetadata.canonicalUrl);
98
+ }
99
+ if (metadataElements.ogUrl.getAttribute('property') !== 'og:url') {
100
+ metadataElements.ogUrl.setAttribute('property', 'og:url');
101
+ }
102
+ if (metadataElements.ogUrl.getAttribute('content') !== nextMetadata.ogUrl) {
103
+ metadataElements.ogUrl.setAttribute('content', nextMetadata.ogUrl);
104
+ }
105
+ metadataState.lastApplied = nextMetadata;
106
+ metadataState.invalidation.isInvalidated = false;
107
+ // NOTE: clear the records produced by Foldkit's own writes before the
108
+ // observer callback runs. Otherwise the next unchanged render would be
109
+ // marked dirty and repeat every DOM read this cache is meant to avoid.
110
+ metadataState.observer.takeRecords();
111
+ };
112
+ export const applyDocumentMetadata = (nextDocument, mountedRoot) => {
113
+ if (!mountedRoot || !document.body.contains(mountedRoot)) {
114
+ return;
115
+ }
116
+ const metadataState = getOrCreateDocumentMetadataState();
117
+ if (metadataState.observedHead !== document.head) {
118
+ rebindDocumentMetadataObserverToCurrentHead(metadataState);
119
+ }
120
+ const canonicalUrl = nextDocument.canonical ?? readOrCacheCurrentLocationUrl(metadataState);
121
+ const ogUrl = nextDocument.ogUrl ?? canonicalUrl;
122
+ const dirAttribute = nextDocument.dir === undefined
123
+ ? undefined
124
+ : textDirectionToAttribute(nextDocument.dir);
125
+ // NOTE: MutationObserver callbacks are asynchronous. Consume queued records
126
+ // synchronously before trusting the unchanged fast path.
127
+ if (Array.isArrayNonEmpty(metadataState.observer.takeRecords())) {
128
+ metadataState.invalidation.isInvalidated = true;
129
+ }
130
+ const lastApplied = metadataState.lastApplied;
131
+ const isAppliedMetadataCurrent = !metadataState.invalidation.isInvalidated &&
132
+ lastApplied !== undefined &&
133
+ lastApplied.title === nextDocument.title &&
134
+ lastApplied.lang === nextDocument.lang &&
135
+ lastApplied.dirAttribute === dirAttribute &&
136
+ lastApplied.canonicalUrl === canonicalUrl &&
137
+ lastApplied.ogUrl === ogUrl;
138
+ if (isAppliedMetadataCurrent) {
139
+ return;
140
+ }
141
+ reconcileDocumentMetadata(metadataState, {
142
+ title: nextDocument.title,
143
+ lang: nextDocument.lang,
144
+ dirAttribute,
145
+ canonicalUrl,
146
+ ogUrl,
147
+ });
148
+ };
@@ -0,0 +1,16 @@
1
+ /** The coalescing scanner: `schedule` queues a scan of a root, `cancel` drops it. */
2
+ export type DuplicateIdScanner = Readonly<{
3
+ schedule: (root: Node | undefined) => void;
4
+ cancel: () => void;
5
+ }>;
6
+ /** Coalesces `warnDuplicateIds` scans so rapid successive renders trigger at
7
+ * most one full-tree scan per `DUPLICATE_ID_SCAN_INTERVAL_MS`, bounding the
8
+ * cost under high-frequency dev rendering such as animationFrame subscriptions
9
+ * or per-keystroke input. A real duplicate id persists across renders, so
10
+ * scanning the latest tree on a trailing timer never misses a genuine
11
+ * collision, and the retained `warnedIds` keeps each id to a single warning.
12
+ * Dev-only: the runtime both creates the scanner and calls `schedule` behind
13
+ * `import.meta.hot`, so the scanner and its DOM scan tree-shake out of
14
+ * production builds. */
15
+ export declare const createDuplicateIdScanner: () => DuplicateIdScanner;
16
+ //# sourceMappingURL=duplicateIdScanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duplicateIdScanner.d.ts","sourceRoot":"","sources":["../../src/runtime/duplicateIdScanner.ts"],"names":[],"mappings":"AAiDA,qFAAqF;AACrF,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,KAAK,IAAI,CAAA;IAC1C,MAAM,EAAE,MAAM,IAAI,CAAA;CACnB,CAAC,CAAA;AAEF;;;;;;;;yBAQyB;AACzB,eAAO,MAAM,wBAAwB,QAAO,kBAyB3C,CAAA"}
@@ -0,0 +1,70 @@
1
+ import { Array } from 'effect';
2
+ /** Dev-only scan for duplicate DOM ids within the Foldkit-rendered root.
3
+ *
4
+ * Duplicate ids are invalid HTML but browsers do not report them, so
5
+ * `getElementById` / `querySelector` silently resolve to the first match and
6
+ * focus or ARIA labelling can target the wrong element with no error. The
7
+ * scan is scoped to `root` (not the whole document) so unrelated page ids do
8
+ * not trigger it, warns rather than throws, and dedupes through `warnedIds`
9
+ * so the same collision is reported at most once instead of every render. */
10
+ const warnDuplicateIds = (root, warnedIds) => {
11
+ if (!(root instanceof Element)) {
12
+ return;
13
+ }
14
+ const seenIds = new Set();
15
+ const duplicateIds = new Set();
16
+ const elementsWithId = Array.fromIterable(root.querySelectorAll('[id]'));
17
+ if (root.id !== '') {
18
+ elementsWithId.unshift(root);
19
+ }
20
+ for (const element of elementsWithId) {
21
+ const { id } = element;
22
+ if (seenIds.has(id)) {
23
+ duplicateIds.add(id);
24
+ }
25
+ else {
26
+ seenIds.add(id);
27
+ }
28
+ }
29
+ for (const id of duplicateIds) {
30
+ if (!warnedIds.has(id)) {
31
+ warnedIds.add(id);
32
+ console.warn(`[foldkit] Duplicate DOM id "${id}" in the rendered tree. Ids must be ` +
33
+ 'unique within the document; otherwise focus and ARIA labelling can ' +
34
+ 'silently target the wrong element. Give each element a unique id.');
35
+ }
36
+ }
37
+ };
38
+ const DUPLICATE_ID_SCAN_INTERVAL_MS = 1000;
39
+ /** Coalesces `warnDuplicateIds` scans so rapid successive renders trigger at
40
+ * most one full-tree scan per `DUPLICATE_ID_SCAN_INTERVAL_MS`, bounding the
41
+ * cost under high-frequency dev rendering such as animationFrame subscriptions
42
+ * or per-keystroke input. A real duplicate id persists across renders, so
43
+ * scanning the latest tree on a trailing timer never misses a genuine
44
+ * collision, and the retained `warnedIds` keeps each id to a single warning.
45
+ * Dev-only: the runtime both creates the scanner and calls `schedule` behind
46
+ * `import.meta.hot`, so the scanner and its DOM scan tree-shake out of
47
+ * production builds. */
48
+ export const createDuplicateIdScanner = () => {
49
+ const warnedIds = new Set();
50
+ let timeoutHandle;
51
+ let latestRoot;
52
+ const schedule = (root) => {
53
+ latestRoot = root;
54
+ if (timeoutHandle !== undefined) {
55
+ return;
56
+ }
57
+ timeoutHandle = setTimeout(() => {
58
+ timeoutHandle = undefined;
59
+ warnDuplicateIds(latestRoot, warnedIds);
60
+ }, DUPLICATE_ID_SCAN_INTERVAL_MS);
61
+ };
62
+ const cancel = () => {
63
+ if (timeoutHandle !== undefined) {
64
+ clearTimeout(timeoutHandle);
65
+ timeoutHandle = undefined;
66
+ }
67
+ latestRoot = undefined;
68
+ };
69
+ return { schedule, cancel };
70
+ };
@@ -0,0 +1,4 @@
1
+ import { Effect } from 'effect';
2
+ export declare const preserveModel: (id: string, encodedModel: unknown, isHmrReload: boolean) => void;
3
+ export declare const resolveHmrModel: (runtimeId: string) => Effect.Effect<unknown>;
4
+ //# sourceMappingURL=hmrModelBridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hmrModelBridge.d.ts","sourceRoot":"","sources":["../../src/runtime/hmrModelBridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAgC,MAAM,QAAQ,CAAA;AAa7D,eAAO,MAAM,aAAa,OACpB,MAAM,gBACI,OAAO,eACR,OAAO,KACnB,IASF,CAAA;AAQD,eAAO,MAAM,eAAe,cAAe,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAqCxE,CAAA"}
@@ -0,0 +1,43 @@
1
+ import { Effect, Exit, Function, Schema, pipe } from 'effect';
2
+ import { PreserveModelMessage, RequestModelMessage, RestoreModelMessage, } from './hmrProtocol.js';
3
+ const encodePreserveModelMessage = Schema.encodeUnknownSync(PreserveModelMessage);
4
+ const encodeRequestModelMessage = Schema.encodeUnknownSync(RequestModelMessage);
5
+ const decodeRestoreModelMessage = Schema.decodeUnknownExit(RestoreModelMessage);
6
+ export const preserveModel = (id, encodedModel, isHmrReload) => {
7
+ if (import.meta.hot) {
8
+ import.meta.hot.send('foldkit:preserve-model', encodePreserveModelMessage(PreserveModelMessage.make({ id, model: encodedModel, isHmrReload })));
9
+ }
10
+ };
11
+ const PLUGIN_RESPONSE_TIMEOUT_MS = 500;
12
+ // NOTE: asks @foldkit/vite-plugin for a model preserved across the last HMR
13
+ // reload. The plugin only serves a model whose preservation was flushed by a
14
+ // reload, so a host-driven dispose-then-embed remount initializes fresh while
15
+ // a code reload restores state.
16
+ export const resolveHmrModel = (runtimeId) => {
17
+ const hot = import.meta.hot;
18
+ if (!hot) {
19
+ return Effect.succeed(undefined);
20
+ }
21
+ return pipe(Effect.callback(resume => {
22
+ const handler = (message) => {
23
+ Exit.match(decodeRestoreModelMessage(message), {
24
+ onFailure: Function.constVoid,
25
+ onSuccess: ({ id, model }) => {
26
+ if (id === runtimeId) {
27
+ hot.off('foldkit:restore-model', handler);
28
+ resume(Effect.succeed(model));
29
+ }
30
+ },
31
+ });
32
+ };
33
+ hot.on('foldkit:restore-model', handler);
34
+ hot.send('foldkit:request-model', encodeRequestModelMessage(RequestModelMessage.make({ id: runtimeId })));
35
+ return Effect.sync(() => hot.off('foldkit:restore-model', handler));
36
+ }), Effect.timeout(PLUGIN_RESPONSE_TIMEOUT_MS), Effect.catchTag('TimeoutError', () => {
37
+ console.warn('[foldkit] No response from @foldkit/vite-plugin. Add it to your vite.config.ts for HMR model preservation:\n\n' +
38
+ " import { foldkit } from '@foldkit/vite-plugin'\n\n" +
39
+ ' export default defineConfig({ plugins: [foldkit()] })\n\n' +
40
+ 'Starting without HMR support.');
41
+ return Effect.succeed(undefined);
42
+ }));
43
+ };
@@ -0,0 +1,61 @@
1
+ import { Exit, Option, Schema } from 'effect';
2
+ import { type Inbound, type Outbound, type Ports, type __PortChannels } from '../port/index.js';
3
+ /** Host-side handle for one inbound Port. `send` validates the value by
4
+ * decoding it against the Port's Schema: on success the decoded value enters
5
+ * the app through the Port's Subscription; on failure nothing reaches the
6
+ * app, the failure is logged, and the returned `Exit` carries the
7
+ * `SchemaError`. Sends after `dispose` are no-ops. */
8
+ export type InboundPortHandle<Encoded> = Readonly<{
9
+ send: (value: Encoded) => Exit.Exit<void, Schema.SchemaError>;
10
+ }>;
11
+ /** Host-side handle for one outbound Port. `subscribe` registers a listener
12
+ * for the encoded values the app emits with `Port.emit` and returns an
13
+ * unsubscribe function. Multiple listeners receive each value in
14
+ * registration order. */
15
+ export type OutboundPortHandle<Encoded> = Readonly<{
16
+ subscribe: (listener: (value: Encoded) => void) => () => void;
17
+ }>;
18
+ /** The inbound half of `PortHandles`: one `InboundPortHandle` per declared
19
+ * inbound Port, keyed by Port name. */
20
+ export type InboundPortHandles<InboundPorts> = InboundPorts extends Readonly<Record<string, Inbound<any, any>>> ? {
21
+ readonly [Name in keyof InboundPorts]: InboundPorts[Name] extends Inbound<any, infer Encoded> ? InboundPortHandle<Encoded> : never;
22
+ } : unknown;
23
+ /** The outbound half of `PortHandles`: one `OutboundPortHandle` per declared
24
+ * outbound Port, keyed by Port name. */
25
+ export type OutboundPortHandles<OutboundPorts> = OutboundPorts extends Readonly<Record<string, Outbound<any, any>>> ? {
26
+ readonly [Name in keyof OutboundPorts]: OutboundPorts[Name] extends Outbound<any, infer Encoded> ? OutboundPortHandle<Encoded> : never;
27
+ } : unknown;
28
+ /** The `ports` field of an `EmbedHandle`: one `InboundPortHandle` or
29
+ * `OutboundPortHandle` per declared Port, keyed by Port name. */
30
+ export type PortHandles<P extends Ports | undefined> = P extends Ports ? InboundPortHandles<P['inbound']> & OutboundPortHandles<P['outbound']> : unknown;
31
+ /**
32
+ * The handle returned by `embed`. The host talks to the embedded app only
33
+ * through it: `ports.<name>.send` pushes values in, `ports.<name>.subscribe`
34
+ * listens to values the app emits, and `dispose` shuts the runtime down.
35
+ *
36
+ * `dispose` is idempotent. It interrupts the runtime and runs all cleanup:
37
+ * Subscriptions, ManagedResources, Mounts, listeners, and in-flight Commands
38
+ * stop, and the rendered DOM is removed with the container element restored
39
+ * empty in its place, ready for a fresh `embed`.
40
+ */
41
+ export type EmbedHandle<P extends Ports | undefined = undefined> = Readonly<{
42
+ ports: PortHandles<P>;
43
+ dispose: () => void;
44
+ }>;
45
+ export type HostConnector = Readonly<{
46
+ sendInbound: (portName: string, port: Inbound<any, any>, value: unknown) => Exit.Exit<void, Schema.SchemaError>;
47
+ addListener: (port: Outbound<any, any>, listener: (encodedValue: unknown) => void) => () => void;
48
+ deliverOutbound: (port: Outbound<any, any>, encodedValue: unknown) => void;
49
+ bind: (deliverInbound: (port: Inbound<any, any>, value: unknown) => void) => void;
50
+ unbind: () => void;
51
+ dispose: () => void;
52
+ }>;
53
+ export declare const makeHostConnector: () => HostConnector;
54
+ export type PortChannelsBundle = Readonly<{
55
+ channels: __PortChannels;
56
+ deliverInbound: (port: Inbound<any, any>, value: unknown) => void;
57
+ }>;
58
+ export declare const makePortChannels: (ports: Ports, maybeConnector: Option.Option<HostConnector>) => PortChannelsBundle;
59
+ export declare const validatePorts: (ports: Ports) => void;
60
+ export declare const buildPortHandles: <P extends Ports | undefined>(ports: P, connector: HostConnector) => PortHandles<P>;
61
+ //# sourceMappingURL=hostConnector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hostConnector.d.ts","sourceRoot":"","sources":["../../src/runtime/hostConnector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,141 @@
1
+ import { Cause, Exit, Function, Option, Predicate, Schema } from 'effect';
2
+ import { __makeInboundChannel, } from '../port/index.js';
3
+ export const makeHostConnector = () => {
4
+ let isDisposed = false;
5
+ let maybeDeliverInbound = Option.none();
6
+ const pendingInboundSends = [];
7
+ const listenersByPort = new Map();
8
+ const sendInbound = (portName, port, value) => {
9
+ if (isDisposed) {
10
+ return Exit.void;
11
+ }
12
+ const decodeExit = Schema.decodeUnknownExit(port.schema)(value);
13
+ Exit.match(decodeExit, {
14
+ onFailure: cause => {
15
+ console.error(`[foldkit] Inbound port "${portName}" rejected a value:`, Cause.squash(cause));
16
+ },
17
+ onSuccess: decodedValue => {
18
+ Option.match(maybeDeliverInbound, {
19
+ onNone: () => {
20
+ pendingInboundSends.push({ port, value: decodedValue });
21
+ },
22
+ onSome: deliverInbound => deliverInbound(port, decodedValue),
23
+ });
24
+ },
25
+ });
26
+ return Exit.asVoid(decodeExit);
27
+ };
28
+ const addListener = (port, listener) => {
29
+ if (isDisposed) {
30
+ return Function.constVoid;
31
+ }
32
+ const listeners = listenersByPort.get(port) ?? new Set();
33
+ listenersByPort.set(port, listeners);
34
+ listeners.add(listener);
35
+ return () => {
36
+ listeners.delete(listener);
37
+ };
38
+ };
39
+ // NOTE: delivery is deferred to a microtask so a host listener never runs
40
+ // inside the runtime's Command fiber (a listener that synchronously calls
41
+ // send or dispose must not re-enter the runtime), and so a host that
42
+ // subscribes synchronously right after embed() returns still receives
43
+ // emissions from init Commands.
44
+ const deliverOutbound = (port, encodedValue) => {
45
+ if (isDisposed) {
46
+ return;
47
+ }
48
+ queueMicrotask(() => {
49
+ if (isDisposed) {
50
+ return;
51
+ }
52
+ const listeners = listenersByPort.get(port) ?? new Set();
53
+ listeners.forEach(listener => {
54
+ try {
55
+ listener(encodedValue);
56
+ }
57
+ catch (listenerError) {
58
+ console.error('[foldkit] An outbound port listener threw:', listenerError);
59
+ }
60
+ });
61
+ });
62
+ };
63
+ const bind = (deliverInbound) => {
64
+ maybeDeliverInbound = Option.some(deliverInbound);
65
+ const flushedSends = pendingInboundSends.splice(0);
66
+ flushedSends.forEach(({ port, value }) => deliverInbound(port, value));
67
+ };
68
+ const unbind = () => {
69
+ maybeDeliverInbound = Option.none();
70
+ };
71
+ const dispose = () => {
72
+ isDisposed = true;
73
+ pendingInboundSends.length = 0;
74
+ listenersByPort.forEach(listeners => listeners.clear());
75
+ listenersByPort.clear();
76
+ };
77
+ return { sendInbound, addListener, deliverOutbound, bind, unbind, dispose };
78
+ };
79
+ export const makePortChannels = (ports, maybeConnector) => {
80
+ const inboundChannelsByPort = new Map();
81
+ Object.values(ports.inbound ?? {}).forEach(port => {
82
+ inboundChannelsByPort.set(port, __makeInboundChannel());
83
+ });
84
+ const outboundPorts = new Set(Object.values(ports.outbound ?? {}));
85
+ const channels = {
86
+ isConfigured: true,
87
+ lookupInbound: port => Option.fromNullishOr(inboundChannelsByPort.get(port)),
88
+ lookupOutbound: port => outboundPorts.has(port)
89
+ ? Option.some(encodedValue => Option.match(maybeConnector, {
90
+ onNone: Function.constVoid,
91
+ onSome: connector => connector.deliverOutbound(port, encodedValue),
92
+ }))
93
+ : Option.none(),
94
+ };
95
+ const deliverInbound = (port, value) => {
96
+ Option.match(Option.fromNullishOr(inboundChannelsByPort.get(port)), {
97
+ onNone: Function.constVoid,
98
+ onSome: channel => channel.deliver(value),
99
+ });
100
+ };
101
+ return { channels, deliverInbound };
102
+ };
103
+ export const validatePorts = (ports) => {
104
+ const inboundEntries = Object.entries(ports.inbound ?? {});
105
+ const outboundEntries = Object.entries(ports.outbound ?? {});
106
+ const inboundNames = new Set(inboundEntries.map(([name]) => name));
107
+ outboundEntries.forEach(([name]) => {
108
+ if (inboundNames.has(name)) {
109
+ throw new Error(`[foldkit] Port name "${name}" appears in both inbound and outbound. ` +
110
+ 'Port names share one namespace on the EmbedHandle, so each name ' +
111
+ 'must be unique across both records.');
112
+ }
113
+ });
114
+ const seenPorts = new Set();
115
+ const allEntries = [...inboundEntries, ...outboundEntries];
116
+ allEntries.forEach(([name, port]) => {
117
+ if (seenPorts.has(port)) {
118
+ throw new Error(`[foldkit] The Port registered as "${name}" is also registered under ` +
119
+ 'another name. Each entry in the ports record needs its own ' +
120
+ 'Port.inbound or Port.outbound value.');
121
+ }
122
+ seenPorts.add(port);
123
+ });
124
+ };
125
+ export const buildPortHandles = (ports, connector) => {
126
+ const handles = {};
127
+ if (Predicate.isNotUndefined(ports)) {
128
+ Object.entries(ports.inbound ?? {}).forEach(([portName, port]) => {
129
+ handles[portName] = {
130
+ send: (value) => connector.sendInbound(portName, port, value),
131
+ };
132
+ });
133
+ Object.entries(ports.outbound ?? {}).forEach(([portName, port]) => {
134
+ handles[portName] = {
135
+ subscribe: (listener) => connector.addListener(port, listener),
136
+ };
137
+ });
138
+ }
139
+ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
140
+ return handles;
141
+ };
@@ -0,0 +1,44 @@
1
+ import { Effect, Option, Schema } from 'effect';
2
+ /** Whether a boot adopts a server-rendered root (`Hydrate`) or builds the DOM
3
+ * fresh in the container (`Fresh`). */
4
+ export type BootMode = 'Fresh' | 'Hydrate';
5
+ export type HydrationConfig = Readonly<{
6
+ root: HTMLElement;
7
+ runtimeId: string;
8
+ flagsScripts: ReadonlyArray<HTMLScriptElement>;
9
+ isFlagsRequired: boolean;
10
+ }>;
11
+ export declare const buildSkew: (root: HTMLElement, buildId: unknown, runtimeId: string) => Error | undefined;
12
+ export declare const containRefusedPage: (ownerDocument: Document) => void;
13
+ export declare const hasServerRenderedMarkup: (ownerDocument: Document) => boolean;
14
+ export declare const findDocumentHydration: (container: HTMLElement | null, isFlagsRequired: boolean) => HydrationConfig | undefined;
15
+ /** What the boot takes from the handoff: the server-rendered root the first
16
+ * render adopts, when this boot adopts one, and the Effect that produces the
17
+ * Flags `init` runs with, from the page's payload or from the app's own
18
+ * Flags Effect. */
19
+ export type ResolvedHydrationHandoff<Flags> = Readonly<{
20
+ maybeHydrationRoot: Option.Option<HTMLElement>;
21
+ resolveFlags: Effect.Effect<Flags>;
22
+ }>;
23
+ /**
24
+ * Decides whether this boot adopts the server-rendered root and where its
25
+ * Flags come from. A hydrating boot with no stamped root, a root served by
26
+ * another deployment, or a missing, duplicated, or undecodable Flags
27
+ * payload contains the served page and stops startup. The build id is
28
+ * compared before the payload text is read and before `init` runs, so
29
+ * nothing acts on another deployment's data. An HMR-restored Model skips
30
+ * adoption and gets a fresh patch against the stamped root.
31
+ */
32
+ export declare const resolveHydrationHandoff: <Flags, Resources>({ bootMode, hydration, bootFlags, configuredFlags, isFlagsRequired, FlagsCodec, hmrModel, container, buildId, provideResources, }: Readonly<{
33
+ bootMode: BootMode;
34
+ hydration: HydrationConfig | undefined;
35
+ bootFlags: Effect.Effect<Flags, never, Resources> | undefined;
36
+ configuredFlags: Option.Option<Effect.Effect<Flags, never, Resources>>;
37
+ isFlagsRequired: boolean;
38
+ FlagsCodec: Schema.Codec<Flags, any, unknown, unknown>;
39
+ hmrModel: unknown;
40
+ container: HTMLElement;
41
+ buildId: string | undefined;
42
+ provideResources: <A>(effect: Effect.Effect<A, never, Resources>) => Effect.Effect<A>;
43
+ }>) => Effect.Effect<ResolvedHydrationHandoff<Flags>>;
44
+ //# sourceMappingURL=hydrationHandoff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hydrationHandoff.d.ts","sourceRoot":"","sources":["../../src/runtime/hydrationHandoff.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAE,MAAM,EAAa,MAAM,EAAQ,MAAM,QAAQ,CAAA;AAQvE;wCACwC;AACxC,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAA;AAK1C,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACrC,IAAI,EAAE,WAAW,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAA;IAC9C,eAAe,EAAE,OAAO,CAAA;CACzB,CAAC,CAAA;AAsGF,eAAO,MAAM,SAAS,SACd,WAAW,WACR,OAAO,aACL,MAAM,KAChB,KAAK,GAAG,SAwBV,CAAA;AAmJD,eAAO,MAAM,kBAAkB,kBAAmB,QAAQ,KAAG,IAO5D,CAAA;AAOD,eAAO,MAAM,uBAAuB,kBAAmB,QAAQ,KAAG,OAItD,CAAA;AAEZ,eAAO,MAAM,qBAAqB,cACrB,WAAW,GAAG,IAAI,mBACZ,OAAO,KACvB,eAAe,GAAG,SAyDpB,CAAA;AAED;;;oBAGoB;AACpB,MAAM,MAAM,wBAAwB,CAAC,KAAK,IAAI,QAAQ,CAAC;IACrD,kBAAkB,EAAE,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;IAC9C,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;CACnC,CAAC,CAAA;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,GAAI,KAAK,EAAE,SAAS,qIAWrD,QAAQ,CAAC;IACV,QAAQ,EAAE,QAAQ,CAAA;IAClB,SAAS,EAAE,eAAe,GAAG,SAAS,CAAA;IACtC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,GAAG,SAAS,CAAA;IAC7D,eAAe,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAA;IACtE,eAAe,EAAE,OAAO,CAAA;IACxB,UAAU,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IACtD,QAAQ,EAAE,OAAO,CAAA;IACjB,SAAS,EAAE,WAAW,CAAA;IACtB,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,gBAAgB,EAAE,CAAC,CAAC,EAClB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,KACvC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;CACtB,CAAC,KAAG,MAAM,CAAC,MAAM,CAAC,wBAAwB,CAAC,KAAK,CAAC,CAuK9C,CAAA"}