solid-tag-runtime 0.0.6 → 0.0.8

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/src/events.js ADDED
@@ -0,0 +1,63 @@
1
+ export function createEventDispatcher(options = {}) {
2
+ const subscriptions = new Set();
3
+ const onListenerError = options.onListenerError;
4
+
5
+ return {
6
+ subscribe,
7
+ emit,
8
+ clear,
9
+ get size() {
10
+ return subscriptions.size;
11
+ },
12
+ };
13
+
14
+ function subscribe(typesOrListener, maybeListener) {
15
+ let types;
16
+ let listener;
17
+
18
+ if (typeof typesOrListener === "function") {
19
+ listener = typesOrListener;
20
+ types = undefined;
21
+ } else {
22
+ listener = maybeListener;
23
+ if (typeof typesOrListener === "string") {
24
+ types = new Set([typesOrListener]);
25
+ } else if (Array.isArray(typesOrListener)) {
26
+ if (typesOrListener.length === 0 || typesOrListener.some(type => typeof type !== "string" || !type)) {
27
+ throw new TypeError("subscribe(types, listener) expects a non-empty event type or event type array.");
28
+ }
29
+ types = new Set(typesOrListener);
30
+ } else {
31
+ throw new TypeError("subscribe() expects a listener, event type + listener, or event type array + listener.");
32
+ }
33
+ }
34
+
35
+ if (typeof listener !== "function") {
36
+ throw new TypeError("subscribe() expects a listener function.");
37
+ }
38
+
39
+ const subscription = { types, listener };
40
+ subscriptions.add(subscription);
41
+
42
+ return () => subscriptions.delete(subscription);
43
+ }
44
+
45
+ function emit(event) {
46
+ for (const subscription of [...subscriptions]) {
47
+ if (subscription.types && !subscription.types.has(event.type)) continue;
48
+ try {
49
+ subscription.listener(event);
50
+ } catch (error) {
51
+ try {
52
+ onListenerError?.(error, event);
53
+ } catch {
54
+ // Listener error reporting must never break the emitter.
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ function clear() {
61
+ subscriptions.clear();
62
+ }
63
+ }
package/src/html.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { SolidTagRuntimeError } from "./errors.js";
2
+ import { createEventDispatcher } from "./events.js";
2
3
 
3
4
  const DEFAULT_SELECTOR = [
4
5
  'script[type="solid-jsx"]',
@@ -47,6 +48,11 @@ export function createHTMLRuntime(runtime, options = {}) {
47
48
  const controller = {
48
49
  id: `solid-tag-html-${++controllerSequence}`,
49
50
  runtime,
51
+ events: createEventDispatcher({
52
+ onListenerError(error) {
53
+ globalThis.console?.error?.("solid-tag-runtime/html lifecycle subscriber error", error);
54
+ },
55
+ }),
50
56
  scope: normalizeScope(options.scope),
51
57
  root: options.root,
52
58
  selector: options.selector ?? DEFAULT_SELECTOR,
@@ -88,6 +94,7 @@ export function createHTMLRuntime(runtime, options = {}) {
88
94
  get appendTarget() {
89
95
  return getAppendTarget(controller);
90
96
  },
97
+ subscribe,
91
98
  register,
92
99
  observe,
93
100
  flush,
@@ -97,6 +104,8 @@ export function createHTMLRuntime(runtime, options = {}) {
97
104
  setAppendTarget,
98
105
  defineElement,
99
106
  registerElement,
107
+ updateElement,
108
+ removeElement,
100
109
  append,
101
110
  addModule,
102
111
  owns,
@@ -107,20 +116,41 @@ export function createHTMLRuntime(runtime, options = {}) {
107
116
  controller.api = api;
108
117
  return api;
109
118
 
119
+ function subscribe(typesOrListener, maybeListener) {
120
+ return controller.events.subscribe(typesOrListener, maybeListener);
121
+ }
122
+
110
123
  /** One-shot scan of matching scripts. */
111
124
  async function register(callOptions = {}) {
112
125
  const root = getRoot(mergeOptions(controller, callOptions), "HTML runtime register");
113
126
  const selector = callOptions.selector ?? controller.selector;
114
- const elements = Array.from(root.querySelectorAll(selector)).filter(element =>
115
- acceptsDiscoveredElement(controller, element, callOptions),
116
- );
127
+ const origin = callOptions.origin ?? "initial-scan";
128
+ const discovered = Array.from(root.querySelectorAll(selector));
129
+ const elements = [];
130
+
131
+ for (const element of discovered) {
132
+ const accepted = acceptsDiscoveredElement(controller, element, callOptions);
133
+ emitHTML(controller, {
134
+ type: "element-discovered",
135
+ element,
136
+ moduleId: nonEmpty(element?.getAttribute?.("module")),
137
+ origin,
138
+ accepted,
139
+ });
140
+ if (accepted) elements.push(element);
141
+ }
117
142
 
118
- return processElements(controller, elements, {
119
- ...callOptions,
120
- root,
121
- origin: callOptions.origin ?? "initial-scan",
122
- executeEntries: callOptions.executeEntries ?? controller.executeEntries,
123
- });
143
+ try {
144
+ return await processElements(controller, elements, {
145
+ ...callOptions,
146
+ root,
147
+ origin,
148
+ executeEntries: callOptions.executeEntries ?? controller.executeEntries,
149
+ });
150
+ } catch (error) {
151
+ emitHTMLError(controller, "register", error, { origin });
152
+ throw error;
153
+ }
124
154
  }
125
155
 
126
156
  /**
@@ -172,6 +202,15 @@ export function createHTMLRuntime(runtime, options = {}) {
172
202
  */
173
203
  async function setRoot(root, callOptions = {}) {
174
204
  const nextRoot = getRoot({ root }, "HTML runtime setRoot");
205
+ const previousRoot = controller.root ?? globalThis.document;
206
+ const previousAppendTarget = getAppendTarget(controller);
207
+ emitHTML(controller, {
208
+ type: "root-changing",
209
+ previousRoot,
210
+ root: nextRoot,
211
+ connected: controller.connected,
212
+ registerExisting: callOptions.registerExisting ?? controller.connected,
213
+ });
175
214
 
176
215
  // Finish work scheduled against the old root before rebinding observer state.
177
216
  await controller.queue;
@@ -184,7 +223,7 @@ export function createHTMLRuntime(runtime, options = {}) {
184
223
  const wasConnected = controller.connected;
185
224
  const previousObserverOptions = controller.observerOptions ?? {};
186
225
 
187
- if (wasConnected) stopObserver(controller, { preserveOptions: true });
226
+ if (wasConnected) stopObserver(controller, { preserveOptions: true, reason: "root-change" });
188
227
 
189
228
  controller.root = nextRoot;
190
229
  if (Object.prototype.hasOwnProperty.call(callOptions, "appendTo")) {
@@ -202,10 +241,13 @@ export function createHTMLRuntime(runtime, options = {}) {
202
241
  };
203
242
  controller.observerOptions = { ...observeOptions };
204
243
  try {
205
- return await connectObserver(controller, nextRoot, observeOptions, {
244
+ const result = await connectObserver(controller, nextRoot, observeOptions, {
206
245
  registerExisting,
207
246
  initial: false,
247
+ reason: "root-change",
208
248
  });
249
+ emitRootChanged(controller, previousRoot, nextRoot, registerExisting, previousAppendTarget);
250
+ return result;
209
251
  } catch (error) {
210
252
  stopObserver(controller, { preserveOptions: true });
211
253
  throw error;
@@ -213,9 +255,12 @@ export function createHTMLRuntime(runtime, options = {}) {
213
255
  }
214
256
 
215
257
  if (registerExisting) {
216
- return register({ ...callOptions, root: nextRoot });
258
+ const result = await register({ ...callOptions, root: nextRoot, origin: callOptions.origin ?? "root-change-scan" });
259
+ emitRootChanged(controller, previousRoot, nextRoot, registerExisting, previousAppendTarget);
260
+ return result;
217
261
  }
218
262
 
263
+ emitRootChanged(controller, previousRoot, nextRoot, registerExisting, previousAppendTarget);
219
264
  return emptyRegistrationResult();
220
265
  }
221
266
 
@@ -233,7 +278,15 @@ export function createHTMLRuntime(runtime, options = {}) {
233
278
  if (!target || (typeof target.append !== "function" && typeof target.appendChild !== "function")) {
234
279
  throw new TypeError("setAppendTarget() requires a target with append() or appendChild().");
235
280
  }
281
+ const previousTarget = getAppendTarget(controller);
236
282
  controller.appendTo = target;
283
+ if (previousTarget !== target) {
284
+ emitHTML(controller, {
285
+ type: "append-target-changed",
286
+ previousTarget,
287
+ target,
288
+ });
289
+ }
237
290
  return api;
238
291
  }
239
292
 
@@ -259,6 +312,152 @@ export function createHTMLRuntime(runtime, options = {}) {
259
312
  return result.modules[0] ?? definitionFromOwnedElement(controller, element);
260
313
  }
261
314
 
315
+ /**
316
+ * Re-read an owned script element and replace its runtime module source. The
317
+ * logical module id is stable; changing `module`/src-derived identity requires
318
+ * removeElement() + registerElement() instead.
319
+ */
320
+ async function updateElement(element, callOptions = {}) {
321
+ const record = requireOwnedRecord(controller, element, "updateElement");
322
+ emitHTML(controller, {
323
+ type: "element-updating",
324
+ element,
325
+ moduleId: record.moduleId,
326
+ origin: callOptions.origin ?? "update-element",
327
+ });
328
+ const metadata = inspectElement(controller, element, {
329
+ ...callOptions,
330
+ existingId: record.moduleId,
331
+ });
332
+ const nextModuleId = normalizeModuleId(runtime, metadata.id);
333
+
334
+ if (nextModuleId !== record.moduleId) {
335
+ throw new HTMLModuleError(
336
+ `updateElement() cannot change module identity from ${JSON.stringify(record.moduleId)} to ${JSON.stringify(nextModuleId)}. Remove and register the element again instead.`,
337
+ { element, moduleId: record.moduleId, sourceUrl: metadata.sourceUrl },
338
+ );
339
+ }
340
+
341
+ const candidate = {
342
+ ...record,
343
+ format: metadata.format,
344
+ entry: metadata.entry,
345
+ sourceUrl: metadata.sourceUrl,
346
+ inline: !metadata.sourceUrl,
347
+ };
348
+
349
+ const { source } = await prepareRecordSource(controller, candidate, callOptions);
350
+ runtime.update(record.moduleId, source, { format: candidate.format });
351
+
352
+ record.format = candidate.format;
353
+ record.entry = candidate.entry;
354
+ record.sourceUrl = candidate.sourceUrl;
355
+ record.inline = candidate.inline;
356
+ record.state = "defined";
357
+ record.error = undefined;
358
+ record.definition = definitionFromRecord(record);
359
+
360
+ if (record.entry && (callOptions.executeEntries ?? controller.executeEntries) !== false) {
361
+ record.state = "executing";
362
+ emitHTML(controller, {
363
+ type: "entry-executing",
364
+ element,
365
+ moduleId: record.moduleId,
366
+ origin: callOptions.origin ?? "update-element",
367
+ });
368
+ try {
369
+ const namespace = await runtime.import(record.moduleId);
370
+ record.state = "ready";
371
+ emitHTML(controller, {
372
+ type: "entry-executed",
373
+ element,
374
+ moduleId: record.moduleId,
375
+ namespace,
376
+ origin: callOptions.origin ?? "update-element",
377
+ });
378
+ } catch (error) {
379
+ record.state = "error";
380
+ record.error = error;
381
+ emitHTMLError(controller, "entry", error, {
382
+ element,
383
+ moduleId: record.moduleId,
384
+ origin: callOptions.origin ?? "update-element",
385
+ });
386
+ throw error;
387
+ }
388
+ }
389
+
390
+ emitHTML(controller, {
391
+ type: "element-updated",
392
+ element,
393
+ moduleId: record.moduleId,
394
+ definition: record.definition,
395
+ origin: callOptions.origin ?? "update-element",
396
+ });
397
+ return record.definition;
398
+ }
399
+
400
+ /**
401
+ * Release an element from this HTML controller. By default both the runtime
402
+ * module and DOM element are removed. DOM removal and module removal can be
403
+ * controlled independently.
404
+ */
405
+ async function removeElement(element, callOptions = {}) {
406
+ const record = requireOwnedRecord(controller, element, "removeElement");
407
+ const origin = callOptions.origin ?? "remove-element";
408
+ emitHTML(controller, {
409
+ type: "element-removing",
410
+ element,
411
+ moduleId: record.moduleId,
412
+ origin,
413
+ removeModule: callOptions.removeModule !== false,
414
+ removeFromDOM: callOptions.removeFromDOM !== false,
415
+ });
416
+ const removeModule = callOptions.removeModule !== false;
417
+ const removeFromDOM = callOptions.removeFromDOM !== false;
418
+ const invalidateDependents = callOptions.invalidateDependents !== false;
419
+
420
+ let moduleRemoved = false;
421
+ if (removeModule) {
422
+ moduleRemoved = runtime.remove(record.moduleId, { invalidateDependents });
423
+ }
424
+
425
+ controller.bindings.delete(record.moduleId);
426
+ elementOwners.delete(element);
427
+ record.state = "removed";
428
+ emitHTML(controller, {
429
+ type: "element-released",
430
+ element,
431
+ moduleId: record.moduleId,
432
+ origin,
433
+ });
434
+
435
+ let elementRemoved = false;
436
+ if (removeFromDOM) {
437
+ elementRemoved = removeDOMElement(element);
438
+ } else {
439
+ // Keep a released element in the observed tree from being immediately
440
+ // rediscovered. Explicit registerElement() can still claim it again.
441
+ controller.ignoredExisting.add(element);
442
+ }
443
+
444
+ const result = {
445
+ id: record.moduleId,
446
+ element,
447
+ moduleRemoved,
448
+ elementRemoved,
449
+ };
450
+ emitHTML(controller, {
451
+ type: "element-removed",
452
+ element,
453
+ moduleId: record.moduleId,
454
+ origin,
455
+ moduleRemoved,
456
+ elementRemoved,
457
+ });
458
+ return result;
459
+ }
460
+
262
461
  /**
263
462
  * Claim a user-created script before DOM insertion, append it to this
264
463
  * controller's target, then register it directly. The observer will see the
@@ -404,6 +603,13 @@ async function connectObserver(controller, root, callOptions = {}, mode = {}) {
404
603
  childList: true,
405
604
  subtree: callOptions.subtree ?? controller.subtree,
406
605
  });
606
+ emitHTML(controller, {
607
+ type: "observer-connected",
608
+ root,
609
+ subtree: callOptions.subtree ?? controller.subtree,
610
+ registerExisting,
611
+ reason: mode.reason ?? (mode.initial ? "observe" : "connect"),
612
+ });
407
613
 
408
614
  const result = registerExisting
409
615
  ? await enqueueObserverScan(controller, {
@@ -418,11 +624,17 @@ async function connectObserver(controller, root, callOptions = {}, mode = {}) {
418
624
 
419
625
  function stopObserver(controller, options = {}) {
420
626
  if (!controller.connected && !controller.observer) return;
627
+ const root = controller.observerRoot;
421
628
  controller.connected = false;
422
629
  controller.observer?.disconnect();
423
630
  controller.observer = undefined;
424
631
  controller.observerRoot = undefined;
425
632
  if (!options.preserveOptions) controller.observerOptions = undefined;
633
+ emitHTML(controller, {
634
+ type: "observer-disconnected",
635
+ root,
636
+ reason: options.reason ?? "disconnect",
637
+ });
426
638
  }
427
639
 
428
640
  async function enqueueObserverScan(controller, callOptions = {}) {
@@ -431,23 +643,76 @@ async function enqueueObserverScan(controller, callOptions = {}) {
431
643
 
432
644
  const root = controller.observerRoot ?? getRoot(mergeOptions(controller, callOptions), "HTML runtime observer");
433
645
  const selector = callOptions.selector ?? controller.selector;
434
- const elements = Array.from(root.querySelectorAll(selector)).filter(element => {
435
- if (controller.ignoredExisting.has(element)) return false;
436
- if (!acceptsDiscoveredElement(controller, element, callOptions)) return false;
646
+ const origin = callOptions.origin ?? "observer";
647
+ const discovered = Array.from(root.querySelectorAll(selector));
648
+ const elements = [];
649
+ let matched = 0;
650
+ let ignored = 0;
651
+
652
+ for (const element of discovered) {
653
+ if (controller.ignoredExisting.has(element)) {
654
+ ignored += 1;
655
+ emitHTML(controller, {
656
+ type: "element-discovered",
657
+ element,
658
+ moduleId: nonEmpty(element?.getAttribute?.("module")),
659
+ origin,
660
+ accepted: false,
661
+ });
662
+ continue;
663
+ }
664
+
665
+ const accepted = acceptsDiscoveredElement(controller, element, callOptions);
666
+ emitHTML(controller, {
667
+ type: "element-discovered",
668
+ element,
669
+ moduleId: nonEmpty(element?.getAttribute?.("module")),
670
+ origin,
671
+ accepted,
672
+ });
673
+ if (!accepted) {
674
+ ignored += 1;
675
+ continue;
676
+ }
677
+ matched += 1;
437
678
 
438
679
  const ownership = elementOwners.get(element);
439
- if (!ownership) return true;
440
- if (ownership.owner === controller) return false;
680
+ if (!ownership) {
681
+ elements.push(element);
682
+ continue;
683
+ }
684
+ if (ownership.owner === controller) {
685
+ ignored += 1;
686
+ continue;
687
+ }
441
688
 
442
689
  throw ownershipError(controller, element, ownership);
443
- });
690
+ }
444
691
 
445
- return processElements(controller, elements, {
446
- ...callOptions,
692
+ let result;
693
+ try {
694
+ result = await processElements(controller, elements, {
695
+ ...callOptions,
696
+ root,
697
+ origin,
698
+ executeEntries: callOptions.executeEntries ?? controller.executeEntries,
699
+ });
700
+ } catch (error) {
701
+ emitHTMLError(controller, "observer", error, { origin });
702
+ throw error;
703
+ }
704
+
705
+ emitHTML(controller, {
706
+ type: "observer-batch",
447
707
  root,
448
- origin: callOptions.origin ?? "observer",
449
- executeEntries: callOptions.executeEntries ?? controller.executeEntries,
708
+ origin,
709
+ discovered: discovered.length,
710
+ matched,
711
+ claimed: result.modules.length,
712
+ ignored,
713
+ modules: result.modules.map(module => module.id),
450
714
  });
715
+ return result;
451
716
  });
452
717
 
453
718
  controller.queue = task.then(
@@ -494,6 +759,12 @@ async function processClaims(controller, records, options = {}) {
494
759
  try {
495
760
  prepared = await Promise.all(records.map(record => prepareRecordSource(controller, record, options)));
496
761
  } catch (error) {
762
+ const failed = records.find(record => record.state === "loading") ?? records[0];
763
+ emitHTMLError(controller, failed?.sourceUrl ? "fetch" : "register", error, {
764
+ element: failed?.element,
765
+ moduleId: failed?.moduleId,
766
+ origin: failed?.origin ?? options.origin,
767
+ });
497
768
  for (const record of records) rejectRecord(record, error);
498
769
  throw error;
499
770
  }
@@ -511,8 +782,21 @@ async function processClaims(controller, records, options = {}) {
511
782
  record.definition = definitionFromRecord(record);
512
783
  record.resolve(record.definition);
513
784
  modules.push(record.definition);
785
+ emitHTML(controller, {
786
+ type: "element-registered",
787
+ element: record.element,
788
+ moduleId: record.moduleId,
789
+ definition: record.definition,
790
+ origin: record.origin,
791
+ });
514
792
  }
515
793
  } catch (error) {
794
+ const failed = records.find(record => record.state === "claimed" || record.state === "loading") ?? records[0];
795
+ emitHTMLError(controller, "register", error, {
796
+ element: failed?.element,
797
+ moduleId: failed?.moduleId,
798
+ origin: failed?.origin ?? options.origin,
799
+ });
516
800
  for (const record of records) {
517
801
  if (record.state === "claimed" || record.state === "loading") rejectRecord(record, error);
518
802
  }
@@ -528,14 +812,32 @@ async function processClaims(controller, records, options = {}) {
528
812
  if (record) record.state = "executing";
529
813
 
530
814
  try {
815
+ emitHTML(controller, {
816
+ type: "entry-executing",
817
+ element: entry.element,
818
+ moduleId: entry.id,
819
+ origin: record?.origin ?? options.origin ?? "entry",
820
+ });
531
821
  const namespace = await controller.runtime.import(entry.id);
532
822
  if (record) record.state = "ready";
533
823
  executed.push({ id: entry.id, namespace });
824
+ emitHTML(controller, {
825
+ type: "entry-executed",
826
+ element: entry.element,
827
+ moduleId: entry.id,
828
+ namespace,
829
+ origin: record?.origin ?? options.origin ?? "entry",
830
+ });
534
831
  } catch (error) {
535
832
  if (record) {
536
833
  record.state = "error";
537
834
  record.error = error;
538
835
  }
836
+ emitHTMLError(controller, "entry", error, {
837
+ element: entry.element,
838
+ moduleId: entry.id,
839
+ origin: record?.origin ?? options.origin ?? "entry",
840
+ });
539
841
  throw error;
540
842
  }
541
843
  }
@@ -608,6 +910,15 @@ function claimElement(controller, element, options = {}) {
608
910
 
609
911
  elementOwners.set(element, record);
610
912
  controller.bindings.set(moduleId, record);
913
+ emitHTML(controller, {
914
+ type: "element-claimed",
915
+ element,
916
+ moduleId,
917
+ format: record.format,
918
+ entry: record.entry,
919
+ sourceUrl: record.sourceUrl,
920
+ origin: record.origin,
921
+ });
611
922
  return { fresh: true, record };
612
923
  }
613
924
 
@@ -616,12 +927,29 @@ async function prepareRecordSource(controller, record, options) {
616
927
 
617
928
  let source;
618
929
  if (record.sourceUrl) {
930
+ const startedAt = performanceNow();
931
+ emitHTML(controller, {
932
+ type: "element-loading",
933
+ element: record.element,
934
+ moduleId: record.moduleId,
935
+ sourceUrl: record.sourceUrl,
936
+ origin: record.origin,
937
+ });
619
938
  source = await fetchSource(
620
939
  record.sourceUrl,
621
940
  options.fetch ?? controller.fetch ?? globalThis.fetch,
622
941
  record.element,
623
942
  record.moduleId,
624
943
  );
944
+ emitHTML(controller, {
945
+ type: "element-loaded",
946
+ element: record.element,
947
+ moduleId: record.moduleId,
948
+ sourceUrl: record.sourceUrl,
949
+ origin: record.origin,
950
+ bytes: String(source).length,
951
+ durationMs: performanceNow() - startedAt,
952
+ });
625
953
  } else {
626
954
  source = record.element.textContent ?? "";
627
955
  }
@@ -649,7 +977,7 @@ function inspectElement(controller, element, options = {}) {
649
977
  sourceUrl,
650
978
  });
651
979
 
652
- const id = explicitId ?? sourceUrl ?? (entry ? createAnonymousId(format) : undefined);
980
+ const id = explicitId ?? sourceUrl ?? (entry ? options.existingId ?? createAnonymousId(format) : undefined);
653
981
  if (!id) {
654
982
  throw new HTMLModuleError(
655
983
  "Inline solid runtime module scripts require a `module` attribute unless they are marked `entry`.",
@@ -660,6 +988,35 @@ function inspectElement(controller, element, options = {}) {
660
988
  return { id, format, entry, sourceUrl };
661
989
  }
662
990
 
991
+ function requireOwnedRecord(controller, element, caller) {
992
+ assertElement(element, caller);
993
+ const record = elementOwners.get(element);
994
+ if (!record) {
995
+ throw new HTMLModuleOwnershipError(
996
+ `${caller}() requires an element already owned by this HTML runtime.`,
997
+ { element, requestedScope: controller.scope },
998
+ );
999
+ }
1000
+ if (record.owner !== controller) throw ownershipError(controller, element, record);
1001
+ return record;
1002
+ }
1003
+
1004
+ function removeDOMElement(element) {
1005
+ if (typeof element?.remove === "function") {
1006
+ const wasConnected = Boolean(element.parentNode);
1007
+ element.remove();
1008
+ return wasConnected || !element.parentNode;
1009
+ }
1010
+
1011
+ const parent = element?.parentNode;
1012
+ if (parent && typeof parent.removeChild === "function") {
1013
+ parent.removeChild(element);
1014
+ return true;
1015
+ }
1016
+
1017
+ return false;
1018
+ }
1019
+
663
1020
  function definitionFromRecord(record) {
664
1021
  return {
665
1022
  id: record.moduleId,
@@ -741,6 +1098,46 @@ function ownershipError(controller, element, ownership) {
741
1098
  );
742
1099
  }
743
1100
 
1101
+ function emitHTML(controller, event) {
1102
+ controller.events.emit({
1103
+ htmlRuntimeId: controller.id,
1104
+ scope: controller.scope,
1105
+ timestamp: Date.now(),
1106
+ ...event,
1107
+ });
1108
+ }
1109
+
1110
+ function emitHTMLError(controller, operation, error, context = {}) {
1111
+ emitHTML(controller, {
1112
+ type: "html-error",
1113
+ operation,
1114
+ error,
1115
+ ...context,
1116
+ });
1117
+ }
1118
+
1119
+ function emitRootChanged(controller, previousRoot, root, registerExisting, previousAppendTarget) {
1120
+ emitHTML(controller, {
1121
+ type: "root-changed",
1122
+ previousRoot,
1123
+ root,
1124
+ registerExisting,
1125
+ connected: controller.connected,
1126
+ });
1127
+ const nextAppendTarget = getAppendTarget(controller);
1128
+ if (previousAppendTarget !== nextAppendTarget) {
1129
+ emitHTML(controller, {
1130
+ type: "append-target-changed",
1131
+ previousTarget: previousAppendTarget,
1132
+ target: nextAppendTarget,
1133
+ });
1134
+ }
1135
+ }
1136
+
1137
+ function performanceNow() {
1138
+ return globalThis.performance?.now?.() ?? Date.now();
1139
+ }
1140
+
744
1141
  function getDefaultController(runtime, options = {}) {
745
1142
  assertRuntime(runtime);
746
1143
  let controller = defaultControllers.get(runtime);