@atolljs/islands 0.1.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.
@@ -0,0 +1,589 @@
1
+ import { parseDocument, ElementType } from 'htmlparser2';
2
+ import {
3
+ allocId,
4
+ bumpOpsVersion,
5
+ getActiveInstance,
6
+ getLastActiveInstance,
7
+ getInstanceSize,
8
+ instances,
9
+ markInstanceActive,
10
+ pushOp,
11
+ registerHandler,
12
+ unregisterHandler,
13
+ } from '../instance';
14
+ import type { ElementInstance, HostInstance, TextInstance } from '../instance';
15
+ import type { EventPayload, Op } from '../../ops';
16
+
17
+ import {
18
+ ProxyNode,
19
+ ProxyText,
20
+ ProxyComment,
21
+ ProxyFragment,
22
+ allocPhantomId,
23
+ rejectForeignChild,
24
+ listenerOptsForWire,
25
+ normalizeListenerOptions,
26
+ type ProxyEventHandler,
27
+ } from './node';
28
+ import { ProxyElement } from './element';
29
+ import type { ProxyClassList } from './css';
30
+ import { parseChildren, serializeNode } from './html';
31
+ import { parseSelector, matchCompound, matchesChain } from './selectors';
32
+ import type { WindowShim, WindowFacadeBundle } from './window';
33
+
34
+ /* ── The document ──────────────────────────────────────────────────────── */
35
+
36
+ export interface ProxyDocument {
37
+ /**
38
+ * The island's root container — driver-side id 0. `body` and
39
+ * `documentElement` are aliases of it; appending to either lands elements
40
+ * at the island root. Its own attr/style/listen mutations target the real
41
+ * container element.
42
+ */
43
+ readonly body: ProxyElement;
44
+ readonly documentElement: ProxyElement;
45
+ createElement(tag: string): ProxyElement;
46
+ /**
47
+ * Namespaced creation for SVG/MathML trees — the `create` op carries the
48
+ * namespace so the driver uses `createElementNS`. Needed by libraries that
49
+ * build SVG imperatively (d3-style code).
50
+ */
51
+ createElementNS(ns: string, tag: string): ProxyElement;
52
+ createTextNode(text: string): ProxyText;
53
+ /**
54
+ * A comment node — nodeType 8 backed by a REAL empty text node
55
+ * driver-side (the wire has no comment op) so it anchors `before:`
56
+ * positions like a real comment. `data` writes stay shadow-only.
57
+ */
58
+ createComment(data?: string): ProxyComment;
59
+ /**
60
+ * A phantom container for batching — appending the fragment splices its
61
+ * children into the target like the DOM does (each child emits its own
62
+ * append op). Fragments never exist driver-side.
63
+ */
64
+ createDocumentFragment(): ProxyFragment;
65
+ /** DOM `importNode` semantics — a deep/shallow clone through this
66
+ * document (proxy nodes are single-instance, so cloning IS importing). */
67
+ importNode(node: ProxyNode, deep?: boolean): ProxyNode;
68
+ /** 'about:blank' — no real document URL exists worker-side. */
69
+ readonly baseURI: string;
70
+ /**
71
+ * Document-level listeners: emitted as `listen` ops on id 0 — the driver
72
+ * attaches them to the island's real container element, so delegated
73
+ * handlers see every event that bubbles inside the island. This is what
74
+ * library-style `document.addEventListener('click', delegate)` needs.
75
+ * `options` carries `{once, passive, capture}` onto the wire.
76
+ */
77
+ addEventListener(
78
+ type: string,
79
+ fn: ProxyEventHandler,
80
+ options?: boolean | AddEventListenerOptions,
81
+ ): void;
82
+ removeEventListener(
83
+ type: string,
84
+ fn: ProxyEventHandler,
85
+ options?: boolean | EventListenerOptions,
86
+ ): void;
87
+ /** id-map lookup over the CONNECTED shadow tree (disconnected nodes with
88
+ * an id set are remembered but not returned, matching real-DOM scoping). */
89
+ getElementById(id: string): ProxyElement | null;
90
+ /** Simple matcher: tag/.class/#id/[attr]/[attr="v"] + descendant
91
+ * combinators. Anything else throws a clear unsupported-selector error. */
92
+ querySelector(selector: string): ProxyElement | null;
93
+ querySelectorAll(selector: string): ProxyElement[];
94
+ /** Wrap an existing host-instance record (e.g. a React-rendered element
95
+ * reached via the shared `instances` map) so imperative code can navigate
96
+ * or mutate around it — the bridge for nesting proxy trees inside
97
+ * React-rendered parents and vice versa. */
98
+ adopt(instance: HostInstance): ProxyNode;
99
+ /**
100
+ * Mark `el` as reporting the pushed container size — the driver's only
101
+ * measured box is the island container, so geometry reads
102
+ * (`clientWidth/Height`, `offsetWidth/Height`, `getBoundingClientRect`)
103
+ * on marked elements return the last `setSize` push exactly like
104
+ * `doc.body`/`documentElement`. Only honest for elements that actually
105
+ * fill the island's box (a map viewport, a canvas host).
106
+ */
107
+ markContainer(el: ProxyElement): void;
108
+ /**
109
+ * Register a handler fired every time the driver pushes a new container
110
+ * size — the app's resize signal. Handlers run inside the instance's task
111
+ * scope, so their proxy-DOM mutations emit ops and `emit` routes.
112
+ */
113
+ onResize(cb: (w: number, h: number) => void): void;
114
+ /**
115
+ * The window facade `installDomShim` installed this document under —
116
+ * what `document.defaultView` reads as (`window.getComputedStyle` etc).
117
+ * undefined until the shim installs.
118
+ */
119
+ readonly defaultView?: WindowShim;
120
+ /** No measurement channel — returns an all-empty declaration (warns once). */
121
+ getComputedStyle(el: ProxyElement): CSSStyleDeclaration;
122
+ /** Unregister every handler this document's listeners hold. Called by the
123
+ * worker on rebuild so stale handler ids can't be dispatched into dead
124
+ * nodes; mutating a disposed document throws. */
125
+ dispose(): void;
126
+ }
127
+
128
+ export class InternalDocument implements ProxyDocument {
129
+ readonly instance: string;
130
+ readonly _root: ProxyElement;
131
+ readonly _wrappers = new Map<number, ProxyNode>();
132
+ readonly _ids = new Map<string, ProxyElement>();
133
+ /** handler ids this document registered — disposed on rebuild. */
134
+ readonly _handlerIds = new Set<number>();
135
+ /**
136
+ * instance ids geometry reads answer for — id 0 (the root) plus every
137
+ * markContainer()ed element. The pushed box is the ISLAND CONTAINER's,
138
+ * so a marked element claims "my box is the container's box".
139
+ */
140
+ readonly _containerIds = new Set<number>([0]);
141
+ /** Handlers fired when a setSize task pushes a new container size. */
142
+ private readonly _resizeHandlers = new Set<(w: number, h: number) => void>();
143
+ /** The window facade bundle — built lazily by windowFacadeFor, shared by
144
+ * installDomShim and the instance dispatcher. */
145
+ _windowBundle?: WindowFacadeBundle;
146
+ /** what `document.defaultView` reads as (`window.getComputedStyle` etc). */
147
+ get defaultView(): WindowShim | undefined {
148
+ return this._windowBundle?.facade;
149
+ }
150
+ /** Document-level listeners — separate table from the root element's, so
151
+ * doc.addEventListener and body.addEventListener don't wrongly dedupe
152
+ * against each other. Both emit `listen` on id 0. */
153
+ private readonly _docListeners: Array<{
154
+ type: string;
155
+ fn: ProxyEventHandler;
156
+ hid: number;
157
+ capture: boolean;
158
+ }> = [];
159
+ private readonly _warned = new Set<string>();
160
+ _disposed = false;
161
+ /** Set by installDomShim — restore globals when this document dies. */
162
+ _uninstallShim: (() => void) | null = null;
163
+
164
+ constructor(instance: string) {
165
+ this.instance = instance;
166
+ // id 0 is the driver-side island container sentinel — deliberately NOT
167
+ // registered in `instances` (the driver's nodes map already maps 0 → el).
168
+ this._root = new ProxyElement(this, {
169
+ kind: 'element',
170
+ id: 0,
171
+ type: '#root',
172
+ instance,
173
+ props: {},
174
+ listenerSlots: {},
175
+ });
176
+ this._wrappers.set(0, this._root);
177
+ }
178
+
179
+ get body(): ProxyElement {
180
+ return this._root;
181
+ }
182
+ get documentElement(): ProxyElement {
183
+ return this._root;
184
+ }
185
+
186
+ /** No hit-testing exists worker-side — honest null (a11y-layer probing). */
187
+ elementFromPoint(_x: number, _y: number): null {
188
+ this._assertAlive();
189
+ this._warn('elementFromPoint');
190
+ return null;
191
+ }
192
+
193
+ _assertAlive(): void {
194
+ if (this._disposed) {
195
+ throw new Error(
196
+ 'proxyDom: this document was disposed by an updateProps/remount rebuild — its nodes are detached',
197
+ );
198
+ }
199
+ }
200
+
201
+ _warn(feature: string): void {
202
+ if (this._warned.has(feature)) return;
203
+ this._warned.add(feature);
204
+ console.warn(
205
+ `[proxyDom] '${feature}' has no measurement channel — the only geometry the worker sees is the pushed container size (setSize); reads on unmarked elements return 0/empty.`,
206
+ );
207
+ }
208
+
209
+ markContainer(el: ProxyElement): void {
210
+ this._assertAlive();
211
+ this._containerIds.add(el.instance.id);
212
+ }
213
+
214
+ /**
215
+ * The size a geometry read on `el` may report: the pushed container box
216
+ * when `el` is the root or was markContainer()ed, undefined otherwise
217
+ * (also undefined when nothing has been pushed yet — reads return 0).
218
+ */
219
+ _sizeFor(el: ProxyElement): { w: number; h: number } | undefined {
220
+ if (!this._containerIds.has(el.instance.id)) return undefined;
221
+ return getInstanceSize(this.instance);
222
+ }
223
+
224
+ onResize(cb: (w: number, h: number) => void): void {
225
+ this._assertAlive();
226
+ this._resizeHandlers.add(cb);
227
+ }
228
+
229
+ /**
230
+ * Fire the doc's resize handlers — the worker's `setSize` task calls this
231
+ * inside the instance's scope, so handler mutations emit ops on the instance
232
+ * queue and come back in the task's return batch.
233
+ */
234
+ _notifySize(w: number, h: number): void {
235
+ for (const cb of [...this._resizeHandlers]) cb(w, h);
236
+ }
237
+
238
+ /** Keep the getElementById map pointing at each element's CURRENT id. */
239
+ _trackId(el: ProxyElement): void {
240
+ for (const [k, v] of this._ids) if (v === el) this._ids.delete(k);
241
+ const id = el._attrs.get('id');
242
+ if (id !== undefined && id !== '') this._ids.set(id, el);
243
+ }
244
+
245
+ /** The synthetic text child an element's `textContent` setter leaves in
246
+ * the shadow tree — no driver-side node, so its writes retarget parent. */
247
+ _phantomText(parent: ProxyNode, text: string): ProxyText {
248
+ const t = new ProxyText(this, {
249
+ kind: 'text',
250
+ id: allocPhantomId(),
251
+ text,
252
+ instance: parent.instance.instance,
253
+ });
254
+ t._parent = parent;
255
+ return t;
256
+ }
257
+
258
+ /**
259
+ * Enrich a dispatched payload before handing it to a proxy listener:
260
+ * - `target` = the proxy node for targetId, looked up in the shared
261
+ * instance space and wrapped in THIS document's wrapper map (so
262
+ * `e.target.closest`/`.dataset`/`.contains` behave like the real DOM
263
+ * for proxy-created nodes).
264
+ * - `currentTarget` = `self` — the node/document/facade the listener was
265
+ * attached to (the handler is also invoked with `this` = self). For
266
+ * document/window listeners this is the document/facade OBJECT, not
267
+ * the id-0 root element dispatch() would otherwise stamp.
268
+ * - `composedPath()` = synthesized from the enriched target's parent
269
+ * chain, with `self` appended when the chain doesn't already reach it
270
+ * (delegated handlers iterate the path until they hit their own
271
+ * element — Svelte's handle_event_propagation terminates there).
272
+ * - `preventDefault`/`stopPropagation`/`stopImmediatePropagation` =
273
+ * synthesized NO-OPS. They cannot cancel anything — the real event
274
+ * already dispatched on the main thread before this payload crossed
275
+ * postMessage — but library code written against real DOM events calls
276
+ * them unconditionally, so the wrapper supplies them.
277
+ */
278
+ _enrichEvent(payload: EventPayload, self?: unknown): EventPayload {
279
+ const out: EventPayload & {
280
+ currentTarget?: unknown;
281
+ composedPath?: () => unknown[];
282
+ } = {
283
+ ...payload,
284
+ preventDefault: () => {},
285
+ stopPropagation: () => {},
286
+ stopImmediatePropagation: () => {},
287
+ };
288
+ if (self !== undefined) out.currentTarget = self;
289
+ const id = payload.targetId;
290
+ if (typeof id === 'number') {
291
+ const instance = instances.get(id);
292
+ if (instance !== undefined) {
293
+ const node = this._wrap(instance);
294
+ // Reflect the dispatched form state into the SHADOW attrs —
295
+ // `e.target.value`/`e.target.checked` are the DOM idioms input
296
+ // handlers read, but the proxy's own fields are whatever the last
297
+ // attr op set (stale). Writing the public setters would emit an
298
+ // attr op echoing the user's own keystroke back to the DOM, so the
299
+ // shadow map is updated directly.
300
+ if (node instanceof ProxyElement) {
301
+ if (payload.value !== undefined) node._attrs.set('value', String(payload.value));
302
+ if (payload.checked !== undefined) {
303
+ if (payload.checked) node._attrs.set('checked', '');
304
+ else node._attrs.delete('checked');
305
+ }
306
+ }
307
+ out.target = node;
308
+ }
309
+ }
310
+ if (out.composedPath === undefined) {
311
+ const path: unknown[] = [];
312
+ let n = out.target as ProxyNode | undefined;
313
+ while (n instanceof ProxyNode) {
314
+ path.push(n);
315
+ n = n._parent ?? undefined;
316
+ }
317
+ if (self !== undefined && !path.includes(self)) path.push(self);
318
+ out.composedPath = () => path;
319
+ }
320
+ return out;
321
+ }
322
+
323
+ createElement(tag: string): ProxyElement {
324
+ return this._newElement(tag.toLowerCase(), undefined);
325
+ }
326
+
327
+ createElementNS(ns: string, tag: string): ProxyElement {
328
+ return this._newElement(tag, ns);
329
+ }
330
+
331
+ private _newElement(type: string, ns: string | undefined): ProxyElement {
332
+ this._assertAlive();
333
+ const instance: ElementInstance = {
334
+ kind: 'element',
335
+ id: allocId(),
336
+ type,
337
+ ns,
338
+ instance: this.instance,
339
+ props: {},
340
+ listenerSlots: {},
341
+ };
342
+ instances.set(instance.id, instance);
343
+ pushOp(this.instance, { t: 'create', id: instance.id, type: instance.type, props: {}, ns: instance.ns });
344
+ if (getActiveInstance() !== this.instance) bumpOpsVersion();
345
+ return this._wrap(instance) as ProxyElement;
346
+ }
347
+
348
+ createTextNode(text: string): ProxyText {
349
+ this._assertAlive();
350
+ const instance: TextInstance = {
351
+ kind: 'text',
352
+ id: allocId(),
353
+ text: String(text),
354
+ instance: this.instance,
355
+ };
356
+ instances.set(instance.id, instance);
357
+ pushOp(this.instance, { t: 'text', id: instance.id, text: instance.text });
358
+ if (getActiveInstance() !== this.instance) bumpOpsVersion();
359
+ return this._wrap(instance) as ProxyText;
360
+ }
361
+
362
+ adopt(instance: HostInstance): ProxyNode {
363
+ this._assertAlive();
364
+ return this._wrap(instance);
365
+ }
366
+
367
+ createDocumentFragment(): ProxyFragment {
368
+ this._assertAlive();
369
+ return new ProxyFragment(this, {
370
+ kind: 'element',
371
+ id: allocPhantomId(),
372
+ type: '#fragment',
373
+ instance: this.instance,
374
+ props: {},
375
+ listenerSlots: {},
376
+ });
377
+ }
378
+
379
+ /**
380
+ * A comment — nodeType 8 over a real EMPTY text instance. The wire has no
381
+ * comment op: the `text` op with '' gives it a driver-side node that can
382
+ * anchor `before:` positions, while `data` stays shadow-only (emitting it
383
+ * would render comment text on the page).
384
+ */
385
+ createComment(data = ''): ProxyComment {
386
+ const backing = this.createTextNode('');
387
+ const comment = new ProxyComment(this, backing.instance);
388
+ comment.instance.text = String(data);
389
+ // createTextNode cached a ProxyText wrapper for this id in _wrappers —
390
+ // replace it so later adopt()/_wrap() calls hand back the comment view.
391
+ this._wrappers.set(backing.instance.id, comment);
392
+ return comment;
393
+ }
394
+
395
+ /**
396
+ * DOM `importNode` — a clone through this document. Foreign (non-proxy)
397
+ * nodes have no shadow shape to copy; they're rejected like any other
398
+ * foreign child.
399
+ */
400
+ importNode(node: ProxyNode, deep = false): ProxyNode {
401
+ this._assertAlive();
402
+ if (!(node instanceof ProxyNode)) rejectForeignChild(node);
403
+ return node.cloneNode(deep);
404
+ }
405
+
406
+ /**
407
+ * 'about:blank' — no real document URL exists worker-side. A plain field
408
+ * (not a getter) so adapters may assign it like a normal document prop.
409
+ */
410
+ readonly baseURI: string = 'about:blank';
411
+
412
+ /** Wrap (or return the existing wrapper for) a shared instance record. */
413
+ _wrap(instance: HostInstance): ProxyNode {
414
+ let node = this._wrappers.get(instance.id);
415
+ if (node === undefined) {
416
+ node =
417
+ instance.kind === 'text'
418
+ ? new ProxyText(this, instance)
419
+ : new ProxyElement(this, instance as ElementInstance);
420
+ this._wrappers.set(instance.id, node);
421
+ }
422
+ return node;
423
+ }
424
+
425
+ /**
426
+ * Document-level listener — same currency as element addEventListener but
427
+ * targeting id 0 (the island's container element). The driver's listen op
428
+ * attaches a real DOM listener on the container, so events bubbling up
429
+ * from any island child dispatch back here. Used by library code doing
430
+ * delegated `document.addEventListener(...)`.
431
+ */
432
+ addEventListener(
433
+ type: string,
434
+ fn: ProxyEventHandler,
435
+ options?: boolean | AddEventListenerOptions,
436
+ ): void {
437
+ this._assertAlive();
438
+ const opts = normalizeListenerOptions(options);
439
+ // Same (type, listener, capture) dedupe as the DOM.
440
+ if (
441
+ this._docListeners.some(
442
+ (l) => l.type === type && l.fn === fn && l.capture === opts.capture,
443
+ )
444
+ ) {
445
+ return;
446
+ }
447
+ const hid = registerHandler(
448
+ (p) => {
449
+ try {
450
+ fn.call(this, this._enrichEvent(p as EventPayload, this));
451
+ } finally {
452
+ if (opts.once) this.removeEventListener(type, fn, { capture: opts.capture });
453
+ }
454
+ },
455
+ this.instance,
456
+ 0, // document-level listener → target id 0 is the island root
457
+ );
458
+ this._docListeners.push({ type, fn, hid, capture: opts.capture });
459
+ this._handlerIds.add(hid);
460
+ pushOp(this.instance, { t: 'listen', id: 0, type, handler: hid, opts: listenerOptsForWire(options) });
461
+ if (getActiveInstance() !== this.instance) bumpOpsVersion();
462
+ }
463
+
464
+ removeEventListener(
465
+ type: string,
466
+ fn: ProxyEventHandler,
467
+ options?: boolean | EventListenerOptions,
468
+ ): void {
469
+ this._assertAlive();
470
+ const capture = normalizeListenerOptions(options).capture;
471
+ const index = this._docListeners.findIndex(
472
+ (l) => l.type === type && l.fn === fn && l.capture === capture,
473
+ );
474
+ if (index === -1) return;
475
+ const [entry] = this._docListeners.splice(index, 1);
476
+ unregisterHandler(entry.hid);
477
+ this._handlerIds.delete(entry.hid);
478
+ pushOp(this.instance, {
479
+ t: 'unlisten',
480
+ id: 0,
481
+ type,
482
+ handler: entry.hid,
483
+ opts: entry.capture ? { capture: true } : undefined,
484
+ });
485
+ if (getActiveInstance() !== this.instance) bumpOpsVersion();
486
+ }
487
+
488
+ getElementById(id: string): ProxyElement | null {
489
+ const el = this._ids.get(id);
490
+ return el !== undefined && el.isConnected ? el : null;
491
+ }
492
+
493
+ querySelector(selector: string): ProxyElement | null {
494
+ return this.querySelectorAll(selector)[0] ?? null;
495
+ }
496
+
497
+ querySelectorAll(selector: string): ProxyElement[] {
498
+ let chain = parseSelector(selector);
499
+ // body/documentElement/html all alias the id-0 root. A leading bare
500
+ // `body`/`html` compound is satisfied BY the root — Vue's Teleport
501
+ // resolves `to="body"` through this path — so pin it there and match
502
+ // the rest of the chain against descendants.
503
+ const first = chain[0];
504
+ if (
505
+ (first.tag === 'body' || first.tag === 'html') &&
506
+ first.id === undefined &&
507
+ first.classes.length === 0 &&
508
+ first.attrs.length === 0
509
+ ) {
510
+ chain = chain.slice(1);
511
+ }
512
+ if (chain.length === 0) return [this._root];
513
+ const out: ProxyElement[] = [];
514
+ const walk = (node: ProxyNode): void => {
515
+ for (const child of node._children) {
516
+ if (child instanceof ProxyElement && matchesChain(child, chain)) out.push(child);
517
+ walk(child);
518
+ }
519
+ };
520
+ walk(this._root);
521
+ return out;
522
+ }
523
+
524
+ getComputedStyle(_el: ProxyElement): CSSStyleDeclaration {
525
+ this._warn('getComputedStyle');
526
+ // Every property reads ''; getPropertyValue('x') returns '' too.
527
+ return new Proxy({} as Record<string, unknown>, {
528
+ get: (_t, prop) => (prop === 'getPropertyValue' ? () => '' : ''),
529
+ }) as unknown as CSSStyleDeclaration;
530
+ }
531
+
532
+ dispose(): void {
533
+ // Tear down the shim FIRST if this doc was installed — the rebuild path
534
+ // (updateProps) swaps in a fresh doc and re-installs.
535
+ this._uninstallShim?.();
536
+ this._uninstallShim = null;
537
+ this._disposed = true;
538
+ if (instanceDocs.get(this.instance) === this) instanceDocs.delete(this.instance);
539
+ this._resizeHandlers.clear();
540
+ for (const hid of this._handlerIds) unregisterHandler(hid);
541
+ this._handlerIds.clear();
542
+ }
543
+ }
544
+
545
+ /**
546
+ * A fresh proxy document scoped to one instance — created per imperative
547
+ * mount and per rebuild (updateProps). Mutations emit ops onto the instance's
548
+ * queue immediately; reads are served from the shadow tree.
549
+ */
550
+ /**
551
+ * instance key → the proxy document for that instance. `createProxyDocument` is
552
+ * the only creator — imperative apps get one at mount, React mounts lazily
553
+ * via `getPublicInstance` — so global `document`/`window`/`Element` can
554
+ * dispatch to the right instance's document (see installInstanceDispatcher).
555
+ */
556
+ const instanceDocs = new Map<string, InternalDocument>();
557
+
558
+ export const createProxyDocument = (instance: string): ProxyDocument => {
559
+ const doc = new InternalDocument(instance);
560
+ instanceDocs.set(instance, doc);
561
+ return doc;
562
+ };
563
+
564
+ /**
565
+ * The instance's proxy document, creating it lazily — the getPublicInstance
566
+ * path (React mounts only need a document when a library holds a ref or a
567
+ * portal target).
568
+ */
569
+ export const docForInstance = (instance: string): InternalDocument =>
570
+ instanceDocs.get(instance) ?? (createProxyDocument(instance) as InternalDocument);
571
+
572
+ /**
573
+ * The document `globalThis.document` should mean right now: the active
574
+ * instance's when a task holds one; the single registered document when only
575
+ * one instance exists (every real island worker — timers and promise
576
+ * continuations run outside instance tasks but unambiguously belong to it);
577
+ * the most recently active instance's otherwise (multi-instance in-process tests
578
+ * still route deferred library callbacks sensibly).
579
+ */
580
+ export const activeInstanceDoc = (): InternalDocument | undefined => instanceDocs.get(getActiveInstance());
581
+
582
+ /** The ambient document: the sole registered doc, or the most recently
583
+ * active instance's — for out-of-task readers (timers, continuations). */
584
+ export const ambientDoc = (): InternalDocument | undefined =>
585
+ instanceDocs.size === 1
586
+ ? instanceDocs.values().next().value
587
+ : instanceDocs.get(getLastActiveInstance());
588
+
589
+