@vidact/runtime 0.2.0-beta.6 → 0.2.0-beta.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/actions.js +1 -1
  3. package/dist/compiled/core.d.ts +17 -5
  4. package/dist/compiled/core.d.ts.map +1 -1
  5. package/dist/compiled/core.js +545 -155
  6. package/dist/compiled/core.js.map +1 -1
  7. package/dist/direct-dom.d.ts.map +1 -1
  8. package/dist/direct-dom.js +102 -11
  9. package/dist/direct-dom.js.map +1 -1
  10. package/dist/dom/events.d.ts +4 -1
  11. package/dist/dom/events.d.ts.map +1 -1
  12. package/dist/dom/events.js +66 -1
  13. package/dist/dom/events.js.map +1 -1
  14. package/dist/dom/namespace.js +1 -1
  15. package/dist/hydration-bridge.d.ts +4 -4
  16. package/dist/hydration-bridge.d.ts.map +1 -1
  17. package/dist/hydration-bridge.js.map +1 -1
  18. package/dist/hydration.d.ts +2 -2
  19. package/dist/hydration.d.ts.map +1 -1
  20. package/dist/hydration.js.map +1 -1
  21. package/dist/index.d.ts +3 -3
  22. package/dist/index.js +3 -3
  23. package/dist/keyed-list.d.ts +6 -2
  24. package/dist/keyed-list.d.ts.map +1 -1
  25. package/dist/keyed-list.js +122 -11
  26. package/dist/keyed-list.js.map +1 -1
  27. package/dist/metadata.js +1 -1
  28. package/dist/renderable.d.ts +2 -1
  29. package/dist/renderable.d.ts.map +1 -1
  30. package/dist/renderable.js +12 -1
  31. package/dist/renderable.js.map +1 -1
  32. package/dist/source-mask.d.ts +2 -1
  33. package/dist/source-mask.d.ts.map +1 -1
  34. package/dist/source-mask.js +16 -1
  35. package/dist/source-mask.js.map +1 -1
  36. package/dist/spread.js +2 -2
  37. package/dist/state-slot.d.ts +12 -1
  38. package/dist/state-slot.d.ts.map +1 -1
  39. package/dist/state-slot.js +54 -24
  40. package/dist/state-slot.js.map +1 -1
  41. package/package.json +3 -3
  42. package/src/compiled/core.ts +785 -179
  43. package/src/direct-dom.ts +136 -10
  44. package/src/dom/events.ts +86 -0
  45. package/src/hydration-bridge.ts +10 -4
  46. package/src/hydration.ts +5 -3
  47. package/src/index.ts +2 -0
  48. package/src/keyed-list.ts +168 -22
  49. package/src/renderable.ts +30 -0
  50. package/src/source-mask.ts +20 -0
  51. package/src/state-slot.ts +83 -27
@@ -1,3 +1,4 @@
1
+ import { COMPILED_DELEGATED_EVENT_INVOKE, retainCompiledEventRoot } from '../dom/events.ts'
1
2
  import {
2
3
  currentIntrinsicNamespace,
3
4
  withIntrinsicNamespace,
@@ -44,8 +45,20 @@ import {
44
45
  type RenderableProtocol,
45
46
  } from '../renderable-protocol.ts'
46
47
  import { scheduleDeferredTask, scheduleTask, type CancelScheduledTask } from '../scheduler.ts'
47
- import { intersectsSources, isEmptySources, unionSources, type SourceMask } from '../source-mask.ts'
48
- import { createStateSlot, type StateSlot } from '../state-slot.ts'
48
+ import {
49
+ forEachSource,
50
+ intersectsSources,
51
+ isEmptySources,
52
+ unionSources,
53
+ type SourceMask,
54
+ } from '../source-mask.ts'
55
+ import {
56
+ createReadOnlyStateSlot,
57
+ createStateSlot,
58
+ replaceReadOnlyStateSlot,
59
+ type ReadOnlyStateCell,
60
+ type StateSlot,
61
+ } from '../state-slot.ts'
49
62
  import { isSuspension, subscribeResource, useAsync } from './async-resource.ts'
50
63
  import {
51
64
  BINDING,
@@ -72,7 +85,28 @@ let retainedUiEnabled = typeof __VIDACT_RETAINED_UI__ !== 'undefined' && __VIDAC
72
85
  let profilingEnabled = false
73
86
  let activeOwnerCount = 0
74
87
  let createdOwnerCount = 0
88
+ let createdUpdaterCount = 0
89
+ let createdSchedulerPlanCount = 0
75
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
+ }
76
110
 
77
111
  type ContextFrame = {
78
112
  readonly context: CompiledContext<unknown>
@@ -98,7 +132,7 @@ type PortalPublication = readonly [commit: () => void, rollback: () => void]
98
132
 
99
133
  type Owner = [
100
134
  disposed: boolean,
101
- cleanups: Set<() => void>,
135
+ cleanups: Array<() => void>,
102
136
  context: ContextFrame | null,
103
137
  rootIdentity: RootIdentity,
104
138
  errorBoundary: ErrorBoundaryHandler | null,
@@ -139,11 +173,13 @@ type RetainedResource = {
139
173
  readonly phase: number
140
174
  }
141
175
 
176
+ type FlushTask = (() => void) | { readonly [1]: (sources?: SourceMask) => void }
177
+
142
178
  type RetainedConnection = {
143
179
  readonly parent: RetainedConnection | null
144
180
  readonly children: Set<RetainedConnection>
145
181
  readonly resources: Set<RetainedResource>
146
- readonly deferredFlushes: Map<() => void, CancelScheduledTask>
182
+ readonly deferredFlushes: Map<FlushTask, CancelScheduledTask>
147
183
  readonly afterFlush: Set<() => void>
148
184
  visible: boolean
149
185
  connected: boolean
@@ -161,13 +197,20 @@ type SourceOperations = readonly [
161
197
  empty: (mask: SourceMask) => boolean,
162
198
  intersects: (left: SourceMask, right: SourceMask) => boolean,
163
199
  union: (left: SourceMask, right: SourceMask) => SourceMask,
200
+ visit: (mask: SourceMask, visitor: (index: number) => void) => void,
164
201
  ]
165
202
 
166
- const wideSourceOperations: SourceOperations = [isEmptySources, intersectsSources, unionSources]
203
+ const wideSourceOperations: SourceOperations = [
204
+ isEmptySources,
205
+ intersectsSources,
206
+ unionSources,
207
+ forEachSource,
208
+ ]
167
209
  const narrowSourceOperations: SourceOperations = [
168
210
  (mask) => mask === 0,
169
211
  (left, right) => ((left as number) & (right as number)) !== 0,
170
212
  (left, right) => ((left as number) | (right as number)) >>> 0,
213
+ (mask, visitor) => forEachSource(mask, visitor),
171
214
  ]
172
215
 
173
216
  type RenderValue = CompiledRenderValue
@@ -176,9 +219,18 @@ export function deferred(render: () => CompiledRenderValue): StructuralBinding {
176
219
  return [
177
220
  STRUCTURAL,
178
221
  (parent, before) => {
179
- const hydrated = claimHydrationSlotRange(parent)
180
- if (hydrated === undefined) insertValue(parent, render(), before)
181
- 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])
182
234
  },
183
235
  'slot',
184
236
  ]
@@ -453,7 +505,7 @@ export function recordCompiledDebugValue<Value>(
453
505
  const remove = subscribeBinding(valueInput, () => {
454
506
  frame.values[index] = read()
455
507
  })
456
- owner[1].add(remove)
508
+ owner[1].push(remove)
457
509
  }
458
510
  }
459
511
 
@@ -810,6 +862,7 @@ function createPortalStructural(
810
862
  const portalOwner = withRootIdentity(rootIdentity, () =>
811
863
  withContextFrame(context, createOwner),
812
864
  )
865
+ let releaseEventRoot: (() => void) | undefined
813
866
  try {
814
867
  withOwner(portalOwner, () => insertValue(fragment, children, end))
815
868
  } catch (error) {
@@ -818,12 +871,15 @@ function createPortalStructural(
818
871
  throw error
819
872
  }
820
873
  const rollback = (): void => {
874
+ releaseEventRoot?.()
875
+ releaseEventRoot = undefined
821
876
  removeBetween(start, end)
822
877
  start.remove()
823
878
  end.remove()
824
879
  }
825
880
  const publication: PortalPublication = [
826
881
  () => {
882
+ releaseEventRoot = retainCompiledEventRoot(container)
827
883
  container.insertBefore(fragment, null)
828
884
  try {
829
885
  commitRangeRefs(start, end)
@@ -893,7 +949,7 @@ let transactionDepth = 0
893
949
  let disposalCascadeDepth = 0
894
950
  let drainingFlushes = false
895
951
  let activePublication: PublicationOperation[] | null = null
896
- const scheduledFlushes = new Set<() => void>()
952
+ const scheduledFlushes = new Set<FlushTask>()
897
953
  const disposalCascadeOwners = new Set<Owner>()
898
954
  const scopeOwners = new WeakMap<CompiledScope, Owner>()
899
955
  const scopeNamespaces = new WeakMap<CompiledScope, IntrinsicNamespace>()
@@ -903,6 +959,22 @@ const componentCommitOwners = new WeakMap<Comment, Owner>()
903
959
  const pendingInsertionCommits = new WeakMap<Owner, Set<() => void>>()
904
960
  const pendingOwnerCommits = new WeakMap<Owner, Set<() => void>>()
905
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
+ }
906
978
 
907
979
  export function createCompiledScope(): CompiledScope {
908
980
  return createScope(wideSourceOperations)
@@ -912,6 +984,171 @@ export function createNarrowCompiledScope(): CompiledScope {
912
984
  return createScope(narrowSourceOperations)
913
985
  }
914
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
+
915
1152
  export function runCompiledTransaction<T>(operation: () => T): T {
916
1153
  transactionDepth += 1
917
1154
  try {
@@ -926,12 +1163,13 @@ function createScope(operations: SourceOperations): CompiledScope {
926
1163
  const namespace = currentIntrinsicNamespace()
927
1164
  const owner = createOwner()
928
1165
  const updaters: Array<CompiledUpdater | undefined> = []
929
- const freeUpdaterIndexes: number[] = []
930
- const addedDuringFlush = new Set<CompiledUpdater>()
1166
+ let freeUpdaterIndexes: number[] | undefined
1167
+ let addedDuringFlush: Set<CompiledUpdater> | undefined
931
1168
  let batchDepth = 0
932
1169
  let flushing = false
933
1170
  let pending: SourceMask = 0
934
1171
  let cachedUpdaterOrder: number[] | undefined
1172
+ let hasWriter = false
935
1173
 
936
1174
  const flush = (): void => {
937
1175
  if (owner[0] || flushing) return
@@ -939,7 +1177,9 @@ function createScope(operations: SourceOperations): CompiledScope {
939
1177
  flushing = true
940
1178
  try {
941
1179
  let pass = 0
942
- const updaterOrder = (cachedUpdaterOrder ??= topologicalUpdaterOrder(updaters, operations))
1180
+ const updaterOrder = hasWriter
1181
+ ? (cachedUpdaterOrder ??= topologicalUpdaterOrder(updaters, operations))
1182
+ : undefined
943
1183
  while (!operations[0](pending)) {
944
1184
  pass += 1
945
1185
  if (pass > MAX_FLUSH_PASSES) {
@@ -949,11 +1189,13 @@ function createScope(operations: SourceOperations): CompiledScope {
949
1189
 
950
1190
  let active = pending
951
1191
  pending = 0
952
- 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
953
1195
  const updater = updaters[index]
954
1196
  if (
955
1197
  updater === undefined ||
956
- addedDuringFlush.has(updater) ||
1198
+ addedDuringFlush?.has(updater) ||
957
1199
  !updater[3] ||
958
1200
  !operations[1](active, updater[0])
959
1201
  ) {
@@ -966,10 +1208,10 @@ function createScope(operations: SourceOperations): CompiledScope {
966
1208
  }
967
1209
  if (updater[1] !== undefined) active = operations[2](active, updater[1])
968
1210
  }
969
- addedDuringFlush.clear()
1211
+ addedDuringFlush?.clear()
970
1212
  }
971
1213
  } finally {
972
- addedDuringFlush.clear()
1214
+ addedDuringFlush?.clear()
973
1215
  flushing = false
974
1216
  }
975
1217
  if (retainedUiEnabled) notifyRetainedFlush(owner[5] ?? null)
@@ -1002,25 +1244,28 @@ function createScope(operations: SourceOperations): CompiledScope {
1002
1244
  },
1003
1245
  true,
1004
1246
  ]
1005
- const reusableIndex = freeUpdaterIndexes.pop()
1247
+ if (DEV) createdUpdaterCount += 1
1248
+ const reusableIndex = freeUpdaterIndexes?.pop()
1006
1249
  const index = reusableIndex ?? updaters.length
1007
1250
  updaters[index] = entry
1251
+ hasWriter ||= writes !== undefined
1008
1252
  cachedUpdaterOrder = undefined
1009
- if (flushing) addedDuringFlush.add(entry)
1253
+ if (flushing) (addedDuringFlush ??= new Set()).add(entry)
1010
1254
  const remove = (): void => {
1011
1255
  if (updaters[index] !== entry) return
1012
1256
  entry[3] = false
1013
1257
  updaters[index] = undefined
1014
1258
  cachedUpdaterOrder = undefined
1015
- freeUpdaterIndexes.push(index)
1259
+ ;(freeUpdaterIndexes ??= []).push(index)
1016
1260
  }
1017
- if (activeOwner !== null && activeOwner !== owner) activeOwner[1].add(remove)
1261
+ if (activeOwner !== null && activeOwner !== owner) activeOwner[1].push(remove)
1018
1262
  return remove
1019
1263
  },
1020
1264
  (sources) => {
1021
1265
  if (owner[0] || operations[0](sources)) return
1266
+ const wasIdle = operations[0](pending)
1022
1267
  pending = operations[2](pending, sources)
1023
- if (batchDepth === 0) {
1268
+ if (wasIdle && batchDepth === 0) {
1024
1269
  if (retainedUiEnabled) scheduleOwnerFlush(owner, flush)
1025
1270
  else scheduleFlush(flush)
1026
1271
  }
@@ -1050,8 +1295,9 @@ function createScope(operations: SourceOperations): CompiledScope {
1050
1295
  if (updater !== undefined) updater[3] = false
1051
1296
  }
1052
1297
  updaters.length = 0
1053
- freeUpdaterIndexes.length = 0
1054
- addedDuringFlush.clear()
1298
+ hasWriter = false
1299
+ if (freeUpdaterIndexes !== undefined) freeUpdaterIndexes.length = 0
1300
+ addedDuringFlush?.clear()
1055
1301
  }
1056
1302
  },
1057
1303
  ]
@@ -1066,20 +1312,40 @@ function topologicalUpdaterOrder(
1066
1312
  updaters: ReadonlyArray<CompiledUpdater | undefined>,
1067
1313
  operations: SourceOperations,
1068
1314
  ): number[] {
1069
- const activeIndexes = updaters.flatMap((updater, index) =>
1070
- updater === undefined || !updater[3] ? [] : [index],
1071
- )
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
1325
+ const readersBySource = new Map<number, number[]>()
1326
+ for (const readerIndex of activeIndexes) {
1327
+ operations[3](updaters[readerIndex]![0], (sourceIndex) => {
1328
+ const readers = readersBySource.get(sourceIndex)
1329
+ if (readers === undefined) readersBySource.set(sourceIndex, [readerIndex])
1330
+ else readers.push(readerIndex)
1331
+ })
1332
+ }
1072
1333
  const edges = new Map<number, number[]>()
1073
1334
  for (const writerIndex of activeIndexes) {
1074
1335
  const writes = updaters[writerIndex]![1]
1075
1336
  if (writes === undefined) continue
1076
- const readers: number[] = []
1077
- for (const readerIndex of activeIndexes) {
1078
- if (writerIndex !== readerIndex && operations[1](writes, updaters[readerIndex]![0])) {
1079
- readers.push(readerIndex)
1337
+ const readers = new Set<number>()
1338
+ operations[3](writes, (sourceIndex) => {
1339
+ for (const readerIndex of readersBySource.get(sourceIndex) ?? []) {
1340
+ if (writerIndex !== readerIndex) readers.add(readerIndex)
1080
1341
  }
1342
+ })
1343
+ if (readers.size > 0) {
1344
+ edges.set(
1345
+ writerIndex,
1346
+ [...readers].toSorted((left, right) => left - right),
1347
+ )
1081
1348
  }
1082
- if (readers.length > 0) edges.set(writerIndex, readers)
1083
1349
  }
1084
1350
 
1085
1351
  const visitIndexes = new Int32Array(updaters.length).fill(-1)
@@ -1213,7 +1479,7 @@ export function createCompiledMemo<T>(
1213
1479
  removeUpdater()
1214
1480
  throw new Error(DEV ? 'createCompiledMemo received an unknown scope' : 'V003')
1215
1481
  }
1216
- owner[1].add(removeUpdater)
1482
+ owner[1].push(removeUpdater)
1217
1483
  return slot
1218
1484
  }
1219
1485
 
@@ -1337,7 +1603,7 @@ export function createCompiledExternalStore<T>(
1337
1603
  removeStoreUpdater()
1338
1604
  throw error
1339
1605
  }
1340
- owner[1].add(() => {
1606
+ owner[1].push(() => {
1341
1607
  removeStoreUpdater()
1342
1608
  deactivateStore()
1343
1609
  })
@@ -1355,7 +1621,7 @@ export function createCompiledExternalStore<T>(
1355
1621
  disposeResource()
1356
1622
  throw error
1357
1623
  }
1358
- owner[1].add(() => {
1624
+ owner[1].push(() => {
1359
1625
  removeStoreUpdater()
1360
1626
  disposeResource()
1361
1627
  })
@@ -1457,6 +1723,42 @@ export function useCallback<T extends Function>(callback: T, _dependencies: read
1457
1723
  return callback
1458
1724
  }
1459
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
+
1460
1762
  export function createCompiledProp<T>(
1461
1763
  scope: CompiledScope,
1462
1764
  sourceMask: SourceMask,
@@ -1467,7 +1769,9 @@ export function createCompiledProp<T>(
1467
1769
  const upstream = isCompiledBinding(input) ? input : undefined
1468
1770
  const read = (): T => {
1469
1771
  const value = upstream === undefined ? (input as T) : upstream[1]()
1470
- return value === undefined && fallback !== undefined ? fallback() : value
1772
+ return sanitizeCompiledPropValue(
1773
+ value === undefined && fallback !== undefined ? fallback() : value,
1774
+ )
1471
1775
  }
1472
1776
  const slot = createStateSlot<T>(scope[1], sourceMask, read(), assertWritable)
1473
1777
  if (upstream !== undefined) {
@@ -1476,7 +1780,7 @@ export function createCompiledProp<T>(
1476
1780
  if (owner === undefined) {
1477
1781
  throw new Error(DEV ? 'createCompiledProp received an unknown scope' : 'V003')
1478
1782
  }
1479
- owner[1].add(remove)
1783
+ owner[1].push(remove)
1480
1784
  }
1481
1785
  return slot
1482
1786
  }
@@ -1492,7 +1796,10 @@ export function createCompiledRestProp(
1492
1796
  Object.entries(input).filter(([name]) => !excluded.has(name))
1493
1797
  const read = (): Record<string, unknown> =>
1494
1798
  Object.fromEntries(
1495
- 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
+ ]),
1496
1803
  )
1497
1804
  const slot = createStateSlot(scope[1], sourceMask, read(), stateWriteGuard(scope))
1498
1805
  const publish = (): void => {
@@ -1519,7 +1826,7 @@ export function createCompiledRestProp(
1519
1826
  if (owner === undefined) {
1520
1827
  throw new Error(DEV ? 'createCompiledRestProp received an unknown scope' : 'V003')
1521
1828
  }
1522
- owner[1].add(() => {
1829
+ owner[1].push(() => {
1523
1830
  for (const remove of removers) remove()
1524
1831
  })
1525
1832
  return slot
@@ -1579,8 +1886,9 @@ export function compiledEvent<Arguments extends unknown[]>(
1579
1886
  throw new Error(DEV ? 'compiledEvent received an unknown scope' : 'V003')
1580
1887
  }
1581
1888
  const errorOwner = activeOwner ?? owner
1582
- return (...arguments_) =>
1583
- runOwnerTask(errorOwner, () =>
1889
+ const listener = (...arguments_: Arguments) => {
1890
+ if (errorOwner[0]) return
1891
+ return runOwnerTask(errorOwner, () =>
1584
1892
  withOwner(errorOwner, () =>
1585
1893
  scope[2](() => {
1586
1894
  const current = isCompiledBinding(handler) ? handler[1]() : handler
@@ -1589,6 +1897,43 @@ export function compiledEvent<Arguments extends unknown[]>(
1589
1897
  }),
1590
1898
  ),
1591
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
+ )
1592
1937
  }
1593
1938
 
1594
1939
  export interface CompiledTaskController {
@@ -1690,7 +2035,12 @@ export function choose(
1690
2035
  return structural(
1691
2036
  scope,
1692
2037
  (parent, before) => {
1693
- 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
1694
2044
  let selected = -1
1695
2045
  let branchOwner: Owner | null = null
1696
2046
  let branchNodes: readonly Node[] = []
@@ -1700,15 +2050,28 @@ export function choose(
1700
2050
  const next =
1701
2051
  mode === 'not-nullish' ? (value === null || value === undefined ? 1 : 0) : value ? 0 : 1
1702
2052
  if (next === selected) return
1703
- const currentParent = rangeParent(start, end, 'choice block')
1704
2053
  const nextOwner = createOwner()
1705
- const [fragment, nodes] =
1706
- hydrated && selected === -1
1707
- ? withHydrationInsertion(currentParent, end, () =>
1708
- stageRender(next === 0 ? consequent : alternate, nextOwner),
1709
- )
1710
- : stageRender(next === 0 ? consequent : alternate, nextOwner)
1711
- 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
+ }
1712
2075
  try {
1713
2076
  commitPublishedNodes(nodes)
1714
2077
  } catch (error) {
@@ -1736,8 +2099,10 @@ export function choose(
1736
2099
  try {
1737
2100
  disposePublished(branchOwner, branchNodes)
1738
2101
  } finally {
1739
- start.remove()
1740
- end.remove()
2102
+ if (ownsMarkers) {
2103
+ start.remove()
2104
+ end.remove()
2105
+ }
1741
2106
  }
1742
2107
  })
1743
2108
  },
@@ -1824,9 +2189,21 @@ export function keyed<T, K>(
1824
2189
  values: () => readonly T[],
1825
2190
  key: (value: T, index: number) => K,
1826
2191
  render: (value: StateSlot<T>, index: StateSlot<number>, itemScope: CompiledScope) => RenderValue,
1827
- additionalScope?: CompiledScope,
2192
+ trackIndexOrAdditionalScope?: boolean | CompiledScope,
2193
+ additionalScopeOrReads?: CompiledScope | SourceMask,
1828
2194
  additionalReads?: SourceMask,
1829
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)
1830
2207
  return structural(
1831
2208
  scope,
1832
2209
  (parent, before) => {
@@ -1837,27 +2214,56 @@ export function keyed<T, K>(
1837
2214
  {
1838
2215
  key,
1839
2216
  render(value, index) {
1840
- const owner = createOwner()
1841
- const itemScope = createNarrowCompiledScope()
1842
- const valueSlot = createCompiledState(itemScope, itemSource, value)
1843
- 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
1844
2229
  try {
1845
2230
  const nodes = withOwner(owner, () => {
1846
- onCleanup(itemScope[3])
1847
- 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)
1848
2241
  })
1849
2242
  return [
1850
2243
  nodes,
1851
2244
  (nextValue: T, nextIndex: number) => {
2245
+ if (
2246
+ Object.is(valueSlot.get(), nextValue) &&
2247
+ (!trackIndex || indexSlot!.get() === nextIndex)
2248
+ ) {
2249
+ return
2250
+ }
1852
2251
  itemScope[2](() => {
1853
- valueSlot.set(nextValue)
1854
- 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
+ }
1855
2261
  })
1856
2262
  },
1857
- () => disposeOwner(owner),
2263
+ itemScope,
1858
2264
  ] as const
1859
2265
  } catch (error) {
1860
- disposeOwner(owner)
2266
+ itemScope[3]()
1861
2267
  throw error
1862
2268
  }
1863
2269
  },
@@ -1874,7 +2280,13 @@ export function keyed<T, K>(
1874
2280
  }
1875
2281
  }
1876
2282
  update()
1877
- const removeUpdater = subscribe(scope, reads, update, additionalScope, additionalReads)
2283
+ const removeUpdater = subscribe(
2284
+ scope,
2285
+ reads,
2286
+ update,
2287
+ additionalScope,
2288
+ resolvedAdditionalReads,
2289
+ )
1878
2290
  onCleanup(() => {
1879
2291
  removeUpdater()
1880
2292
  list.dispose()
@@ -1962,7 +2374,7 @@ export function compiledRoot(
1962
2374
  componentCommitOwners.set(end, owner)
1963
2375
  const fragment = document.createDocumentFragment()
1964
2376
  if (hydratedRange === undefined) fragment.append(start, end)
1965
- owner[1].add(() => {
2377
+ owner[1].push(() => {
1966
2378
  try {
1967
2379
  removeBetween(start, end)
1968
2380
  } finally {
@@ -2026,7 +2438,7 @@ export function adoptCompiledRoot(root: unknown): void {
2026
2438
  if (range === undefined) return
2027
2439
  const owner = scopeOwners.get(range[2])
2028
2440
  if (activeOwner !== null && owner !== undefined && activeOwner !== owner) {
2029
- activeOwner[1].add(range[2][3])
2441
+ activeOwner[1].push(range[2][3])
2030
2442
  }
2031
2443
  }
2032
2444
 
@@ -2067,7 +2479,9 @@ export function queueElementRef(element: Element, value: unknown): void {
2067
2479
  if (!isRefValue(value)) {
2068
2480
  throw new TypeError(DEV ? 'ref must be a callback or an object with current' : 'V006')
2069
2481
  }
2070
- pendingRefs.set(element, [activeOwner, value])
2482
+ const pending: PendingRef = [activeOwner, value]
2483
+ setPendingRef(element, pending)
2484
+ activeOwner?.[1].push(() => deletePendingRef(element, pending))
2071
2485
  }
2072
2486
 
2073
2487
  export function useImperativeHandle<T>(
@@ -2091,7 +2505,7 @@ export function useImperativeHandle<T>(
2091
2505
  DEV ? 'reactive useImperativeHandle dependencies are not supported yet' : 'V014',
2092
2506
  )
2093
2507
  }
2094
- queueOwnerCommit(owner, () => owner[1].add(attachRef(ref, create())))
2508
+ queueOwnerCommit(owner, () => owner[1].push(attachRef(ref, create())))
2095
2509
  }
2096
2510
 
2097
2511
  type EffectResult = void | (() => void)
@@ -2128,7 +2542,7 @@ export function useInsertionEffect(
2128
2542
  ? readProfiledEffectCleanup(owner, create)
2129
2543
  : readEffectCleanup(create())
2130
2544
  })
2131
- lifetimeOwner[1].add(() => cleanup())
2545
+ lifetimeOwner[1].push(() => cleanup())
2132
2546
  return
2133
2547
  }
2134
2548
  let cleanup = noop
@@ -2155,7 +2569,7 @@ export function useInsertionEffect(
2155
2569
  queueInsertionCommit(owner, () => {
2156
2570
  if (!lifetimeOwner[0]) activate()
2157
2571
  })
2158
- lifetimeOwner[1].add(disposeResource)
2572
+ lifetimeOwner[1].push(disposeResource)
2159
2573
  }
2160
2574
 
2161
2575
  export function useEffect(create: () => EffectResult, dependencies?: readonly unknown[]): void {
@@ -2195,7 +2609,7 @@ function registerEffect(
2195
2609
  run()
2196
2610
  }
2197
2611
  })
2198
- lifetimeOwner[1].add(() => {
2612
+ lifetimeOwner[1].push(() => {
2199
2613
  generation += 1
2200
2614
  if (passive) (DEV ? scheduleTask : queueMicrotask)(() => runOwnerTask(lifetimeOwner, cleanup))
2201
2615
  else cleanup()
@@ -2244,7 +2658,7 @@ function registerEffect(
2244
2658
  queueOwnerCommit(owner, () => {
2245
2659
  if (!lifetimeOwner[0]) activate()
2246
2660
  })
2247
- lifetimeOwner[1].add(disposeResource)
2661
+ lifetimeOwner[1].push(disposeResource)
2248
2662
  }
2249
2663
 
2250
2664
  export function compiledLayoutEffect(
@@ -2293,7 +2707,7 @@ export function compiledInsertionEffect(
2293
2707
  const removeUpdater = subscribe(scope, reads, () =>
2294
2708
  stagePublication([noop, noop, undefined, run, -20]),
2295
2709
  )
2296
- lifetimeOwner[1].add(() => {
2710
+ lifetimeOwner[1].push(() => {
2297
2711
  removeUpdater()
2298
2712
  cleanup()
2299
2713
  })
@@ -2346,7 +2760,7 @@ export function compiledInsertionEffect(
2346
2760
  const removeUpdater = subscribe(scope, reads, () => {
2347
2761
  if (connected) stagePublication([noop, noop, undefined, run, -20])
2348
2762
  })
2349
- lifetimeOwner[1].add(() => {
2763
+ lifetimeOwner[1].push(() => {
2350
2764
  removeUpdater()
2351
2765
  disposeResource()
2352
2766
  })
@@ -2402,7 +2816,7 @@ export function compiledEffect(
2402
2816
  if (passive) schedule()
2403
2817
  else stagePublication([noop, noop, undefined, run, 20])
2404
2818
  })
2405
- lifetimeOwner[1].add(() => {
2819
+ lifetimeOwner[1].push(() => {
2406
2820
  generation += 1
2407
2821
  removeUpdater()
2408
2822
  if (passive) (DEV ? scheduleTask : queueMicrotask)(() => runOwnerTask(lifetimeOwner, cleanup))
@@ -2475,7 +2889,7 @@ export function compiledEffect(
2475
2889
  if (passive) schedule()
2476
2890
  else stagePublication([noop, noop, undefined, run, 20])
2477
2891
  })
2478
- lifetimeOwner[1].add(() => {
2892
+ lifetimeOwner[1].push(() => {
2479
2893
  removeUpdater()
2480
2894
  disposeResource()
2481
2895
  })
@@ -2594,7 +3008,7 @@ export function compiledImperativeHandle<T>(
2594
3008
  ])
2595
3009
  }
2596
3010
  const removeUpdater = subscribe(scope, reads, update)
2597
- owner[1].add(() => {
3011
+ owner[1].push(() => {
2598
3012
  removeUpdater()
2599
3013
  cleanup()
2600
3014
  })
@@ -2605,6 +3019,7 @@ function queueOwnerCommit(owner: Owner, commit: () => void): void {
2605
3019
  if (commits === undefined) {
2606
3020
  commits = new Set()
2607
3021
  pendingOwnerCommits.set(owner, commits)
3022
+ pendingOwnerCommitCount += 1
2608
3023
  }
2609
3024
  commits.add(commit)
2610
3025
  }
@@ -2614,6 +3029,7 @@ function queueInsertionCommit(owner: Owner, commit: () => void): void {
2614
3029
  if (commits === undefined) {
2615
3030
  commits = new Set()
2616
3031
  pendingInsertionCommits.set(owner, commits)
3032
+ pendingInsertionOwnerCount += 1
2617
3033
  }
2618
3034
  commits.add(commit)
2619
3035
  }
@@ -2644,7 +3060,7 @@ export function mountCompiledRef(element: Element, value: CompiledBinding<unknow
2644
3060
  // oxlint-disable-next-line unicorn/consistent-function-scoping -- This placeholder is replaced when the ref attaches.
2645
3061
  let cleanup = noop
2646
3062
  const pending: PendingRef = [activeOwner, current, (attached) => (cleanup = attached)]
2647
- pendingRefs.set(element, pending)
3063
+ setPendingRef(element, pending)
2648
3064
 
2649
3065
  const removeUpdater = subscribeBinding(value, () => {
2650
3066
  const next = value[1]()
@@ -2703,7 +3119,7 @@ export function mountCompiledRef(element: Element, value: CompiledBinding<unknow
2703
3119
  })
2704
3120
  onCleanup(() => {
2705
3121
  removeUpdater()
2706
- if (pendingRefs.get(element) === pending) pendingRefs.delete(element)
3122
+ deletePendingRef(element, pending)
2707
3123
  cleanup()
2708
3124
  })
2709
3125
  }
@@ -2788,6 +3204,7 @@ export function mountCompiled(
2788
3204
  throw new Error(DEV ? 'mountCompiled received an unknown component result' : 'V007')
2789
3205
  }
2790
3206
  const previous = [...host.childNodes]
3207
+ const releaseEventRoot = retainCompiledEventRoot(host)
2791
3208
  try {
2792
3209
  root[1](host, previous[0] ?? null)
2793
3210
  const rootOwner = scopeOwners.get(range[2])
@@ -2803,6 +3220,7 @@ export function mountCompiled(
2803
3220
  for (const node of previous) host.removeChild(node)
2804
3221
  }
2805
3222
  } catch (error) {
3223
+ releaseEventRoot()
2806
3224
  try {
2807
3225
  range[2][3]()
2808
3226
  } catch {
@@ -2812,8 +3230,17 @@ export function mountCompiled(
2812
3230
  rootIdentity.onUncaughtError(error)
2813
3231
  return { dispose: noop }
2814
3232
  }
3233
+ let disposed = false
2815
3234
  return {
2816
- 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
+ },
2817
3244
  }
2818
3245
  }
2819
3246
 
@@ -2833,12 +3260,43 @@ export function mountCompiledBinding(parent: Node, value: CompiledBinding<unknow
2833
3260
  mountCompiledBindingBefore(parent, value, null)
2834
3261
  }
2835
3262
 
2836
- export function mountCompiledProp(
2837
- value: CompiledBinding<unknown>,
2838
- 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,
2839
3297
  ): void {
2840
3298
  let current = value[1]()
2841
- let cleanup = apply(current)
3299
+ let cleanup = applyCompiledPropValue(apply, element, name, current)
2842
3300
  const removeUpdater = subscribeBinding(value, () => {
2843
3301
  const next = value[1]()
2844
3302
  if (Object.is(next, current)) return
@@ -2848,10 +3306,10 @@ export function mountCompiledProp(
2848
3306
  stagePublication([
2849
3307
  () => {
2850
3308
  try {
2851
- nextCleanup = apply(next)
3309
+ nextCleanup = applyCompiledPropValue(apply, element, name, next)
2852
3310
  } catch (error) {
2853
3311
  try {
2854
- apply(previous)
3312
+ applyCompiledPropValue(apply, element, name, previous)
2855
3313
  } catch {
2856
3314
  // Preserve the setter error that aborted publication.
2857
3315
  }
@@ -2865,7 +3323,7 @@ export function mountCompiledProp(
2865
3323
  () => {
2866
3324
  if (!committedNext) return
2867
3325
  nextCleanup?.()
2868
- cleanup = apply(previous)
3326
+ cleanup = applyCompiledPropValue(apply, element, name, previous)
2869
3327
  current = previous
2870
3328
  committedNext = false
2871
3329
  },
@@ -2942,14 +3400,40 @@ function structuralRange(
2942
3400
  parent: Node,
2943
3401
  before: Node | null,
2944
3402
  label: string,
2945
- ): 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
2946
3430
  const hydrated = claimHydrationSlotRange(parent)
2947
- if (hydrated !== undefined) return [hydrated[0], hydrated[1], true]
3431
+ if (hydrated !== undefined) return [hydrated[0], hydrated[1], true, true]
2948
3432
  const start = document.createComment(DEV ? `vidact:${label}` : '')
2949
3433
  const end = document.createComment(DEV ? `/vidact:${label}` : '')
2950
3434
  parent.insertBefore(start, before)
2951
3435
  parent.insertBefore(end, before)
2952
- return [start, end, false]
3436
+ return [start, end, false, true]
2953
3437
  }
2954
3438
 
2955
3439
  function subscribe(
@@ -2959,12 +3443,29 @@ function subscribe(
2959
3443
  additionalScope?: CompiledScope,
2960
3444
  additionalReads?: SourceMask,
2961
3445
  ): () => void {
2962
- const removers = [scope[0](reads, run)]
2963
- if (additionalScope !== undefined && additionalReads !== undefined) {
2964
- removers.push(additionalScope[0](additionalReads, () => withScopeNamespace(scope, run)))
2965
- }
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
2966
3466
  return () => {
2967
- for (const remove of removers) remove()
3467
+ first()
3468
+ second()
2968
3469
  }
2969
3470
  }
2970
3471
 
@@ -3007,7 +3508,7 @@ function createOwner(
3007
3508
  if (profilingEnabled) {
3008
3509
  const owner: Owner = [
3009
3510
  false,
3010
- new Set(),
3511
+ [],
3011
3512
  context,
3012
3513
  rootIdentity,
3013
3514
  boundary,
@@ -3018,8 +3519,8 @@ function createOwner(
3018
3519
  return owner
3019
3520
  }
3020
3521
  const owner: Owner = retainedUiEnabled
3021
- ? [false, new Set(), context, rootIdentity, boundary, retainedConnection]
3022
- : [false, new Set(), context, rootIdentity, boundary]
3522
+ ? [false, [], context, rootIdentity, boundary, retainedConnection]
3523
+ : [false, [], context, rootIdentity, boundary]
3023
3524
  if (DEV) owner[7] = activeConstructionOwner?.[7] ?? activeProfileName
3024
3525
  return owner
3025
3526
  }
@@ -3362,7 +3863,7 @@ function withRootIdentity<T>(rootIdentity: RootIdentity | null, operation: () =>
3362
3863
  }
3363
3864
 
3364
3865
  function onCleanup(cleanup: () => void): void {
3365
- activeOwner?.[1].add(cleanup)
3866
+ activeOwner?.[1].push(cleanup)
3366
3867
  }
3367
3868
 
3368
3869
  function disposeOwner(owner: Owner): void {
@@ -3371,21 +3872,21 @@ function disposeOwner(owner: Owner): void {
3371
3872
  disposalCascadeDepth += 1
3372
3873
  disposalCascadeOwners.add(owner)
3373
3874
  if (DEV) activeOwnerCount -= 1
3374
- pendingInsertionCommits.delete(owner)
3375
- pendingOwnerCommits.delete(owner)
3376
- const cleanups = [...owner[1]]
3377
- owner[1].clear()
3875
+ if (pendingInsertionCommits.delete(owner)) pendingInsertionOwnerCount -= 1
3876
+ if (pendingOwnerCommits.delete(owner)) pendingOwnerCommitCount -= 1
3378
3877
  let firstError: unknown
3379
3878
  let hasError = false
3380
3879
  try {
3381
- 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()
3382
3882
  try {
3383
- cleanups[index]?.()
3883
+ cleanup?.()
3384
3884
  } catch (error) {
3385
3885
  if (!hasError) firstError = error
3386
3886
  hasError = true
3387
3887
  }
3388
3888
  }
3889
+ owner[1].length = 0
3389
3890
  } finally {
3390
3891
  disposalCascadeDepth -= 1
3391
3892
  if (disposalCascadeDepth === 0) disposalCascadeOwners.clear()
@@ -3397,8 +3898,50 @@ function disposeOwner(owner: Owner): void {
3397
3898
  export function readCompiledOwnerMetrics(): {
3398
3899
  readonly active: number
3399
3900
  readonly created: number
3901
+ readonly updaters: number
3902
+ readonly schedulerPlans: number
3400
3903
  } {
3401
- 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)
3402
3945
  }
3403
3946
 
3404
3947
  function mountCompiledBindingBefore(
@@ -3406,144 +3949,160 @@ function mountCompiledBindingBefore(
3406
3949
  value: CompiledBinding<unknown>,
3407
3950
  before: Node | null,
3408
3951
  ): void {
3409
- const unset = Symbol(DEV ? 'Vidact.UnsetBinding' : undefined)
3410
3952
  const initial = value[1]()
3411
3953
  const scalarInitial = isScalarRenderValue(initial)
3954
+ const structuralInitial = ownedStructuralBinding(initial)
3412
3955
  const hydratedTextRange = scalarInitial
3413
3956
  ? claimHydrationTextRange(parent, toText(initial))
3414
3957
  : undefined
3415
3958
  const borrowedHydrationRange =
3416
3959
  isHydrating() && !scalarInitial
3417
- ? borrowHydrationSlotRange(parent, isStructuralBinding(initial) && initial[2] === 'slot')
3960
+ ? borrowHydrationSlotRange(parent, structuralInitial?.[2] === 'slot')
3418
3961
  : undefined
3419
3962
  const hydratedStructuralRange =
3420
3963
  isHydrating() && !scalarInitial
3421
3964
  ? (borrowedHydrationRange ?? claimHydrationSlotRange(parent))
3422
3965
  : undefined
3423
3966
  const hydratedRange = hydratedTextRange ?? hydratedStructuralRange
3424
- const start = hydratedRange?.[0] ?? document.createComment(DEV ? 'vidact:binding' : '')
3425
- const end = hydratedRange?.[1] ?? document.createComment(DEV ? '/vidact:binding' : '')
3426
- 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' : '')
3427
3973
  parent.insertBefore(start, before)
3428
3974
  parent.insertBefore(end, before)
3429
3975
  }
3430
- let current: unknown = hydratedTextRange === undefined ? unset : initial
3431
- let currentOwner: Owner | null = null
3432
- let text: Text | null = hydratedTextRange?.[2] ?? null
3433
- const stableRenderable = (renderable: RenderableProtocol): unknown => {
3434
- const identity = renderableIdentity(renderable)
3435
- let previous = renderablePropsSnapshot(renderable)
3436
- const input = binding(
3437
- value[2],
3438
- value[3],
3439
- () => {
3440
- const next = value[1]()
3441
- if (!isRenderableProtocol(next) || !Object.is(renderableIdentity(next), identity)) {
3442
- return previous
3443
- }
3444
- previous = renderablePropsSnapshot(next)
3445
- return previous
3446
- },
3447
- value[4],
3448
- value[5],
3449
- )
3450
- 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,
3451
3982
  }
3452
-
3453
- const clear = (): void => {
3454
- const owner = currentOwner
3455
- currentOwner = null
3456
- text = null
3457
- disposeRange(owner, start, end)
3983
+ if (markerlessScalar) {
3984
+ state.text = document.createTextNode(toText(initial))
3985
+ parent.insertBefore(state.text, before)
3458
3986
  }
3459
3987
  const update = (): void => {
3460
3988
  const next = value[1]()
3461
- if (current !== unset && Object.is(next, current)) return
3989
+ if (state.current !== UNSET_BINDING && Object.is(next, state.current)) return
3462
3990
  if (
3463
- current !== unset &&
3464
- isRenderableProtocol(current) &&
3991
+ state.current !== UNSET_BINDING &&
3992
+ isRenderableProtocol(state.current) &&
3465
3993
  isRenderableProtocol(next) &&
3466
- canReconcileRenderable(current) &&
3994
+ canReconcileRenderable(state.current) &&
3467
3995
  canReconcileRenderable(next) &&
3468
- Object.is(renderableIdentity(current), renderableIdentity(next))
3996
+ Object.is(renderableIdentity(state.current), renderableIdentity(next))
3469
3997
  ) {
3470
- current = next
3998
+ state.current = next
3471
3999
  return
3472
4000
  }
3473
- const currentParent = rangeParent(start, end, 'binding range')
3474
-
3475
4001
  if (isScalarRenderValue(next)) {
3476
4002
  const content = toText(next)
3477
- if (text !== null) {
3478
- const target = text
4003
+ if (state.text !== null) {
4004
+ const target = state.text
3479
4005
  const previousContent = target.data
3480
- const previous = current
4006
+ const previous = state.current
3481
4007
  stagePublication([
3482
4008
  () => {
3483
4009
  if (target.data !== content) target.data = content
3484
- current = next
4010
+ state.current = next
3485
4011
  },
3486
4012
  () => {
3487
4013
  if (target.data !== previousContent) target.data = previousContent
3488
- current = previous
4014
+ state.current = previous
3489
4015
  },
3490
4016
  ])
3491
4017
  return
3492
4018
  }
3493
- clear()
3494
- text = document.createTextNode(content)
3495
- currentParent.insertBefore(text, end)
3496
- 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
3497
4025
  return
3498
4026
  }
3499
4027
 
4028
+ const [rangeStart, rangeEnd] = ensureMountedBindingRange(state)
4029
+ const currentParent = rangeParent(rangeStart, rangeEnd, 'binding range')
3500
4030
  const nextOwner = createOwner()
3501
4031
  const stagedValue =
3502
4032
  isRenderableProtocol(next) && canReconcileRenderable(next)
3503
- ? stableRenderable(next)
4033
+ ? stableRenderableBinding(value, next)
3504
4034
  : (next as RenderValue)
3505
4035
  const [fragment, staged] = stageValue(stagedValue as RenderValue, nextOwner)
3506
4036
  try {
3507
- clear()
4037
+ clearMountedBinding(state)
3508
4038
  } catch (error) {
3509
4039
  disposeOwner(nextOwner)
3510
4040
  throw error
3511
4041
  }
3512
- currentOwner = nextOwner
3513
- currentParent.insertBefore(fragment, end)
4042
+ state.currentOwner = nextOwner
4043
+ currentParent.insertBefore(fragment, rangeEnd)
3514
4044
  commitPublishedNodes(staged)
3515
- current = next
4045
+ state.current = next
3516
4046
  }
3517
4047
 
3518
4048
  if (hydratedStructuralRange !== undefined) {
4049
+ const rangeEnd = hydratedStructuralRange[1]
3519
4050
  const nextOwner = createOwner()
3520
- const [fragment, staged] = withHydrationInsertion(parent, end, () =>
4051
+ const [fragment, staged] = withHydrationInsertion(parent, rangeEnd, () =>
3521
4052
  stageRender(
3522
4053
  () =>
3523
4054
  (isRenderableProtocol(initial) && canReconcileRenderable(initial)
3524
- ? stableRenderable(initial)
4055
+ ? stableRenderableBinding(value, initial)
3525
4056
  : initial) as RenderValue,
3526
4057
  nextOwner,
3527
4058
  ),
3528
4059
  )
3529
- parent.insertBefore(fragment, end)
4060
+ parent.insertBefore(fragment, rangeEnd)
3530
4061
  commitPublishedNodes(staged)
3531
- currentOwner = nextOwner
3532
- current = initial
3533
- } else if (hydratedRange === undefined) {
4062
+ state.currentOwner = nextOwner
4063
+ state.current = initial
4064
+ } else if (hydratedRange === undefined && !markerlessScalar) {
3534
4065
  update()
3535
4066
  }
3536
4067
  const removeUpdater = subscribeBinding(value, update)
3537
4068
  onCleanup(() => {
3538
4069
  removeUpdater()
3539
- clear()
4070
+ if (state.start === null || state.end === null) {
4071
+ state.text?.remove()
4072
+ state.text = null
4073
+ return
4074
+ }
4075
+ clearMountedBinding(state)
3540
4076
  if (borrowedHydrationRange === undefined) {
3541
- start.remove()
3542
- end.remove()
4077
+ state.start.remove()
4078
+ state.end.remove()
3543
4079
  }
3544
4080
  })
3545
4081
  }
3546
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
+
3547
4106
  function materialize(value: RenderValue): Node[] {
3548
4107
  const insertionPoint = hydrationInsertionPoint()
3549
4108
  if (insertionPoint !== undefined) {
@@ -3617,6 +4176,40 @@ function stageRender(
3617
4176
  }
3618
4177
  }
3619
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
+
3620
4213
  function insertValue(
3621
4214
  parent: Node,
3622
4215
  value: RenderValue,
@@ -3766,10 +4359,13 @@ function isRefValue(value: unknown): value is RefValue {
3766
4359
  }
3767
4360
 
3768
4361
  function claimPendingRefOwners(root: Node): void {
4362
+ if (pendingRefCount === 0) return
3769
4363
  visitNodes(root, (node) => {
3770
4364
  if (!(node instanceof Element)) return
3771
4365
  const pending = pendingRefs.get(node)
3772
- 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))
3773
4369
  })
3774
4370
  }
3775
4371
 
@@ -3787,9 +4383,15 @@ function commitPublishedNodesNow(nodes: readonly Node[]): void {
3787
4383
  if (publicationRoot instanceof DocumentFragment && !(publicationRoot instanceof ShadowRoot)) {
3788
4384
  return
3789
4385
  }
3790
- for (const node of nodes) commitNodeInsertions(node)
3791
- for (const node of nodes) commitNodeRefs(node)
3792
- 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
+ }
3793
4395
  }
3794
4396
 
3795
4397
  function commitNodeInsertions(root: Node): void {
@@ -3803,12 +4405,12 @@ function commitNodeRefs(root: Node): void {
3803
4405
  if (node instanceof Element) {
3804
4406
  const pending = pendingRefs.get(node)
3805
4407
  if (pending !== undefined) {
3806
- pendingRefs.delete(node)
4408
+ deletePendingRef(node, pending)
3807
4409
  const owner = pending[0] ?? activeOwner
3808
4410
  commitContextInsertions(owner?.[2] ?? null)
3809
4411
  commitOwnerInsertions(owner ?? undefined)
3810
4412
  const cleanup = attachRef(pending[1], node)
3811
- if (pending[2] === undefined) owner?.[1].add(cleanup)
4413
+ if (pending[2] === undefined) owner?.[1].push(cleanup)
3812
4414
  else pending[2](cleanup)
3813
4415
  }
3814
4416
  }
@@ -3832,6 +4434,7 @@ function commitOwnerInsertions(owner: Owner | undefined): void {
3832
4434
  const commits = pendingInsertionCommits.get(owner)
3833
4435
  if (commits === undefined) return
3834
4436
  pendingInsertionCommits.delete(owner)
4437
+ pendingInsertionOwnerCount -= 1
3835
4438
  for (const commit of commits) commit()
3836
4439
  }
3837
4440
 
@@ -3840,6 +4443,7 @@ function commitOwnerResources(owner: Owner | undefined): void {
3840
4443
  const commits = pendingOwnerCommits.get(owner)
3841
4444
  if (commits === undefined) return
3842
4445
  pendingOwnerCommits.delete(owner)
4446
+ pendingOwnerCommitCount -= 1
3843
4447
  for (const commit of commits) commit()
3844
4448
  }
3845
4449
 
@@ -3854,6 +4458,7 @@ function commitRangeRefs(start: Node, end: Node): void {
3854
4458
  }
3855
4459
 
3856
4460
  function visitNodes(root: Node, visit: (node: Node) => void): void {
4461
+ visitedPublishedNodeCount += 1
3857
4462
  visit(root)
3858
4463
  for (const child of root.childNodes) visitNodes(child, visit)
3859
4464
  }
@@ -3870,12 +4475,12 @@ function attachRef<T>(value: RefValue<T>, target: T): () => void {
3870
4475
  }
3871
4476
  }
3872
4477
 
3873
- function scheduleFlush(flush: () => void): void {
4478
+ function scheduleFlush(flush: FlushTask): void {
3874
4479
  scheduledFlushes.add(flush)
3875
4480
  if (transactionDepth === 0) drainFlushes()
3876
4481
  }
3877
4482
 
3878
- function scheduleOwnerFlush(owner: Owner, flush: () => void): void {
4483
+ function scheduleOwnerFlush(owner: Owner, flush: FlushTask): void {
3879
4484
  const connection = owner[5]
3880
4485
  if (connection === null || connection === undefined || connection.connected) {
3881
4486
  scheduleFlush(flush)
@@ -3894,7 +4499,7 @@ function drainFlushes(): void {
3894
4499
  drainingFlushes = true
3895
4500
  const publication: PublicationOperation[] = []
3896
4501
  activePublication = publication
3897
- const runs = new Map<() => void, number>()
4502
+ const runs = new Map<FlushTask, number>()
3898
4503
  let committing = false
3899
4504
  let failure: unknown
3900
4505
  let failed = false
@@ -3909,7 +4514,8 @@ function drainFlushes(): void {
3909
4514
  throw new Error(DEV ? 'Vidact compiled scopes did not stabilize' : 'V011')
3910
4515
  }
3911
4516
  runs.set(flush, runCount)
3912
- flush()
4517
+ if (typeof flush === 'function') flush()
4518
+ else flush[1]()
3913
4519
  }
3914
4520
  activePublication = null
3915
4521
  committing = true