@vidact/runtime 0.2.0-beta.7 → 0.2.0-beta.9

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.
@@ -1,3 +1,4 @@
1
+ import { COMPILED_DELEGATED_EVENT_INVOKE, retainCompiledEventRoot } from '../dom/events.ts'
1
2
  import {
2
3
  currentIntrinsicNamespace,
3
4
  withIntrinsicNamespace,
@@ -51,7 +52,13 @@ import {
51
52
  unionSources,
52
53
  type SourceMask,
53
54
  } from '../source-mask.ts'
54
- import { createStateSlot, type StateSlot } from '../state-slot.ts'
55
+ import {
56
+ createReadOnlyStateSlot,
57
+ createStateSlot,
58
+ replaceReadOnlyStateSlot,
59
+ type ReadOnlyStateCell,
60
+ type StateSlot,
61
+ } from '../state-slot.ts'
55
62
  import { isSuspension, subscribeResource, useAsync } from './async-resource.ts'
56
63
  import {
57
64
  BINDING,
@@ -78,7 +85,28 @@ let retainedUiEnabled = typeof __VIDACT_RETAINED_UI__ !== 'undefined' && __VIDAC
78
85
  let profilingEnabled = false
79
86
  let activeOwnerCount = 0
80
87
  let createdOwnerCount = 0
88
+ let createdUpdaterCount = 0
89
+ let createdSchedulerPlanCount = 0
81
90
  const noop = (): void => {}
91
+ const COMPILED_EVENT_HANDLER = Symbol('vidact.compiled-event')
92
+ const COMPILED_INLINE_EVENT_OWNER = Symbol('vidact.compiled-event.owner')
93
+ const UNSET_BINDING = Symbol(DEV ? 'Vidact.UnsetBinding' : undefined)
94
+
95
+ type CompiledEventHandler = ((...arguments_: never[]) => void) & {
96
+ [COMPILED_EVENT_HANDLER]: true
97
+ }
98
+
99
+ type CompiledInlineEventHandler<Arguments extends unknown[]> = ((
100
+ ...arguments_: Arguments
101
+ ) => void) & {
102
+ [COMPILED_INLINE_EVENT_OWNER]: Owner
103
+ [COMPILED_DELEGATED_EVENT_INVOKE]: (...arguments_: Arguments) => void
104
+ }
105
+
106
+ type CompiledEventMetadata = {
107
+ [COMPILED_EVENT_HANDLER]?: true
108
+ [COMPILED_DELEGATED_EVENT_INVOKE]?: (...arguments_: never[]) => void
109
+ }
82
110
 
83
111
  type ContextFrame = {
84
112
  readonly context: CompiledContext<unknown>
@@ -104,7 +132,7 @@ type PortalPublication = readonly [commit: () => void, rollback: () => void]
104
132
 
105
133
  type Owner = [
106
134
  disposed: boolean,
107
- cleanups: Set<() => void>,
135
+ cleanups: Array<() => void>,
108
136
  context: ContextFrame | null,
109
137
  rootIdentity: RootIdentity,
110
138
  errorBoundary: ErrorBoundaryHandler | null,
@@ -145,11 +173,13 @@ type RetainedResource = {
145
173
  readonly phase: number
146
174
  }
147
175
 
176
+ type FlushTask = (() => void) | { readonly [1]: (sources?: SourceMask) => void }
177
+
148
178
  type RetainedConnection = {
149
179
  readonly parent: RetainedConnection | null
150
180
  readonly children: Set<RetainedConnection>
151
181
  readonly resources: Set<RetainedResource>
152
- readonly deferredFlushes: Map<() => void, CancelScheduledTask>
182
+ readonly deferredFlushes: Map<FlushTask, CancelScheduledTask>
153
183
  readonly afterFlush: Set<() => void>
154
184
  visible: boolean
155
185
  connected: boolean
@@ -189,9 +219,18 @@ export function deferred(render: () => CompiledRenderValue): StructuralBinding {
189
219
  return [
190
220
  STRUCTURAL,
191
221
  (parent, before) => {
192
- const hydrated = claimHydrationSlotRange(parent)
193
- if (hydrated === undefined) insertValue(parent, render(), before)
194
- else insertValue(parent, render(), hydrated[1])
222
+ const value = render()
223
+ if (ownedStructuralBinding(value)?.[2] === 'slot') {
224
+ insertValue(parent, value, before)
225
+ return
226
+ }
227
+ const hydrated = borrowHydrationSlotRange(parent, true)
228
+ if (hydrated === undefined) {
229
+ insertValue(parent, value, before)
230
+ return
231
+ }
232
+ claimHydrationSlotRange(parent)
233
+ insertValue(parent, value, hydrated[1])
195
234
  },
196
235
  'slot',
197
236
  ]
@@ -466,7 +505,7 @@ export function recordCompiledDebugValue<Value>(
466
505
  const remove = subscribeBinding(valueInput, () => {
467
506
  frame.values[index] = read()
468
507
  })
469
- owner[1].add(remove)
508
+ owner[1].push(remove)
470
509
  }
471
510
  }
472
511
 
@@ -823,6 +862,7 @@ function createPortalStructural(
823
862
  const portalOwner = withRootIdentity(rootIdentity, () =>
824
863
  withContextFrame(context, createOwner),
825
864
  )
865
+ let releaseEventRoot: (() => void) | undefined
826
866
  try {
827
867
  withOwner(portalOwner, () => insertValue(fragment, children, end))
828
868
  } catch (error) {
@@ -831,12 +871,15 @@ function createPortalStructural(
831
871
  throw error
832
872
  }
833
873
  const rollback = (): void => {
874
+ releaseEventRoot?.()
875
+ releaseEventRoot = undefined
834
876
  removeBetween(start, end)
835
877
  start.remove()
836
878
  end.remove()
837
879
  }
838
880
  const publication: PortalPublication = [
839
881
  () => {
882
+ releaseEventRoot = retainCompiledEventRoot(container)
840
883
  container.insertBefore(fragment, null)
841
884
  try {
842
885
  commitRangeRefs(start, end)
@@ -906,7 +949,7 @@ let transactionDepth = 0
906
949
  let disposalCascadeDepth = 0
907
950
  let drainingFlushes = false
908
951
  let activePublication: PublicationOperation[] | null = null
909
- const scheduledFlushes = new Set<() => void>()
952
+ const scheduledFlushes = new Set<FlushTask>()
910
953
  const disposalCascadeOwners = new Set<Owner>()
911
954
  const scopeOwners = new WeakMap<CompiledScope, Owner>()
912
955
  const scopeNamespaces = new WeakMap<CompiledScope, IntrinsicNamespace>()
@@ -916,6 +959,22 @@ const componentCommitOwners = new WeakMap<Comment, Owner>()
916
959
  const pendingInsertionCommits = new WeakMap<Owner, Set<() => void>>()
917
960
  const pendingOwnerCommits = new WeakMap<Owner, Set<() => void>>()
918
961
  const pendingRootPortals = new WeakMap<RootIdentity, Set<PortalPublication>>()
962
+ let pendingRefCount = 0
963
+ let pendingInsertionOwnerCount = 0
964
+ let pendingOwnerCommitCount = 0
965
+ let visitedPublishedNodeCount = 0
966
+
967
+ function setPendingRef(element: Element, pending: PendingRef): void {
968
+ if (!pendingRefs.has(element)) pendingRefCount += 1
969
+ pendingRefs.set(element, pending)
970
+ }
971
+
972
+ function deletePendingRef(element: Element, expected?: PendingRef): boolean {
973
+ if (expected !== undefined && pendingRefs.get(element) !== expected) return false
974
+ if (!pendingRefs.delete(element)) return false
975
+ pendingRefCount -= 1
976
+ return true
977
+ }
919
978
 
920
979
  export function createCompiledScope(): CompiledScope {
921
980
  return createScope(wideSourceOperations)
@@ -925,6 +984,171 @@ export function createNarrowCompiledScope(): CompiledScope {
925
984
  return createScope(narrowSourceOperations)
926
985
  }
927
986
 
987
+ type CompilerKeyedUpdater = [
988
+ reads: SourceMask,
989
+ writes: SourceMask | undefined,
990
+ run: (active: SourceMask) => void,
991
+ active: boolean,
992
+ errorOwner: Owner,
993
+ context: ContextFrame | null,
994
+ ]
995
+
996
+ /** Compiler-emitted map callbacks only need a compact, narrow row scheduler. */
997
+ class CompilerKeyedScope {
998
+ readonly owner = createOwner()
999
+ readonly updaters: Array<CompilerKeyedUpdater | undefined> = []
1000
+ pending: SourceMask = 0
1001
+ batchDepth = 0
1002
+ flushing = false
1003
+ hasWriter = false
1004
+ cachedUpdaterOrder: number[] | undefined;
1005
+
1006
+ [1](sources?: SourceMask): void {
1007
+ if (this.owner[0]) return
1008
+ if (sources === undefined) {
1009
+ this.flush()
1010
+ return
1011
+ }
1012
+ if (narrowSourceOperations[0](sources)) return
1013
+ const wasIdle = narrowSourceOperations[0](this.pending)
1014
+ this.pending = narrowSourceOperations[2](this.pending, sources)
1015
+ if (wasIdle && this.batchDepth === 0) this.schedule()
1016
+ }
1017
+
1018
+ [3](): void {
1019
+ if (this.owner[0]) return
1020
+ try {
1021
+ disposeOwner(this.owner)
1022
+ } finally {
1023
+ this.pending = 0
1024
+ for (const updater of this.updaters) {
1025
+ if (updater !== undefined) updater[3] = false
1026
+ }
1027
+ this.updaters.length = 0
1028
+ this.cachedUpdaterOrder = undefined
1029
+ }
1030
+ }
1031
+
1032
+ constructor() {
1033
+ const scope = this as unknown as CompiledScope
1034
+ scopeOwners.set(scope, this.owner)
1035
+ scopeNamespaces.set(scope, currentIntrinsicNamespace())
1036
+ activeScopeCollector?.add(scope)
1037
+ if (activeScopeCollector !== null) activeConstructionOwner = this.owner
1038
+ }
1039
+
1040
+ [0](reads: SourceMask, run: (active: SourceMask) => void, writes?: SourceMask): () => void {
1041
+ if (this.owner[0]) {
1042
+ throw new Error(DEV ? 'cannot add an updater to a disposed scope' : 'V002')
1043
+ }
1044
+ const errorOwner = activeOwner ?? this.owner
1045
+ const entry: CompilerKeyedUpdater = [
1046
+ reads,
1047
+ writes,
1048
+ run,
1049
+ true,
1050
+ errorOwner,
1051
+ activeContextFrame ?? errorOwner[2],
1052
+ ]
1053
+ if (DEV) createdUpdaterCount += 1
1054
+ const index = this.updaters.length
1055
+ this.updaters.push(entry)
1056
+ this.hasWriter ||= writes !== undefined
1057
+ this.cachedUpdaterOrder = undefined
1058
+ if (activeOwner === this.owner) return noop
1059
+ const remove = (): void => {
1060
+ if (this.updaters[index] !== entry) return
1061
+ entry[3] = false
1062
+ this.updaters[index] = undefined
1063
+ this.cachedUpdaterOrder = undefined
1064
+ }
1065
+ activeOwner?.[1].push(remove)
1066
+ return remove
1067
+ }
1068
+
1069
+ [2]<T>(operation: () => T): T {
1070
+ this.batchDepth += 1
1071
+ transactionDepth += 1
1072
+ try {
1073
+ return operation()
1074
+ } finally {
1075
+ this.batchDepth -= 1
1076
+ if (this.batchDepth === 0 && !narrowSourceOperations[0](this.pending)) this.schedule()
1077
+ transactionDepth -= 1
1078
+ if (transactionDepth === 0) drainFlushes()
1079
+ }
1080
+ }
1081
+
1082
+ private schedule(): void {
1083
+ if (retainedUiEnabled) scheduleOwnerFlush(this.owner, this)
1084
+ else scheduleFlush(this)
1085
+ }
1086
+
1087
+ private flush(): void {
1088
+ if (this.flushing || narrowSourceOperations[0](this.pending)) return
1089
+ const operation = (): void => {
1090
+ this.flushing = true
1091
+ try {
1092
+ let pass = 0
1093
+ while (!narrowSourceOperations[0](this.pending)) {
1094
+ pass += 1
1095
+ if (pass > MAX_FLUSH_PASSES) {
1096
+ this.pending = 0
1097
+ throw new Error(DEV ? 'Vidact compiled scope did not stabilize' : 'V001')
1098
+ }
1099
+ let active = this.pending
1100
+ this.pending = 0
1101
+ const order = this.hasWriter
1102
+ ? (this.cachedUpdaterOrder ??= topologicalUpdaterOrder(
1103
+ this.updaters as ReadonlyArray<CompiledUpdater | undefined>,
1104
+ narrowSourceOperations,
1105
+ ))
1106
+ : undefined
1107
+ const count = order?.length ?? this.updaters.length
1108
+ for (let position = 0; position < count; position += 1) {
1109
+ const index = order?.[position] ?? position
1110
+ const updater = this.updaters[index]
1111
+ if (
1112
+ updater === undefined ||
1113
+ !updater[3] ||
1114
+ !narrowSourceOperations[1](active, updater[0])
1115
+ ) {
1116
+ continue
1117
+ }
1118
+ try {
1119
+ const invoke = () =>
1120
+ withOwner(updater[4], () =>
1121
+ withContextFrame(updater[5], () =>
1122
+ withScopeNamespace(this as unknown as CompiledScope, () => updater[2](active)),
1123
+ ),
1124
+ )
1125
+ if (profilingEnabled) measureProfileWork(this.owner, 'updater', invoke)
1126
+ else invoke()
1127
+ } catch (error) {
1128
+ if (!routeOwnerError(updater[4], error)) {
1129
+ failedOwner = updater[4]
1130
+ throw error
1131
+ }
1132
+ }
1133
+ if (updater[1] !== undefined) {
1134
+ active = narrowSourceOperations[2](active, updater[1])
1135
+ }
1136
+ }
1137
+ }
1138
+ } finally {
1139
+ this.flushing = false
1140
+ }
1141
+ if (retainedUiEnabled) notifyRetainedFlush(this.owner[5] ?? null)
1142
+ }
1143
+ if (profilingEnabled) measureProfileWork(this.owner, 'scheduler', operation, true)
1144
+ else operation()
1145
+ }
1146
+ }
1147
+
1148
+ function createCompilerKeyedScope(): CompiledScope {
1149
+ return new CompilerKeyedScope() as unknown as CompiledScope
1150
+ }
1151
+
928
1152
  export function runCompiledTransaction<T>(operation: () => T): T {
929
1153
  transactionDepth += 1
930
1154
  try {
@@ -939,12 +1163,13 @@ function createScope(operations: SourceOperations): CompiledScope {
939
1163
  const namespace = currentIntrinsicNamespace()
940
1164
  const owner = createOwner()
941
1165
  const updaters: Array<CompiledUpdater | undefined> = []
942
- const freeUpdaterIndexes: number[] = []
943
- const addedDuringFlush = new Set<CompiledUpdater>()
1166
+ let freeUpdaterIndexes: number[] | undefined
1167
+ let addedDuringFlush: Set<CompiledUpdater> | undefined
944
1168
  let batchDepth = 0
945
1169
  let flushing = false
946
1170
  let pending: SourceMask = 0
947
1171
  let cachedUpdaterOrder: number[] | undefined
1172
+ let hasWriter = false
948
1173
 
949
1174
  const flush = (): void => {
950
1175
  if (owner[0] || flushing) return
@@ -952,7 +1177,9 @@ function createScope(operations: SourceOperations): CompiledScope {
952
1177
  flushing = true
953
1178
  try {
954
1179
  let pass = 0
955
- const updaterOrder = (cachedUpdaterOrder ??= topologicalUpdaterOrder(updaters, operations))
1180
+ const updaterOrder = hasWriter
1181
+ ? (cachedUpdaterOrder ??= topologicalUpdaterOrder(updaters, operations))
1182
+ : undefined
956
1183
  while (!operations[0](pending)) {
957
1184
  pass += 1
958
1185
  if (pass > MAX_FLUSH_PASSES) {
@@ -962,11 +1189,13 @@ function createScope(operations: SourceOperations): CompiledScope {
962
1189
 
963
1190
  let active = pending
964
1191
  pending = 0
965
- for (const index of updaterOrder) {
1192
+ const updaterCount = updaterOrder?.length ?? updaters.length
1193
+ for (let position = 0; position < updaterCount; position += 1) {
1194
+ const index = updaterOrder?.[position] ?? position
966
1195
  const updater = updaters[index]
967
1196
  if (
968
1197
  updater === undefined ||
969
- addedDuringFlush.has(updater) ||
1198
+ addedDuringFlush?.has(updater) ||
970
1199
  !updater[3] ||
971
1200
  !operations[1](active, updater[0])
972
1201
  ) {
@@ -979,10 +1208,10 @@ function createScope(operations: SourceOperations): CompiledScope {
979
1208
  }
980
1209
  if (updater[1] !== undefined) active = operations[2](active, updater[1])
981
1210
  }
982
- addedDuringFlush.clear()
1211
+ addedDuringFlush?.clear()
983
1212
  }
984
1213
  } finally {
985
- addedDuringFlush.clear()
1214
+ addedDuringFlush?.clear()
986
1215
  flushing = false
987
1216
  }
988
1217
  if (retainedUiEnabled) notifyRetainedFlush(owner[5] ?? null)
@@ -1015,25 +1244,28 @@ function createScope(operations: SourceOperations): CompiledScope {
1015
1244
  },
1016
1245
  true,
1017
1246
  ]
1018
- const reusableIndex = freeUpdaterIndexes.pop()
1247
+ if (DEV) createdUpdaterCount += 1
1248
+ const reusableIndex = freeUpdaterIndexes?.pop()
1019
1249
  const index = reusableIndex ?? updaters.length
1020
1250
  updaters[index] = entry
1251
+ hasWriter ||= writes !== undefined
1021
1252
  cachedUpdaterOrder = undefined
1022
- if (flushing) addedDuringFlush.add(entry)
1253
+ if (flushing) (addedDuringFlush ??= new Set()).add(entry)
1023
1254
  const remove = (): void => {
1024
1255
  if (updaters[index] !== entry) return
1025
1256
  entry[3] = false
1026
1257
  updaters[index] = undefined
1027
1258
  cachedUpdaterOrder = undefined
1028
- freeUpdaterIndexes.push(index)
1259
+ ;(freeUpdaterIndexes ??= []).push(index)
1029
1260
  }
1030
- if (activeOwner !== null && activeOwner !== owner) activeOwner[1].add(remove)
1261
+ if (activeOwner !== null && activeOwner !== owner) activeOwner[1].push(remove)
1031
1262
  return remove
1032
1263
  },
1033
1264
  (sources) => {
1034
1265
  if (owner[0] || operations[0](sources)) return
1266
+ const wasIdle = operations[0](pending)
1035
1267
  pending = operations[2](pending, sources)
1036
- if (batchDepth === 0) {
1268
+ if (wasIdle && batchDepth === 0) {
1037
1269
  if (retainedUiEnabled) scheduleOwnerFlush(owner, flush)
1038
1270
  else scheduleFlush(flush)
1039
1271
  }
@@ -1063,8 +1295,9 @@ function createScope(operations: SourceOperations): CompiledScope {
1063
1295
  if (updater !== undefined) updater[3] = false
1064
1296
  }
1065
1297
  updaters.length = 0
1066
- freeUpdaterIndexes.length = 0
1067
- addedDuringFlush.clear()
1298
+ hasWriter = false
1299
+ if (freeUpdaterIndexes !== undefined) freeUpdaterIndexes.length = 0
1300
+ addedDuringFlush?.clear()
1068
1301
  }
1069
1302
  },
1070
1303
  ]
@@ -1079,9 +1312,16 @@ function topologicalUpdaterOrder(
1079
1312
  updaters: ReadonlyArray<CompiledUpdater | undefined>,
1080
1313
  operations: SourceOperations,
1081
1314
  ): number[] {
1082
- const activeIndexes = updaters.flatMap((updater, index) =>
1083
- updater === undefined || !updater[3] ? [] : [index],
1084
- )
1315
+ if (DEV) createdSchedulerPlanCount += 1
1316
+ const activeIndexes: number[] = []
1317
+ let hasWriter = false
1318
+ for (let index = 0; index < updaters.length; index += 1) {
1319
+ const updater = updaters[index]
1320
+ if (updater === undefined || !updater[3]) continue
1321
+ activeIndexes.push(index)
1322
+ if (updater[1] !== undefined) hasWriter = true
1323
+ }
1324
+ if (!hasWriter) return activeIndexes
1085
1325
  const readersBySource = new Map<number, number[]>()
1086
1326
  for (const readerIndex of activeIndexes) {
1087
1327
  operations[3](updaters[readerIndex]![0], (sourceIndex) => {
@@ -1239,7 +1479,7 @@ export function createCompiledMemo<T>(
1239
1479
  removeUpdater()
1240
1480
  throw new Error(DEV ? 'createCompiledMemo received an unknown scope' : 'V003')
1241
1481
  }
1242
- owner[1].add(removeUpdater)
1482
+ owner[1].push(removeUpdater)
1243
1483
  return slot
1244
1484
  }
1245
1485
 
@@ -1363,7 +1603,7 @@ export function createCompiledExternalStore<T>(
1363
1603
  removeStoreUpdater()
1364
1604
  throw error
1365
1605
  }
1366
- owner[1].add(() => {
1606
+ owner[1].push(() => {
1367
1607
  removeStoreUpdater()
1368
1608
  deactivateStore()
1369
1609
  })
@@ -1381,7 +1621,7 @@ export function createCompiledExternalStore<T>(
1381
1621
  disposeResource()
1382
1622
  throw error
1383
1623
  }
1384
- owner[1].add(() => {
1624
+ owner[1].push(() => {
1385
1625
  removeStoreUpdater()
1386
1626
  disposeResource()
1387
1627
  })
@@ -1483,6 +1723,42 @@ export function useCallback<T extends Function>(callback: T, _dependencies: read
1483
1723
  return callback
1484
1724
  }
1485
1725
 
1726
+ const ownedBlockFacades = new WeakMap<StructuralBinding, RenderableProtocol>()
1727
+ const ownedFacadeBlocks = new WeakMap<RenderableProtocol, StructuralBinding>()
1728
+
1729
+ function ownedStructuralBinding(value: unknown): StructuralBinding | undefined {
1730
+ if (isStructuralBinding(value)) return value
1731
+ return isRenderableProtocol(value) ? ownedFacadeBlocks.get(value) : undefined
1732
+ }
1733
+
1734
+ /**
1735
+ * Owned blocks are arrays internally, which user-level data flow must never
1736
+ * observe: dependency code inspects children with `Array.isArray` and spreads
1737
+ * them. Prop reads wrap a block in a stable renderable facade that
1738
+ * materializes back to the block when rendered.
1739
+ */
1740
+ function sanitizeCompiledPropValue<T>(value: T): T {
1741
+ if (!isStructuralBinding(value)) return value
1742
+ let facade = ownedBlockFacades.get(value)
1743
+ if (facade === undefined) {
1744
+ const capability = { props: {} } as unknown as RenderableProtocol
1745
+ Object.defineProperty(capability, RENDERABLE, {
1746
+ configurable: false,
1747
+ enumerable: false,
1748
+ value: {
1749
+ identity: value,
1750
+ input: {},
1751
+ reconcile: false,
1752
+ construct: () => value,
1753
+ },
1754
+ })
1755
+ facade = capability
1756
+ ownedBlockFacades.set(value, facade)
1757
+ ownedFacadeBlocks.set(facade, value)
1758
+ }
1759
+ return facade as T
1760
+ }
1761
+
1486
1762
  export function createCompiledProp<T>(
1487
1763
  scope: CompiledScope,
1488
1764
  sourceMask: SourceMask,
@@ -1493,7 +1769,9 @@ export function createCompiledProp<T>(
1493
1769
  const upstream = isCompiledBinding(input) ? input : undefined
1494
1770
  const read = (): T => {
1495
1771
  const value = upstream === undefined ? (input as T) : upstream[1]()
1496
- return value === undefined && fallback !== undefined ? fallback() : value
1772
+ return sanitizeCompiledPropValue(
1773
+ value === undefined && fallback !== undefined ? fallback() : value,
1774
+ )
1497
1775
  }
1498
1776
  const slot = createStateSlot<T>(scope[1], sourceMask, read(), assertWritable)
1499
1777
  if (upstream !== undefined) {
@@ -1502,7 +1780,7 @@ export function createCompiledProp<T>(
1502
1780
  if (owner === undefined) {
1503
1781
  throw new Error(DEV ? 'createCompiledProp received an unknown scope' : 'V003')
1504
1782
  }
1505
- owner[1].add(remove)
1783
+ owner[1].push(remove)
1506
1784
  }
1507
1785
  return slot
1508
1786
  }
@@ -1518,7 +1796,10 @@ export function createCompiledRestProp(
1518
1796
  Object.entries(input).filter(([name]) => !excluded.has(name))
1519
1797
  const read = (): Record<string, unknown> =>
1520
1798
  Object.fromEntries(
1521
- entries().map(([name, value]) => [name, isCompiledBinding(value) ? value[1]() : value]),
1799
+ entries().map(([name, value]) => [
1800
+ name,
1801
+ sanitizeCompiledPropValue(isCompiledBinding(value) ? value[1]() : value),
1802
+ ]),
1522
1803
  )
1523
1804
  const slot = createStateSlot(scope[1], sourceMask, read(), stateWriteGuard(scope))
1524
1805
  const publish = (): void => {
@@ -1545,7 +1826,7 @@ export function createCompiledRestProp(
1545
1826
  if (owner === undefined) {
1546
1827
  throw new Error(DEV ? 'createCompiledRestProp received an unknown scope' : 'V003')
1547
1828
  }
1548
- owner[1].add(() => {
1829
+ owner[1].push(() => {
1549
1830
  for (const remove of removers) remove()
1550
1831
  })
1551
1832
  return slot
@@ -1605,8 +1886,9 @@ export function compiledEvent<Arguments extends unknown[]>(
1605
1886
  throw new Error(DEV ? 'compiledEvent received an unknown scope' : 'V003')
1606
1887
  }
1607
1888
  const errorOwner = activeOwner ?? owner
1608
- return (...arguments_) =>
1609
- runOwnerTask(errorOwner, () =>
1889
+ const listener = (...arguments_: Arguments) => {
1890
+ if (errorOwner[0]) return
1891
+ return runOwnerTask(errorOwner, () =>
1610
1892
  withOwner(errorOwner, () =>
1611
1893
  scope[2](() => {
1612
1894
  const current = isCompiledBinding(handler) ? handler[1]() : handler
@@ -1615,6 +1897,43 @@ export function compiledEvent<Arguments extends unknown[]>(
1615
1897
  }),
1616
1898
  ),
1617
1899
  )
1900
+ }
1901
+ ;(listener as unknown as CompiledEventHandler)[COMPILED_EVENT_HANDLER] = true
1902
+ return listener
1903
+ }
1904
+
1905
+ export function compiledInlineEvent<Arguments extends unknown[]>(
1906
+ scope: CompiledScope,
1907
+ handler: (...arguments_: Arguments) => void,
1908
+ ): (...arguments_: Arguments) => void {
1909
+ const owner = scopeOwners.get(scope)
1910
+ if (owner === undefined) {
1911
+ throw new Error(DEV ? 'compiledInlineEvent received an unknown scope' : 'V003')
1912
+ }
1913
+ const listener = handler as unknown as CompiledInlineEventHandler<Arguments>
1914
+ listener[COMPILED_INLINE_EVENT_OWNER] = activeOwner ?? owner
1915
+ listener[COMPILED_DELEGATED_EVENT_INVOKE] = invokeCompiledInlineEvent
1916
+ return listener
1917
+ }
1918
+
1919
+ function invokeCompiledInlineEvent<Arguments extends unknown[]>(
1920
+ this: CompiledInlineEventHandler<Arguments>,
1921
+ ...arguments_: Arguments
1922
+ ): void {
1923
+ const owner = this[COMPILED_INLINE_EVENT_OWNER]
1924
+ if (owner[0]) return
1925
+ return runOwnerTask(owner, () =>
1926
+ withOwner(owner, () => runCompiledTransaction(() => this(...arguments_))),
1927
+ )
1928
+ }
1929
+
1930
+ export function isCompiledEventHandler(value: unknown): value is EventListener {
1931
+ if (typeof value !== 'function') return false
1932
+ const handler = value as typeof value & CompiledEventMetadata
1933
+ return (
1934
+ handler[COMPILED_EVENT_HANDLER] === true ||
1935
+ typeof handler[COMPILED_DELEGATED_EVENT_INVOKE] === 'function'
1936
+ )
1618
1937
  }
1619
1938
 
1620
1939
  export interface CompiledTaskController {
@@ -1716,7 +2035,12 @@ export function choose(
1716
2035
  return structural(
1717
2036
  scope,
1718
2037
  (parent, before) => {
1719
- const [start, end, hydrated] = structuralRange(parent, before, 'choice')
2038
+ const range = structuralRange(parent, before, 'choice', true, true)
2039
+ const markerlessHydration = range === undefined
2040
+ const start = range?.[0] ?? document.createComment(DEV ? 'vidact:choice' : '')
2041
+ const end = range?.[1] ?? document.createComment(DEV ? '/vidact:choice' : '')
2042
+ const hydrated = range?.[2] ?? true
2043
+ const ownsMarkers = range?.[3] ?? true
1720
2044
  let selected = -1
1721
2045
  let branchOwner: Owner | null = null
1722
2046
  let branchNodes: readonly Node[] = []
@@ -1726,15 +2050,28 @@ export function choose(
1726
2050
  const next =
1727
2051
  mode === 'not-nullish' ? (value === null || value === undefined ? 1 : 0) : value ? 0 : 1
1728
2052
  if (next === selected) return
1729
- const currentParent = rangeParent(start, end, 'choice block')
1730
2053
  const nextOwner = createOwner()
1731
- const [fragment, nodes] =
1732
- hydrated && selected === -1
1733
- ? withHydrationInsertion(currentParent, end, () =>
1734
- stageRender(next === 0 ? consequent : alternate, nextOwner),
1735
- )
1736
- : stageRender(next === 0 ? consequent : alternate, nextOwner)
1737
- currentParent.insertBefore(fragment, end)
2054
+ const render = next === 0 ? consequent : alternate
2055
+ let nodes: readonly Node[]
2056
+ if (markerlessHydration && selected === -1) {
2057
+ const [first, after, hydratedNodes] = stageMarkerlessHydrationRender(
2058
+ parent,
2059
+ before,
2060
+ render,
2061
+ nextOwner,
2062
+ )
2063
+ parent.insertBefore(start, first)
2064
+ parent.insertBefore(end, after)
2065
+ nodes = hydratedNodes
2066
+ } else {
2067
+ const currentParent = rangeParent(start, end, 'choice block')
2068
+ const [fragment, stagedNodes] =
2069
+ hydrated && selected === -1
2070
+ ? withHydrationInsertion(currentParent, end, () => stageRender(render, nextOwner))
2071
+ : stageRender(render, nextOwner)
2072
+ currentParent.insertBefore(fragment, end)
2073
+ nodes = stagedNodes
2074
+ }
1738
2075
  try {
1739
2076
  commitPublishedNodes(nodes)
1740
2077
  } catch (error) {
@@ -1762,8 +2099,10 @@ export function choose(
1762
2099
  try {
1763
2100
  disposePublished(branchOwner, branchNodes)
1764
2101
  } finally {
1765
- start.remove()
1766
- end.remove()
2102
+ if (ownsMarkers) {
2103
+ start.remove()
2104
+ end.remove()
2105
+ }
1767
2106
  }
1768
2107
  })
1769
2108
  },
@@ -1850,9 +2189,21 @@ export function keyed<T, K>(
1850
2189
  values: () => readonly T[],
1851
2190
  key: (value: T, index: number) => K,
1852
2191
  render: (value: StateSlot<T>, index: StateSlot<number>, itemScope: CompiledScope) => RenderValue,
1853
- additionalScope?: CompiledScope,
2192
+ trackIndexOrAdditionalScope?: boolean | CompiledScope,
2193
+ additionalScopeOrReads?: CompiledScope | SourceMask,
1854
2194
  additionalReads?: SourceMask,
1855
2195
  ): StructuralBinding {
2196
+ const compilerManaged = typeof trackIndexOrAdditionalScope === 'boolean'
2197
+ const trackIndex =
2198
+ typeof trackIndexOrAdditionalScope === 'boolean' ? trackIndexOrAdditionalScope : true
2199
+ const additionalScope =
2200
+ typeof trackIndexOrAdditionalScope === 'boolean'
2201
+ ? (additionalScopeOrReads as CompiledScope | undefined)
2202
+ : trackIndexOrAdditionalScope
2203
+ const resolvedAdditionalReads =
2204
+ typeof trackIndexOrAdditionalScope === 'boolean'
2205
+ ? additionalReads
2206
+ : (additionalScopeOrReads as SourceMask | undefined)
1856
2207
  return structural(
1857
2208
  scope,
1858
2209
  (parent, before) => {
@@ -1863,27 +2214,56 @@ export function keyed<T, K>(
1863
2214
  {
1864
2215
  key,
1865
2216
  render(value, index) {
1866
- const owner = createOwner()
1867
- const itemScope = createNarrowCompiledScope()
1868
- const valueSlot = createCompiledState(itemScope, itemSource, value)
1869
- const indexSlot = createCompiledState(itemScope, indexSource, index)
2217
+ const itemScope = compilerManaged
2218
+ ? createCompilerKeyedScope()
2219
+ : createNarrowCompiledScope()
2220
+ const owner = scopeOwners.get(itemScope)!
2221
+ const valueSlot = compilerManaged
2222
+ ? createReadOnlyStateSlot(itemScope, itemSource, value)
2223
+ : createCompiledState(itemScope, itemSource, value)
2224
+ const indexSlot = trackIndex
2225
+ ? compilerManaged
2226
+ ? createReadOnlyStateSlot(itemScope, indexSource, index)
2227
+ : createCompiledState(itemScope, indexSource, index)
2228
+ : undefined
1870
2229
  try {
1871
2230
  const nodes = withOwner(owner, () => {
1872
- onCleanup(itemScope[3])
1873
- return materialize(render(valueSlot, indexSlot, itemScope))
2231
+ const rendered = render(
2232
+ valueSlot as StateSlot<T>,
2233
+ indexSlot as StateSlot<number>,
2234
+ itemScope,
2235
+ )
2236
+ return !isHydrating() &&
2237
+ rendered instanceof Node &&
2238
+ !(rendered instanceof DocumentFragment)
2239
+ ? [rendered]
2240
+ : materialize(rendered)
1874
2241
  })
1875
2242
  return [
1876
2243
  nodes,
1877
2244
  (nextValue: T, nextIndex: number) => {
2245
+ if (
2246
+ Object.is(valueSlot.get(), nextValue) &&
2247
+ (!trackIndex || indexSlot!.get() === nextIndex)
2248
+ ) {
2249
+ return
2250
+ }
1878
2251
  itemScope[2](() => {
1879
- valueSlot.set(nextValue)
1880
- indexSlot.set(nextIndex)
2252
+ if (compilerManaged) {
2253
+ replaceReadOnlyStateSlot(valueSlot as ReadOnlyStateCell<T>, nextValue)
2254
+ if (indexSlot !== undefined) {
2255
+ replaceReadOnlyStateSlot(indexSlot as ReadOnlyStateCell<number>, nextIndex)
2256
+ }
2257
+ } else {
2258
+ ;(valueSlot as StateSlot<T>).set(nextValue)
2259
+ ;(indexSlot as StateSlot<number> | undefined)?.set(nextIndex)
2260
+ }
1881
2261
  })
1882
2262
  },
1883
- () => disposeOwner(owner),
2263
+ itemScope,
1884
2264
  ] as const
1885
2265
  } catch (error) {
1886
- disposeOwner(owner)
2266
+ itemScope[3]()
1887
2267
  throw error
1888
2268
  }
1889
2269
  },
@@ -1900,7 +2280,13 @@ export function keyed<T, K>(
1900
2280
  }
1901
2281
  }
1902
2282
  update()
1903
- const removeUpdater = subscribe(scope, reads, update, additionalScope, additionalReads)
2283
+ const removeUpdater = subscribe(
2284
+ scope,
2285
+ reads,
2286
+ update,
2287
+ additionalScope,
2288
+ resolvedAdditionalReads,
2289
+ )
1904
2290
  onCleanup(() => {
1905
2291
  removeUpdater()
1906
2292
  list.dispose()
@@ -1988,7 +2374,7 @@ export function compiledRoot(
1988
2374
  componentCommitOwners.set(end, owner)
1989
2375
  const fragment = document.createDocumentFragment()
1990
2376
  if (hydratedRange === undefined) fragment.append(start, end)
1991
- owner[1].add(() => {
2377
+ owner[1].push(() => {
1992
2378
  try {
1993
2379
  removeBetween(start, end)
1994
2380
  } finally {
@@ -2052,7 +2438,7 @@ export function adoptCompiledRoot(root: unknown): void {
2052
2438
  if (range === undefined) return
2053
2439
  const owner = scopeOwners.get(range[2])
2054
2440
  if (activeOwner !== null && owner !== undefined && activeOwner !== owner) {
2055
- activeOwner[1].add(range[2][3])
2441
+ activeOwner[1].push(range[2][3])
2056
2442
  }
2057
2443
  }
2058
2444
 
@@ -2093,7 +2479,9 @@ export function queueElementRef(element: Element, value: unknown): void {
2093
2479
  if (!isRefValue(value)) {
2094
2480
  throw new TypeError(DEV ? 'ref must be a callback or an object with current' : 'V006')
2095
2481
  }
2096
- pendingRefs.set(element, [activeOwner, value])
2482
+ const pending: PendingRef = [activeOwner, value]
2483
+ setPendingRef(element, pending)
2484
+ activeOwner?.[1].push(() => deletePendingRef(element, pending))
2097
2485
  }
2098
2486
 
2099
2487
  export function useImperativeHandle<T>(
@@ -2117,7 +2505,7 @@ export function useImperativeHandle<T>(
2117
2505
  DEV ? 'reactive useImperativeHandle dependencies are not supported yet' : 'V014',
2118
2506
  )
2119
2507
  }
2120
- queueOwnerCommit(owner, () => owner[1].add(attachRef(ref, create())))
2508
+ queueOwnerCommit(owner, () => owner[1].push(attachRef(ref, create())))
2121
2509
  }
2122
2510
 
2123
2511
  type EffectResult = void | (() => void)
@@ -2154,7 +2542,7 @@ export function useInsertionEffect(
2154
2542
  ? readProfiledEffectCleanup(owner, create)
2155
2543
  : readEffectCleanup(create())
2156
2544
  })
2157
- lifetimeOwner[1].add(() => cleanup())
2545
+ lifetimeOwner[1].push(() => cleanup())
2158
2546
  return
2159
2547
  }
2160
2548
  let cleanup = noop
@@ -2181,7 +2569,7 @@ export function useInsertionEffect(
2181
2569
  queueInsertionCommit(owner, () => {
2182
2570
  if (!lifetimeOwner[0]) activate()
2183
2571
  })
2184
- lifetimeOwner[1].add(disposeResource)
2572
+ lifetimeOwner[1].push(disposeResource)
2185
2573
  }
2186
2574
 
2187
2575
  export function useEffect(create: () => EffectResult, dependencies?: readonly unknown[]): void {
@@ -2221,7 +2609,7 @@ function registerEffect(
2221
2609
  run()
2222
2610
  }
2223
2611
  })
2224
- lifetimeOwner[1].add(() => {
2612
+ lifetimeOwner[1].push(() => {
2225
2613
  generation += 1
2226
2614
  if (passive) (DEV ? scheduleTask : queueMicrotask)(() => runOwnerTask(lifetimeOwner, cleanup))
2227
2615
  else cleanup()
@@ -2270,7 +2658,7 @@ function registerEffect(
2270
2658
  queueOwnerCommit(owner, () => {
2271
2659
  if (!lifetimeOwner[0]) activate()
2272
2660
  })
2273
- lifetimeOwner[1].add(disposeResource)
2661
+ lifetimeOwner[1].push(disposeResource)
2274
2662
  }
2275
2663
 
2276
2664
  export function compiledLayoutEffect(
@@ -2319,7 +2707,7 @@ export function compiledInsertionEffect(
2319
2707
  const removeUpdater = subscribe(scope, reads, () =>
2320
2708
  stagePublication([noop, noop, undefined, run, -20]),
2321
2709
  )
2322
- lifetimeOwner[1].add(() => {
2710
+ lifetimeOwner[1].push(() => {
2323
2711
  removeUpdater()
2324
2712
  cleanup()
2325
2713
  })
@@ -2372,7 +2760,7 @@ export function compiledInsertionEffect(
2372
2760
  const removeUpdater = subscribe(scope, reads, () => {
2373
2761
  if (connected) stagePublication([noop, noop, undefined, run, -20])
2374
2762
  })
2375
- lifetimeOwner[1].add(() => {
2763
+ lifetimeOwner[1].push(() => {
2376
2764
  removeUpdater()
2377
2765
  disposeResource()
2378
2766
  })
@@ -2428,7 +2816,7 @@ export function compiledEffect(
2428
2816
  if (passive) schedule()
2429
2817
  else stagePublication([noop, noop, undefined, run, 20])
2430
2818
  })
2431
- lifetimeOwner[1].add(() => {
2819
+ lifetimeOwner[1].push(() => {
2432
2820
  generation += 1
2433
2821
  removeUpdater()
2434
2822
  if (passive) (DEV ? scheduleTask : queueMicrotask)(() => runOwnerTask(lifetimeOwner, cleanup))
@@ -2501,7 +2889,7 @@ export function compiledEffect(
2501
2889
  if (passive) schedule()
2502
2890
  else stagePublication([noop, noop, undefined, run, 20])
2503
2891
  })
2504
- lifetimeOwner[1].add(() => {
2892
+ lifetimeOwner[1].push(() => {
2505
2893
  removeUpdater()
2506
2894
  disposeResource()
2507
2895
  })
@@ -2620,7 +3008,7 @@ export function compiledImperativeHandle<T>(
2620
3008
  ])
2621
3009
  }
2622
3010
  const removeUpdater = subscribe(scope, reads, update)
2623
- owner[1].add(() => {
3011
+ owner[1].push(() => {
2624
3012
  removeUpdater()
2625
3013
  cleanup()
2626
3014
  })
@@ -2631,6 +3019,7 @@ function queueOwnerCommit(owner: Owner, commit: () => void): void {
2631
3019
  if (commits === undefined) {
2632
3020
  commits = new Set()
2633
3021
  pendingOwnerCommits.set(owner, commits)
3022
+ pendingOwnerCommitCount += 1
2634
3023
  }
2635
3024
  commits.add(commit)
2636
3025
  }
@@ -2640,6 +3029,7 @@ function queueInsertionCommit(owner: Owner, commit: () => void): void {
2640
3029
  if (commits === undefined) {
2641
3030
  commits = new Set()
2642
3031
  pendingInsertionCommits.set(owner, commits)
3032
+ pendingInsertionOwnerCount += 1
2643
3033
  }
2644
3034
  commits.add(commit)
2645
3035
  }
@@ -2670,7 +3060,7 @@ export function mountCompiledRef(element: Element, value: CompiledBinding<unknow
2670
3060
  // oxlint-disable-next-line unicorn/consistent-function-scoping -- This placeholder is replaced when the ref attaches.
2671
3061
  let cleanup = noop
2672
3062
  const pending: PendingRef = [activeOwner, current, (attached) => (cleanup = attached)]
2673
- pendingRefs.set(element, pending)
3063
+ setPendingRef(element, pending)
2674
3064
 
2675
3065
  const removeUpdater = subscribeBinding(value, () => {
2676
3066
  const next = value[1]()
@@ -2729,7 +3119,7 @@ export function mountCompiledRef(element: Element, value: CompiledBinding<unknow
2729
3119
  })
2730
3120
  onCleanup(() => {
2731
3121
  removeUpdater()
2732
- if (pendingRefs.get(element) === pending) pendingRefs.delete(element)
3122
+ deletePendingRef(element, pending)
2733
3123
  cleanup()
2734
3124
  })
2735
3125
  }
@@ -2814,6 +3204,7 @@ export function mountCompiled(
2814
3204
  throw new Error(DEV ? 'mountCompiled received an unknown component result' : 'V007')
2815
3205
  }
2816
3206
  const previous = [...host.childNodes]
3207
+ const releaseEventRoot = retainCompiledEventRoot(host)
2817
3208
  try {
2818
3209
  root[1](host, previous[0] ?? null)
2819
3210
  const rootOwner = scopeOwners.get(range[2])
@@ -2829,6 +3220,7 @@ export function mountCompiled(
2829
3220
  for (const node of previous) host.removeChild(node)
2830
3221
  }
2831
3222
  } catch (error) {
3223
+ releaseEventRoot()
2832
3224
  try {
2833
3225
  range[2][3]()
2834
3226
  } catch {
@@ -2838,8 +3230,17 @@ export function mountCompiled(
2838
3230
  rootIdentity.onUncaughtError(error)
2839
3231
  return { dispose: noop }
2840
3232
  }
3233
+ let disposed = false
2841
3234
  return {
2842
- dispose: range[2][3],
3235
+ dispose: () => {
3236
+ if (disposed) return
3237
+ disposed = true
3238
+ try {
3239
+ range[2][3]()
3240
+ } finally {
3241
+ releaseEventRoot()
3242
+ }
3243
+ },
2843
3244
  }
2844
3245
  }
2845
3246
 
@@ -2859,12 +3260,43 @@ export function mountCompiledBinding(parent: Node, value: CompiledBinding<unknow
2859
3260
  mountCompiledBindingBefore(parent, value, null)
2860
3261
  }
2861
3262
 
2862
- export function mountCompiledProp(
2863
- value: CompiledBinding<unknown>,
2864
- apply: (next: unknown) => void | (() => void),
3263
+ type CompiledPropApply<T> =
3264
+ | ((next: T) => void | (() => void))
3265
+ | ((element: Element, name: string, next: T) => void | (() => void))
3266
+
3267
+ function applyCompiledPropValue<T>(
3268
+ apply: CompiledPropApply<T>,
3269
+ element: Element | undefined,
3270
+ name: string | undefined,
3271
+ next: T,
3272
+ ): void | (() => void) {
3273
+ return element === undefined
3274
+ ? (apply as (next: T) => void | (() => void))(next)
3275
+ : (apply as (element: Element, name: string, next: T) => void | (() => void))(
3276
+ element,
3277
+ name!,
3278
+ next,
3279
+ )
3280
+ }
3281
+
3282
+ export function mountCompiledProp<T>(
3283
+ value: CompiledBinding<T>,
3284
+ apply: (next: T) => void | (() => void),
3285
+ ): void
3286
+ export function mountCompiledProp<T>(
3287
+ value: CompiledBinding<T>,
3288
+ apply: (element: Element, name: string, next: T) => void | (() => void),
3289
+ element: Element,
3290
+ name: string,
3291
+ ): void
3292
+ export function mountCompiledProp<T>(
3293
+ value: CompiledBinding<T>,
3294
+ apply: CompiledPropApply<T>,
3295
+ element?: Element,
3296
+ name?: string,
2865
3297
  ): void {
2866
3298
  let current = value[1]()
2867
- let cleanup = apply(current)
3299
+ let cleanup = applyCompiledPropValue(apply, element, name, current)
2868
3300
  const removeUpdater = subscribeBinding(value, () => {
2869
3301
  const next = value[1]()
2870
3302
  if (Object.is(next, current)) return
@@ -2874,10 +3306,10 @@ export function mountCompiledProp(
2874
3306
  stagePublication([
2875
3307
  () => {
2876
3308
  try {
2877
- nextCleanup = apply(next)
3309
+ nextCleanup = applyCompiledPropValue(apply, element, name, next)
2878
3310
  } catch (error) {
2879
3311
  try {
2880
- apply(previous)
3312
+ applyCompiledPropValue(apply, element, name, previous)
2881
3313
  } catch {
2882
3314
  // Preserve the setter error that aborted publication.
2883
3315
  }
@@ -2891,7 +3323,7 @@ export function mountCompiledProp(
2891
3323
  () => {
2892
3324
  if (!committedNext) return
2893
3325
  nextCleanup?.()
2894
- cleanup = apply(previous)
3326
+ cleanup = applyCompiledPropValue(apply, element, name, previous)
2895
3327
  current = previous
2896
3328
  committedNext = false
2897
3329
  },
@@ -2968,14 +3400,40 @@ function structuralRange(
2968
3400
  parent: Node,
2969
3401
  before: Node | null,
2970
3402
  label: string,
2971
- ): readonly [start: Comment, end: Comment, hydrated: boolean] {
3403
+ borrowOuter?: boolean,
3404
+ allowMarkerlessHydration?: false,
3405
+ ): readonly [start: Comment, end: Comment, hydrated: boolean, ownsMarkers: boolean]
3406
+ function structuralRange(
3407
+ parent: Node,
3408
+ before: Node | null,
3409
+ label: string,
3410
+ borrowOuter: boolean,
3411
+ allowMarkerlessHydration: true,
3412
+ ): readonly [start: Comment, end: Comment, hydrated: boolean, ownsMarkers: boolean] | undefined
3413
+ function structuralRange(
3414
+ parent: Node,
3415
+ before: Node | null,
3416
+ label: string,
3417
+ borrowOuter = false,
3418
+ allowMarkerlessHydration = false,
3419
+ ): readonly [start: Comment, end: Comment, hydrated: boolean, ownsMarkers: boolean] | undefined {
3420
+ const current = borrowHydrationSlotRange(parent, true)
3421
+ if (current !== undefined) {
3422
+ claimHydrationSlotRange(parent)
3423
+ return [current[0], current[1], true, true]
3424
+ }
3425
+ if (borrowOuter) {
3426
+ const outer = borrowHydrationSlotRange(parent, false)
3427
+ if (outer !== undefined) return [outer[0], outer[1], true, false]
3428
+ }
3429
+ if (allowMarkerlessHydration && hydrationCursor(parent) !== undefined) return undefined
2972
3430
  const hydrated = claimHydrationSlotRange(parent)
2973
- if (hydrated !== undefined) return [hydrated[0], hydrated[1], true]
3431
+ if (hydrated !== undefined) return [hydrated[0], hydrated[1], true, true]
2974
3432
  const start = document.createComment(DEV ? `vidact:${label}` : '')
2975
3433
  const end = document.createComment(DEV ? `/vidact:${label}` : '')
2976
3434
  parent.insertBefore(start, before)
2977
3435
  parent.insertBefore(end, before)
2978
- return [start, end, false]
3436
+ return [start, end, false, true]
2979
3437
  }
2980
3438
 
2981
3439
  function subscribe(
@@ -2985,12 +3443,29 @@ function subscribe(
2985
3443
  additionalScope?: CompiledScope,
2986
3444
  additionalReads?: SourceMask,
2987
3445
  ): () => void {
2988
- const removers = [scope[0](reads, run)]
2989
- if (additionalScope !== undefined && additionalReads !== undefined) {
2990
- removers.push(additionalScope[0](additionalReads, () => withScopeNamespace(scope, run)))
2991
- }
3446
+ let first: (() => void) | undefined
3447
+ if (!isEmptySources(reads)) {
3448
+ const remove = scope[0](reads, run)
3449
+ if (remove !== noop) first = remove
3450
+ }
3451
+ let second: (() => void) | undefined
3452
+ if (
3453
+ additionalScope !== undefined &&
3454
+ additionalReads !== undefined &&
3455
+ !isEmptySources(additionalReads)
3456
+ ) {
3457
+ const additionalRun =
3458
+ scopeNamespaces.get(additionalScope) === scopeNamespaces.get(scope)
3459
+ ? run
3460
+ : () => withScopeNamespace(scope, run)
3461
+ const remove = additionalScope[0](additionalReads, additionalRun)
3462
+ if (remove !== noop) second = remove
3463
+ }
3464
+ if (first === undefined) return second ?? noop
3465
+ if (second === undefined) return first
2992
3466
  return () => {
2993
- for (const remove of removers) remove()
3467
+ first()
3468
+ second()
2994
3469
  }
2995
3470
  }
2996
3471
 
@@ -3033,7 +3508,7 @@ function createOwner(
3033
3508
  if (profilingEnabled) {
3034
3509
  const owner: Owner = [
3035
3510
  false,
3036
- new Set(),
3511
+ [],
3037
3512
  context,
3038
3513
  rootIdentity,
3039
3514
  boundary,
@@ -3044,8 +3519,8 @@ function createOwner(
3044
3519
  return owner
3045
3520
  }
3046
3521
  const owner: Owner = retainedUiEnabled
3047
- ? [false, new Set(), context, rootIdentity, boundary, retainedConnection]
3048
- : [false, new Set(), context, rootIdentity, boundary]
3522
+ ? [false, [], context, rootIdentity, boundary, retainedConnection]
3523
+ : [false, [], context, rootIdentity, boundary]
3049
3524
  if (DEV) owner[7] = activeConstructionOwner?.[7] ?? activeProfileName
3050
3525
  return owner
3051
3526
  }
@@ -3388,7 +3863,7 @@ function withRootIdentity<T>(rootIdentity: RootIdentity | null, operation: () =>
3388
3863
  }
3389
3864
 
3390
3865
  function onCleanup(cleanup: () => void): void {
3391
- activeOwner?.[1].add(cleanup)
3866
+ activeOwner?.[1].push(cleanup)
3392
3867
  }
3393
3868
 
3394
3869
  function disposeOwner(owner: Owner): void {
@@ -3397,21 +3872,21 @@ function disposeOwner(owner: Owner): void {
3397
3872
  disposalCascadeDepth += 1
3398
3873
  disposalCascadeOwners.add(owner)
3399
3874
  if (DEV) activeOwnerCount -= 1
3400
- pendingInsertionCommits.delete(owner)
3401
- pendingOwnerCommits.delete(owner)
3402
- const cleanups = [...owner[1]]
3403
- owner[1].clear()
3875
+ if (pendingInsertionCommits.delete(owner)) pendingInsertionOwnerCount -= 1
3876
+ if (pendingOwnerCommits.delete(owner)) pendingOwnerCommitCount -= 1
3404
3877
  let firstError: unknown
3405
3878
  let hasError = false
3406
3879
  try {
3407
- for (let index = cleanups.length - 1; index >= 0; index -= 1) {
3880
+ for (let remaining = owner[1].length; remaining > 0; remaining -= 1) {
3881
+ const cleanup = owner[1].pop()
3408
3882
  try {
3409
- cleanups[index]?.()
3883
+ cleanup?.()
3410
3884
  } catch (error) {
3411
3885
  if (!hasError) firstError = error
3412
3886
  hasError = true
3413
3887
  }
3414
3888
  }
3889
+ owner[1].length = 0
3415
3890
  } finally {
3416
3891
  disposalCascadeDepth -= 1
3417
3892
  if (disposalCascadeDepth === 0) disposalCascadeOwners.clear()
@@ -3423,8 +3898,50 @@ function disposeOwner(owner: Owner): void {
3423
3898
  export function readCompiledOwnerMetrics(): {
3424
3899
  readonly active: number
3425
3900
  readonly created: number
3901
+ readonly updaters: number
3902
+ readonly schedulerPlans: number
3426
3903
  } {
3427
- return { active: activeOwnerCount, created: createdOwnerCount }
3904
+ return {
3905
+ active: activeOwnerCount,
3906
+ created: createdOwnerCount,
3907
+ updaters: createdUpdaterCount,
3908
+ schedulerPlans: createdSchedulerPlanCount,
3909
+ }
3910
+ }
3911
+
3912
+ /** @internal Test-only publication traversal evidence. */
3913
+ export function readCompiledPublicationMetrics(): { readonly visitedNodes: number } {
3914
+ return { visitedNodes: visitedPublishedNodeCount }
3915
+ }
3916
+
3917
+ interface MountedBindingState {
3918
+ start: Comment | null
3919
+ end: Comment | null
3920
+ current: unknown
3921
+ currentOwner: Owner | null
3922
+ text: Text | null
3923
+ }
3924
+
3925
+ function ensureMountedBindingRange(state: MountedBindingState): readonly [Comment, Comment] {
3926
+ if (state.start !== null && state.end !== null) return [state.start, state.end]
3927
+ const anchor = state.text
3928
+ const parent = anchor?.parentNode
3929
+ if (anchor === null || parent === null || parent === undefined) {
3930
+ throw new Error(DEV ? 'markerless binding text is detached' : 'V010')
3931
+ }
3932
+ state.start = document.createComment(DEV ? 'vidact:binding' : '')
3933
+ state.end = document.createComment(DEV ? '/vidact:binding' : '')
3934
+ parent.insertBefore(state.start, anchor)
3935
+ parent.insertBefore(state.end, anchor.nextSibling)
3936
+ return [state.start, state.end]
3937
+ }
3938
+
3939
+ function clearMountedBinding(state: MountedBindingState): void {
3940
+ const [start, end] = ensureMountedBindingRange(state)
3941
+ const owner = state.currentOwner
3942
+ state.currentOwner = null
3943
+ state.text = null
3944
+ disposeRange(owner, start, end)
3428
3945
  }
3429
3946
 
3430
3947
  function mountCompiledBindingBefore(
@@ -3432,144 +3949,160 @@ function mountCompiledBindingBefore(
3432
3949
  value: CompiledBinding<unknown>,
3433
3950
  before: Node | null,
3434
3951
  ): void {
3435
- const unset = Symbol(DEV ? 'Vidact.UnsetBinding' : undefined)
3436
3952
  const initial = value[1]()
3437
3953
  const scalarInitial = isScalarRenderValue(initial)
3954
+ const structuralInitial = ownedStructuralBinding(initial)
3438
3955
  const hydratedTextRange = scalarInitial
3439
3956
  ? claimHydrationTextRange(parent, toText(initial))
3440
3957
  : undefined
3441
3958
  const borrowedHydrationRange =
3442
3959
  isHydrating() && !scalarInitial
3443
- ? borrowHydrationSlotRange(parent, isStructuralBinding(initial) && initial[2] === 'slot')
3960
+ ? borrowHydrationSlotRange(parent, structuralInitial?.[2] === 'slot')
3444
3961
  : undefined
3445
3962
  const hydratedStructuralRange =
3446
3963
  isHydrating() && !scalarInitial
3447
3964
  ? (borrowedHydrationRange ?? claimHydrationSlotRange(parent))
3448
3965
  : undefined
3449
3966
  const hydratedRange = hydratedTextRange ?? hydratedStructuralRange
3450
- const start = hydratedRange?.[0] ?? document.createComment(DEV ? 'vidact:binding' : '')
3451
- const end = hydratedRange?.[1] ?? document.createComment(DEV ? '/vidact:binding' : '')
3452
- if (hydratedRange === undefined) {
3967
+ const markerlessScalar = hydratedRange === undefined && scalarInitial && !isHydrating()
3968
+ let start = hydratedRange?.[0] ?? null
3969
+ let end = hydratedRange?.[1] ?? null
3970
+ if (!markerlessScalar && hydratedRange === undefined) {
3971
+ start = document.createComment(DEV ? 'vidact:binding' : '')
3972
+ end = document.createComment(DEV ? '/vidact:binding' : '')
3453
3973
  parent.insertBefore(start, before)
3454
3974
  parent.insertBefore(end, before)
3455
3975
  }
3456
- let current: unknown = hydratedTextRange === undefined ? unset : initial
3457
- let currentOwner: Owner | null = null
3458
- let text: Text | null = hydratedTextRange?.[2] ?? null
3459
- const stableRenderable = (renderable: RenderableProtocol): unknown => {
3460
- const identity = renderableIdentity(renderable)
3461
- let previous = renderablePropsSnapshot(renderable)
3462
- const input = binding(
3463
- value[2],
3464
- value[3],
3465
- () => {
3466
- const next = value[1]()
3467
- if (!isRenderableProtocol(next) || !Object.is(renderableIdentity(next), identity)) {
3468
- return previous
3469
- }
3470
- previous = renderablePropsSnapshot(next)
3471
- return previous
3472
- },
3473
- value[4],
3474
- value[5],
3475
- )
3476
- return materializeRenderableWithInput(renderable, input)
3976
+ const state: MountedBindingState = {
3977
+ start,
3978
+ end,
3979
+ current: hydratedTextRange === undefined && !markerlessScalar ? UNSET_BINDING : initial,
3980
+ currentOwner: null,
3981
+ text: hydratedTextRange?.[2] ?? null,
3477
3982
  }
3478
-
3479
- const clear = (): void => {
3480
- const owner = currentOwner
3481
- currentOwner = null
3482
- text = null
3483
- disposeRange(owner, start, end)
3983
+ if (markerlessScalar) {
3984
+ state.text = document.createTextNode(toText(initial))
3985
+ parent.insertBefore(state.text, before)
3484
3986
  }
3485
3987
  const update = (): void => {
3486
3988
  const next = value[1]()
3487
- if (current !== unset && Object.is(next, current)) return
3989
+ if (state.current !== UNSET_BINDING && Object.is(next, state.current)) return
3488
3990
  if (
3489
- current !== unset &&
3490
- isRenderableProtocol(current) &&
3991
+ state.current !== UNSET_BINDING &&
3992
+ isRenderableProtocol(state.current) &&
3491
3993
  isRenderableProtocol(next) &&
3492
- canReconcileRenderable(current) &&
3994
+ canReconcileRenderable(state.current) &&
3493
3995
  canReconcileRenderable(next) &&
3494
- Object.is(renderableIdentity(current), renderableIdentity(next))
3996
+ Object.is(renderableIdentity(state.current), renderableIdentity(next))
3495
3997
  ) {
3496
- current = next
3998
+ state.current = next
3497
3999
  return
3498
4000
  }
3499
- const currentParent = rangeParent(start, end, 'binding range')
3500
-
3501
4001
  if (isScalarRenderValue(next)) {
3502
4002
  const content = toText(next)
3503
- if (text !== null) {
3504
- const target = text
4003
+ if (state.text !== null) {
4004
+ const target = state.text
3505
4005
  const previousContent = target.data
3506
- const previous = current
4006
+ const previous = state.current
3507
4007
  stagePublication([
3508
4008
  () => {
3509
4009
  if (target.data !== content) target.data = content
3510
- current = next
4010
+ state.current = next
3511
4011
  },
3512
4012
  () => {
3513
4013
  if (target.data !== previousContent) target.data = previousContent
3514
- current = previous
4014
+ state.current = previous
3515
4015
  },
3516
4016
  ])
3517
4017
  return
3518
4018
  }
3519
- clear()
3520
- text = document.createTextNode(content)
3521
- currentParent.insertBefore(text, end)
3522
- current = next
4019
+ clearMountedBinding(state)
4020
+ const [rangeStart, rangeEnd] = ensureMountedBindingRange(state)
4021
+ const currentParent = rangeParent(rangeStart, rangeEnd, 'binding range')
4022
+ state.text = document.createTextNode(content)
4023
+ currentParent.insertBefore(state.text, rangeEnd)
4024
+ state.current = next
3523
4025
  return
3524
4026
  }
3525
4027
 
4028
+ const [rangeStart, rangeEnd] = ensureMountedBindingRange(state)
4029
+ const currentParent = rangeParent(rangeStart, rangeEnd, 'binding range')
3526
4030
  const nextOwner = createOwner()
3527
4031
  const stagedValue =
3528
4032
  isRenderableProtocol(next) && canReconcileRenderable(next)
3529
- ? stableRenderable(next)
4033
+ ? stableRenderableBinding(value, next)
3530
4034
  : (next as RenderValue)
3531
4035
  const [fragment, staged] = stageValue(stagedValue as RenderValue, nextOwner)
3532
4036
  try {
3533
- clear()
4037
+ clearMountedBinding(state)
3534
4038
  } catch (error) {
3535
4039
  disposeOwner(nextOwner)
3536
4040
  throw error
3537
4041
  }
3538
- currentOwner = nextOwner
3539
- currentParent.insertBefore(fragment, end)
4042
+ state.currentOwner = nextOwner
4043
+ currentParent.insertBefore(fragment, rangeEnd)
3540
4044
  commitPublishedNodes(staged)
3541
- current = next
4045
+ state.current = next
3542
4046
  }
3543
4047
 
3544
4048
  if (hydratedStructuralRange !== undefined) {
4049
+ const rangeEnd = hydratedStructuralRange[1]
3545
4050
  const nextOwner = createOwner()
3546
- const [fragment, staged] = withHydrationInsertion(parent, end, () =>
4051
+ const [fragment, staged] = withHydrationInsertion(parent, rangeEnd, () =>
3547
4052
  stageRender(
3548
4053
  () =>
3549
4054
  (isRenderableProtocol(initial) && canReconcileRenderable(initial)
3550
- ? stableRenderable(initial)
4055
+ ? stableRenderableBinding(value, initial)
3551
4056
  : initial) as RenderValue,
3552
4057
  nextOwner,
3553
4058
  ),
3554
4059
  )
3555
- parent.insertBefore(fragment, end)
4060
+ parent.insertBefore(fragment, rangeEnd)
3556
4061
  commitPublishedNodes(staged)
3557
- currentOwner = nextOwner
3558
- current = initial
3559
- } else if (hydratedRange === undefined) {
4062
+ state.currentOwner = nextOwner
4063
+ state.current = initial
4064
+ } else if (hydratedRange === undefined && !markerlessScalar) {
3560
4065
  update()
3561
4066
  }
3562
4067
  const removeUpdater = subscribeBinding(value, update)
3563
4068
  onCleanup(() => {
3564
4069
  removeUpdater()
3565
- clear()
4070
+ if (state.start === null || state.end === null) {
4071
+ state.text?.remove()
4072
+ state.text = null
4073
+ return
4074
+ }
4075
+ clearMountedBinding(state)
3566
4076
  if (borrowedHydrationRange === undefined) {
3567
- start.remove()
3568
- end.remove()
4077
+ state.start.remove()
4078
+ state.end.remove()
3569
4079
  }
3570
4080
  })
3571
4081
  }
3572
4082
 
4083
+ function stableRenderableBinding(
4084
+ value: CompiledBinding<unknown>,
4085
+ renderable: RenderableProtocol,
4086
+ ): unknown {
4087
+ const identity = renderableIdentity(renderable)
4088
+ let previous = renderablePropsSnapshot(renderable)
4089
+ const input = binding(
4090
+ value[2],
4091
+ value[3],
4092
+ () => {
4093
+ const next = value[1]()
4094
+ if (!isRenderableProtocol(next) || !Object.is(renderableIdentity(next), identity)) {
4095
+ return previous
4096
+ }
4097
+ previous = renderablePropsSnapshot(next)
4098
+ return previous
4099
+ },
4100
+ value[4],
4101
+ value[5],
4102
+ )
4103
+ return materializeRenderableWithInput(renderable, input)
4104
+ }
4105
+
3573
4106
  function materialize(value: RenderValue): Node[] {
3574
4107
  const insertionPoint = hydrationInsertionPoint()
3575
4108
  if (insertionPoint !== undefined) {
@@ -3643,6 +4176,40 @@ function stageRender(
3643
4176
  }
3644
4177
  }
3645
4178
 
4179
+ function stageMarkerlessHydrationRender(
4180
+ parent: Node,
4181
+ before: Node | null,
4182
+ render: () => CompiledRenderValue,
4183
+ owner: Owner,
4184
+ ): readonly [first: Node | null, after: Node | null, nodes: readonly Node[]] {
4185
+ const first = hydrationCursor(parent)
4186
+ if (first === undefined) {
4187
+ throw new Error('markerless hydration render requires active hydration')
4188
+ }
4189
+ try {
4190
+ withHydrationInsertion(parent, before, () =>
4191
+ withOwner(owner, () => insertValue(parent, render(), before)),
4192
+ )
4193
+ } catch (error) {
4194
+ try {
4195
+ disposeOwner(owner)
4196
+ } catch {
4197
+ // Preserve the render or hydration error after every registered cleanup ran.
4198
+ }
4199
+ throw error
4200
+ }
4201
+ const after = hydrationCursor(parent)
4202
+ if (after === undefined) {
4203
+ disposeOwner(owner)
4204
+ throw new Error('markerless hydration render lost its hydration cursor')
4205
+ }
4206
+ const nodes: Node[] = []
4207
+ for (let node = first; node !== null && node !== after; node = node.nextSibling) {
4208
+ nodes.push(node)
4209
+ }
4210
+ return [first, after, nodes]
4211
+ }
4212
+
3646
4213
  function insertValue(
3647
4214
  parent: Node,
3648
4215
  value: RenderValue,
@@ -3792,10 +4359,13 @@ function isRefValue(value: unknown): value is RefValue {
3792
4359
  }
3793
4360
 
3794
4361
  function claimPendingRefOwners(root: Node): void {
4362
+ if (pendingRefCount === 0) return
3795
4363
  visitNodes(root, (node) => {
3796
4364
  if (!(node instanceof Element)) return
3797
4365
  const pending = pendingRefs.get(node)
3798
- if (pending !== undefined && pending[0] === null) pending[0] = activeOwner
4366
+ if (pending === undefined || pending[0] !== null) return
4367
+ pending[0] = activeOwner
4368
+ activeOwner?.[1].push(() => deletePendingRef(node, pending))
3799
4369
  })
3800
4370
  }
3801
4371
 
@@ -3813,9 +4383,15 @@ function commitPublishedNodesNow(nodes: readonly Node[]): void {
3813
4383
  if (publicationRoot instanceof DocumentFragment && !(publicationRoot instanceof ShadowRoot)) {
3814
4384
  return
3815
4385
  }
3816
- for (const node of nodes) commitNodeInsertions(node)
3817
- for (const node of nodes) commitNodeRefs(node)
3818
- for (const node of nodes) commitNodeResources(node)
4386
+ if (pendingInsertionOwnerCount > 0) {
4387
+ for (const node of nodes) commitNodeInsertions(node)
4388
+ }
4389
+ if (pendingRefCount > 0) {
4390
+ for (const node of nodes) commitNodeRefs(node)
4391
+ }
4392
+ if (pendingOwnerCommitCount > 0) {
4393
+ for (const node of nodes) commitNodeResources(node)
4394
+ }
3819
4395
  }
3820
4396
 
3821
4397
  function commitNodeInsertions(root: Node): void {
@@ -3829,12 +4405,12 @@ function commitNodeRefs(root: Node): void {
3829
4405
  if (node instanceof Element) {
3830
4406
  const pending = pendingRefs.get(node)
3831
4407
  if (pending !== undefined) {
3832
- pendingRefs.delete(node)
4408
+ deletePendingRef(node, pending)
3833
4409
  const owner = pending[0] ?? activeOwner
3834
4410
  commitContextInsertions(owner?.[2] ?? null)
3835
4411
  commitOwnerInsertions(owner ?? undefined)
3836
4412
  const cleanup = attachRef(pending[1], node)
3837
- if (pending[2] === undefined) owner?.[1].add(cleanup)
4413
+ if (pending[2] === undefined) owner?.[1].push(cleanup)
3838
4414
  else pending[2](cleanup)
3839
4415
  }
3840
4416
  }
@@ -3858,6 +4434,7 @@ function commitOwnerInsertions(owner: Owner | undefined): void {
3858
4434
  const commits = pendingInsertionCommits.get(owner)
3859
4435
  if (commits === undefined) return
3860
4436
  pendingInsertionCommits.delete(owner)
4437
+ pendingInsertionOwnerCount -= 1
3861
4438
  for (const commit of commits) commit()
3862
4439
  }
3863
4440
 
@@ -3866,6 +4443,7 @@ function commitOwnerResources(owner: Owner | undefined): void {
3866
4443
  const commits = pendingOwnerCommits.get(owner)
3867
4444
  if (commits === undefined) return
3868
4445
  pendingOwnerCommits.delete(owner)
4446
+ pendingOwnerCommitCount -= 1
3869
4447
  for (const commit of commits) commit()
3870
4448
  }
3871
4449
 
@@ -3880,6 +4458,7 @@ function commitRangeRefs(start: Node, end: Node): void {
3880
4458
  }
3881
4459
 
3882
4460
  function visitNodes(root: Node, visit: (node: Node) => void): void {
4461
+ visitedPublishedNodeCount += 1
3883
4462
  visit(root)
3884
4463
  for (const child of root.childNodes) visitNodes(child, visit)
3885
4464
  }
@@ -3896,12 +4475,12 @@ function attachRef<T>(value: RefValue<T>, target: T): () => void {
3896
4475
  }
3897
4476
  }
3898
4477
 
3899
- function scheduleFlush(flush: () => void): void {
4478
+ function scheduleFlush(flush: FlushTask): void {
3900
4479
  scheduledFlushes.add(flush)
3901
4480
  if (transactionDepth === 0) drainFlushes()
3902
4481
  }
3903
4482
 
3904
- function scheduleOwnerFlush(owner: Owner, flush: () => void): void {
4483
+ function scheduleOwnerFlush(owner: Owner, flush: FlushTask): void {
3905
4484
  const connection = owner[5]
3906
4485
  if (connection === null || connection === undefined || connection.connected) {
3907
4486
  scheduleFlush(flush)
@@ -3920,7 +4499,7 @@ function drainFlushes(): void {
3920
4499
  drainingFlushes = true
3921
4500
  const publication: PublicationOperation[] = []
3922
4501
  activePublication = publication
3923
- const runs = new Map<() => void, number>()
4502
+ const runs = new Map<FlushTask, number>()
3924
4503
  let committing = false
3925
4504
  let failure: unknown
3926
4505
  let failed = false
@@ -3935,7 +4514,8 @@ function drainFlushes(): void {
3935
4514
  throw new Error(DEV ? 'Vidact compiled scopes did not stabilize' : 'V011')
3936
4515
  }
3937
4516
  runs.set(flush, runCount)
3938
- flush()
4517
+ if (typeof flush === 'function') flush()
4518
+ else flush[1]()
3939
4519
  }
3940
4520
  activePublication = null
3941
4521
  committing = true