form0-core 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { flattenFields } from '../utilities/field-helpers.js';
1
+ import { assertConditionReferences, createConditionContext } from './condition-context.js';
2
2
 
3
3
  const valuesEqual = (a, b) => {
4
4
  if (Array.isArray(a) && Array.isArray(b)) {
@@ -81,37 +81,50 @@ function normalizeConditionValue(field, value) {
81
81
  return value;
82
82
  }
83
83
 
84
- export function evaluateConditions(conditions, values, allFields) {
84
+ export function evaluateConditions(conditions, values, allFields, scope = null) {
85
+ if (scope) {
86
+ assertConditionReferences(conditions, scope.owner, scope.property, scope.context);
87
+ }
88
+ return evaluateConditionExpression(conditions, values, allFields);
89
+ }
90
+
91
+ function evaluateConditionExpression(conditions, values, allFields) {
85
92
  if (Array.isArray(conditions)) {
86
- return conditions.every((c) => evaluateConditions(c, values, allFields));
93
+ return conditions.every((c) => evaluateConditionExpression(c, values, allFields));
87
94
  }
88
95
 
89
96
  if (conditions.and) {
90
- return conditions.and.every((c) => evaluateConditions(c, values, allFields));
97
+ return conditions.and.every((c) => evaluateConditionExpression(c, values, allFields));
91
98
  }
92
99
 
93
100
  if (conditions.or) {
94
- return conditions.or.some((c) => evaluateConditions(c, values, allFields));
101
+ return conditions.or.some((c) => evaluateConditionExpression(c, values, allFields));
95
102
  }
96
103
 
97
104
  // Map field_id (key) to data_name for value lookup
98
105
  const field = allFields ? allFields[conditions.field_id] : undefined;
99
106
  const dataName = field ? field.data_name : undefined;
100
107
  const val = normalizeConditionValue(field, dataName ? values[dataName] : undefined);
101
- // Debug log: show what is being checked
102
- //console.log('[evaluateConditions] field_id:', conditions.field_id, 'data_name:', dataName, 'value in values:', val, 'expected:', conditions.value, 'operator:', conditions.operator);
103
108
  const fn = OPERATORS[conditions.operator];
104
109
  return fn ? fn(val, conditions.value) : false;
105
110
  }
106
111
 
112
+ function evaluateElementCondition(owner, property, values, context) {
113
+ return evaluateConditions(owner[property], values, context.allFields, {
114
+ owner,
115
+ property,
116
+ context,
117
+ });
118
+ }
119
+
107
120
  /**
108
121
  * Recursively evaluates visibility for both fields and sections in bottom-up order
109
122
  * @param {Array} elements - Form elements to evaluate
110
123
  * @param {Object} values - Current form values
111
124
  * @param {Object} visible - Visibility state object to populate
112
- * @param {Object} allFields - Map of all fields by key for condition evaluation
125
+ * @param {Object} context - Shared field index and ownership resolver
113
126
  */
114
- function evaluateVisibilityRecursive(elements, values, visible, allFields) {
127
+ function evaluateVisibilityRecursive(elements, values, visible, context) {
115
128
  elements.forEach((element) => {
116
129
  if (
117
130
  element.type === 'Section' ||
@@ -120,7 +133,7 @@ function evaluateVisibilityRecursive(elements, values, visible, allFields) {
120
133
  ) {
121
134
  // First evaluate children (bottom-up approach)
122
135
  if (element.elements && element.elements.length > 0) {
123
- evaluateVisibilityRecursive(element.elements, values, visible, allFields);
136
+ evaluateVisibilityRecursive(element.elements, values, visible, context);
124
137
 
125
138
  // Check if any child is visible
126
139
  const hasVisibleChild = element.elements.some(
@@ -133,7 +146,12 @@ function evaluateVisibilityRecursive(elements, values, visible, allFields) {
133
146
  } else {
134
147
  // At least one child visible → respect explicit section visibility settings
135
148
  if (element.visible_conditions) {
136
- const isVisible = evaluateConditions(element.visible_conditions, values, allFields);
149
+ const isVisible = evaluateElementCondition(
150
+ element,
151
+ 'visible_conditions',
152
+ values,
153
+ context
154
+ );
137
155
  visible[element.data_name] = isVisible;
138
156
  } else {
139
157
  visible[element.data_name] = element.visible === true;
@@ -142,7 +160,12 @@ function evaluateVisibilityRecursive(elements, values, visible, allFields) {
142
160
  } else {
143
161
  // Empty section fallback (though schema validation should prevent this)
144
162
  if (element.visible_conditions) {
145
- const isVisible = evaluateConditions(element.visible_conditions, values, allFields);
163
+ const isVisible = evaluateElementCondition(
164
+ element,
165
+ 'visible_conditions',
166
+ values,
167
+ context
168
+ );
146
169
  visible[element.data_name] = isVisible;
147
170
  } else {
148
171
  visible[element.data_name] = element.visible === true;
@@ -151,7 +174,7 @@ function evaluateVisibilityRecursive(elements, values, visible, allFields) {
151
174
  } else {
152
175
  // Regular field - existing logic
153
176
  if (element.visible_conditions) {
154
- const isVisible = evaluateConditions(element.visible_conditions, values, allFields);
177
+ const isVisible = evaluateElementCondition(element, 'visible_conditions', values, context);
155
178
  visible[element.data_name] = isVisible;
156
179
  } else {
157
180
  visible[element.data_name] = element.visible === true;
@@ -160,34 +183,24 @@ function evaluateVisibilityRecursive(elements, values, visible, allFields) {
160
183
  });
161
184
  }
162
185
 
163
- export function evaluateVisibility(schema, values, visible) {
164
- // Build allFields map by key using flattened fields for condition evaluation
165
- // PERFORMANCE NOTE: For large/static schemas, consider caching/memoizing allFields per schema
166
- // to avoid rebuilding on every evaluation. See #perf-caching-idea.
167
- const fields = flattenFields(schema.elements);
168
- const allFields = {};
169
- fields.forEach((field) => {
170
- if (field.key) allFields[field.key] = field;
171
- if (field.data_name) allFields[field.data_name] = field;
172
- });
173
-
174
- // Use recursive approach for single-pass visibility evaluation
175
- evaluateVisibilityRecursive(schema.elements, values, visible, allFields);
186
+ export function evaluateVisibility(
187
+ schema,
188
+ values,
189
+ visible,
190
+ context = createConditionContext(schema)
191
+ ) {
192
+ evaluateVisibilityRecursive(schema.elements, values, visible, context);
176
193
  }
177
194
 
178
- export function evaluateRequirement(schema, values, required) {
179
- const fields = flattenFields(schema.elements);
180
- // Build allFields map by key
181
- // PERFORMANCE NOTE: For large/static schemas, consider caching/memoizing allFields per schema
182
- // to avoid rebuilding on every evaluation. See #perf-caching-idea.
183
- const allFields = {};
184
- fields.forEach((field) => {
185
- if (field.key) allFields[field.key] = field;
186
- if (field.data_name) allFields[field.data_name] = field;
187
- });
188
- fields.forEach((field) => {
195
+ export function evaluateRequirement(
196
+ schema,
197
+ values,
198
+ required,
199
+ context = createConditionContext(schema)
200
+ ) {
201
+ context.fields.forEach((field) => {
189
202
  if (field.required_conditions) {
190
- const isRequired = evaluateConditions(field.required_conditions, values, allFields);
203
+ const isRequired = evaluateElementCondition(field, 'required_conditions', values, context);
191
204
  required[field.data_name] = isRequired;
192
205
  } else {
193
206
  required[field.data_name] = field.required === true;
@@ -195,19 +208,15 @@ export function evaluateRequirement(schema, values, required) {
195
208
  });
196
209
  }
197
210
 
198
- export function evaluateReadOnly(schema, values, read_only) {
199
- const fields = flattenFields(schema.elements);
200
- // Build allFields map by key
201
- // PERFORMANCE NOTE: For large/static schemas, consider caching/memoizing allFields per schema
202
- // to avoid rebuilding on every evaluation. See #perf-caching-idea.
203
- const allFields = {};
204
- fields.forEach((field) => {
205
- if (field.key) allFields[field.key] = field;
206
- if (field.data_name) allFields[field.data_name] = field;
207
- });
208
- fields.forEach((field) => {
211
+ export function evaluateReadOnly(
212
+ schema,
213
+ values,
214
+ read_only,
215
+ context = createConditionContext(schema)
216
+ ) {
217
+ context.fields.forEach((field) => {
209
218
  if (field.read_only_conditions) {
210
- const isReadOnly = evaluateConditions(field.read_only_conditions, values, allFields);
219
+ const isReadOnly = evaluateElementCondition(field, 'read_only_conditions', values, context);
211
220
  read_only[field.data_name] = isReadOnly;
212
221
  } else {
213
222
  read_only[field.data_name] = field.read_only === true;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Context-aware field resolution for calculations and events
2
+ * Context-aware field resolution for calculations, conditions and events
3
3
  * Determines which fields are accessible based on execution context
4
4
  * Reuses tree-building logic from record-transformer.js
5
5
  */
@@ -10,6 +10,7 @@ export class ContextResolver {
10
10
  constructor(schema) {
11
11
  // Maps to store field context information (similar to record-transformer.js)
12
12
  this.fieldOwnership = new Map(); // Field data_name -> { preferredKey, field, parentPath }
13
+ this.elementOwnership = new Map(); // Includes containers in their containing record context.
13
14
  this.repeatableSectionTree = new Map(); // RepeatableSection data_name -> tree info
14
15
  this.sectionFields = new Set(); // Section field names (organizational only)
15
16
 
@@ -26,6 +27,12 @@ export class ContextResolver {
26
27
  if (!Array.isArray(elements)) return;
27
28
 
28
29
  elements.forEach((element) => {
30
+ const preferredKey =
31
+ typeof element.key === 'string' && element.key.trim() !== ''
32
+ ? element.key
33
+ : element.data_name;
34
+ const ownership = { preferredKey, field: element, parentPath: [...parentPath] };
35
+ this.elementOwnership.set(element.data_name, ownership);
29
36
  if (element.type === 'Section' || element.type === 'BuildingPlanSection') {
30
37
  this.sectionFields.add(element.data_name);
31
38
  // Recursively process Section children with same parentPath
@@ -34,8 +41,6 @@ export class ContextResolver {
34
41
  this.buildFieldContext(element.elements, parentPath);
35
42
  }
36
43
  } else if (element.type === 'RepeatableSection') {
37
- const preferredKey =
38
- element.key && element.key.trim() !== '' ? element.key : element.data_name;
39
44
  const currentPath = [...parentPath, preferredKey];
40
45
 
41
46
  // Store this RepeatableSection in the tree
@@ -54,13 +59,7 @@ export class ContextResolver {
54
59
  }
55
60
  } else {
56
61
  // This is a regular field - determine its ownership
57
- const preferredFieldKey =
58
- element.key && element.key.trim() !== '' ? element.key : element.data_name;
59
- this.fieldOwnership.set(element.data_name, {
60
- preferredKey: preferredFieldKey,
61
- field: element,
62
- parentPath: [...parentPath], // Copy the current path
63
- });
62
+ this.fieldOwnership.set(element.data_name, ownership);
64
63
  }
65
64
  });
66
65
  }
@@ -146,6 +145,19 @@ export class ContextResolver {
146
145
  checkRepeatableSectionAccessInfo(executionContext, fieldName, fieldInfo) {
147
146
  const { type: contextType, eventType, fieldName: contextFieldName } = executionContext;
148
147
 
148
+ if (contextType === 'condition') {
149
+ const owner = this.elementOwnership.get(contextFieldName);
150
+ if (owner) {
151
+ return this.resolveRepeatableSectionRelationship(
152
+ owner.parentPath,
153
+ fieldInfo.parentPath,
154
+ fieldName,
155
+ fieldInfo,
156
+ executionContext
157
+ );
158
+ }
159
+ }
160
+
149
161
  if (contextType === 'calculation') {
150
162
  // CalculatedField can only access fields in same RepeatableSection context
151
163
  const calculationFieldInfo = this.fieldOwnership.get(contextFieldName);
@@ -318,6 +330,10 @@ export class ContextResolver {
318
330
  buildRestrictedAccessSuggestion(executionContext, fieldName, fieldInfo, code) {
319
331
  const { type: contextType, eventType } = executionContext;
320
332
 
333
+ if (contextType === 'condition') {
334
+ return `Conditions can only reference fields in their containing record or its ancestors. Field '${fieldName}' belongs to: ${this.formatRepeatableSectionPath(fieldInfo.parentPath)}`;
335
+ }
336
+
321
337
  if (contextType === 'calculation') {
322
338
  if (code === 'different_repeatable_section') {
323
339
  return `CalculatedField can only access fields in the same RepeatableSection or ancestor contexts. Field '${fieldName}' is in RepeatableSection: ${this.formatRepeatableSectionPath(fieldInfo.parentPath)}`;
@@ -54,8 +54,10 @@ export function runExpression(
54
54
 
55
55
  // Create secure context based on security mode
56
56
  const secureContext = createSecureContext(context, securityConfig);
57
- const keys = Object.keys(secureContext);
58
- const values = Object.values(secureContext);
57
+ const keys = options.compileCache
58
+ ? Object.keys(secureContext).sort()
59
+ : Object.keys(secureContext);
60
+ const values = keys.map((key) => secureContext[key]);
59
61
 
60
62
  // Execute expression
61
63
  const executeExpression = () => {
@@ -70,9 +72,11 @@ export function runExpression(
70
72
  isMultilineCalculationExpression(sourceExpression);
71
73
 
72
74
  if (isMultiLine) {
73
- // Execute as code block (recompile each time for now)
75
+ // Reuse a factory-owned compilation cache when supplied; values stay per evaluation.
74
76
  phase = 'syntax';
75
- const fn = new Function(...keys, expr);
77
+ const cacheKey = JSON.stringify(['body', keys, expr]);
78
+ const fn = options.compileCache?.get(cacheKey) ?? new Function(...keys, expr);
79
+ options.compileCache?.set(cacheKey, fn);
76
80
  phase = 'runtime';
77
81
  const result = fn(...values);
78
82
  // Check for consumed result from SETRESULT() calls in multiline code
@@ -85,7 +89,11 @@ export function runExpression(
85
89
  // Execute as expression (existing behavior)
86
90
  const inlineExpression = normalizeInlineCalculationExpression(expr);
87
91
  phase = 'syntax';
88
- const fn = new Function(...keys, `return (${inlineExpression});`);
92
+ const cacheKey = JSON.stringify(['expression', keys, inlineExpression]);
93
+ const fn =
94
+ options.compileCache?.get(cacheKey) ??
95
+ new Function(...keys, `return (${inlineExpression});`);
96
+ options.compileCache?.set(cacheKey, fn);
89
97
  phase = 'runtime';
90
98
  const result = fn(...values);
91
99
  const consumed = __consumeResult();
@@ -1,10 +1,43 @@
1
1
  import { parse } from 'acorn';
2
2
 
3
3
  /** Retains authored callback source without depending on engine-specific toString output. */
4
- export function createEventSourceRegistry() {
4
+ export function createEventSourceRegistry(preparationCache = null) {
5
5
  const sources = new WeakMap();
6
6
 
7
+ function bindPrepared(prepared, context) {
8
+ const propertyNames = new Map();
9
+ const capture = (callback, source, name) => {
10
+ sources.set(callback, source);
11
+ if (name && !callback.name)
12
+ Object.defineProperty(callback, 'name', { value: name, configurable: true });
13
+ return callback;
14
+ };
15
+ capture.key = (value, id) => {
16
+ const key = Reflect.ownKeys({ [value]: null })[0];
17
+ propertyNames.set(
18
+ id,
19
+ typeof key === 'symbol'
20
+ ? key.description === undefined
21
+ ? ''
22
+ : `[${key.description}]`
23
+ : key
24
+ );
25
+ return key;
26
+ };
27
+ capture.nameFor = (id) => propertyNames.get(id);
28
+
29
+ return {
30
+ code: prepared.code,
31
+ context: {
32
+ ...context,
33
+ [prepared.captureName]: capture,
34
+ },
35
+ };
36
+ }
37
+
7
38
  function prepare(code, context) {
39
+ const cached = preparationCache?.get(code);
40
+ if (cached && !Object.hasOwn(context, cached.captureName)) return bindPrepared(cached, context);
8
41
  let tree;
9
42
  try {
10
43
  // Event scripts are Function bodies, rather than modules or global scripts.
@@ -27,7 +60,6 @@ export function createEventSourceRegistry() {
27
60
  let captureName = '__form0CaptureEventSource';
28
61
  while (identifiers.has(captureName) || Object.hasOwn(context, captureName)) captureName += '_';
29
62
  const edits = [];
30
- const propertyNames = new Map();
31
63
 
32
64
  function inferredName(node, parent) {
33
65
  if (node.id) return '';
@@ -120,33 +152,9 @@ export function createEventSourceRegistry() {
120
152
  prepared = prepared.slice(0, position) + text + prepared.slice(position);
121
153
  }
122
154
 
123
- const capture = (callback, source, name) => {
124
- sources.set(callback, source);
125
- if (name && !callback.name)
126
- Object.defineProperty(callback, 'name', { value: name, configurable: true });
127
- return callback;
128
- };
129
- capture.key = (value, id) => {
130
- const key = Reflect.ownKeys({ [value]: null })[0];
131
- propertyNames.set(
132
- id,
133
- typeof key === 'symbol'
134
- ? key.description === undefined
135
- ? ''
136
- : `[${key.description}]`
137
- : key
138
- );
139
- return key;
140
- };
141
- capture.nameFor = (id) => propertyNames.get(id);
142
-
143
- return {
144
- code: prepared,
145
- context: {
146
- ...context,
147
- [captureName]: capture,
148
- },
149
- };
155
+ const result = { code: prepared, captureName };
156
+ preparationCache?.set(code, result);
157
+ return bindPrepared(result, context);
150
158
  }
151
159
 
152
160
  function getSource(callback) {
@@ -17,7 +17,7 @@ const _loggedHandlers = new Set();
17
17
  * EventManager handles form event registration and execution
18
18
  */
19
19
  export class EventManager {
20
- constructor(schema = null, contextResolver = null, warningSystem = null) {
20
+ constructor(schema = null, contextResolver = null, warningSystem = null, preparation = {}) {
21
21
  this.listeners = new Map(); // eventType -> Map<fieldKey, callback[]>
22
22
  this.eventContext = null;
23
23
  this.securityConfig = null;
@@ -26,7 +26,9 @@ export class EventManager {
26
26
  this.contextResolver = contextResolver || (schema ? new ContextResolver(schema) : null);
27
27
  this.warningSystem = warningSystem || new WarningSystem();
28
28
  this.schema = schema;
29
- this.callbackSources = createEventSourceRegistry();
29
+ this.callbackSources = createEventSourceRegistry(preparation.sourceCache);
30
+ this.compilationCache = preparation.compilationCache;
31
+ this.referenceSourceCache = preparation.referenceSourceCache;
30
32
  this.initializationIssues = [];
31
33
  }
32
34
 
@@ -65,6 +67,7 @@ export class EventManager {
65
67
  const result = runInEventOperationScope(scope, () =>
66
68
  runExpression(prepared.code, prepared.context, this.securityConfig, true, this.schema, {
67
69
  sourceExpression: code,
70
+ compileCache: this.compilationCache,
68
71
  onDiagnostic: (diagnostic) =>
69
72
  this.initializationIssues.push({
70
73
  code: 'initialization_failed',
@@ -305,6 +308,7 @@ export class EventManager {
305
308
  const functionCall = `(${callbackCode})(event)`;
306
309
  return runInEventOperationScope(scope, () =>
307
310
  runExpression(functionCall, contextWithEvent, this.securityConfig, true, this.schema, {
311
+ compileCache: this.compilationCache,
308
312
  evaluateAsExpression: true,
309
313
  ...(managed?.reporting || {}),
310
314
  })
@@ -4,7 +4,8 @@ import { evaluateRequirement } from './conditions.js';
4
4
  import { evaluateReadOnly } from './conditions.js';
5
5
  import { validateFields } from './field-validation.js';
6
6
  import { calculationBuiltins, eventBuiltins } from '../builtins/registry.js';
7
- import { validateSchema } from '../schema/schema-validator.js';
7
+ import { validateSchemaWithContext } from '../schema/schema-validator.js';
8
+ import { createConditionContext } from './condition-context.js';
8
9
  import { flattenFields } from '../utilities/field-helpers.js';
9
10
  import { DEFAULT_SECURITY_CONFIG } from '../security/config.js';
10
11
  import { EventManager } from './events.js';
@@ -77,7 +78,8 @@ export function createFormEngine({
77
78
  }) {
78
79
  const { schema: preparedSchema, buildingPlanMeta } = expandBuildingPlanSchema(schema);
79
80
 
80
- validateSchema(preparedSchema.form);
81
+ const contextResolver = new ContextResolver(preparedSchema.form);
82
+ validateSchemaWithContext(preparedSchema.form, contextResolver);
81
83
 
82
84
  const { form } = preparedSchema;
83
85
  const values = { ...initialValues };
@@ -147,9 +149,9 @@ export function createFormEngine({
147
149
  const allHelpers = { ...calculationBuiltins, ...helpers };
148
150
 
149
151
  // Initialize context resolution system
150
- const contextResolver = new ContextResolver(form);
151
152
  const calculationPlan = buildCalculationDependencyPlan(form, contextResolver);
152
153
  const sharedWarningSystem = warningSystem || new WarningSystem();
154
+ const conditionContext = createConditionContext(form, contextResolver, sharedWarningSystem);
153
155
  const calculationDiagnostics = createCalculationDiagnostics(
154
156
  diagnostics,
155
157
  onDiagnostics,
@@ -196,9 +198,9 @@ export function createFormEngine({
196
198
  calculationPlan,
197
199
  calculationDiagnostics
198
200
  );
199
- evaluateRequirement(form, values, required);
200
- evaluateVisibility(form, values, visible);
201
- evaluateReadOnly(form, values, read_only);
201
+ evaluateRequirement(form, values, required, conditionContext);
202
+ evaluateVisibility(form, values, visible, conditionContext);
203
+ evaluateReadOnly(form, values, read_only, conditionContext);
202
204
  validateFields(form, values, errors);
203
205
  } catch (error) {
204
206
  calculationDiagnostics.abort(frame);
@@ -247,7 +249,7 @@ export function createFormEngine({
247
249
  };
248
250
  }
249
251
 
250
- function getDefaultValue(field) {
252
+ export function getDefaultValue(field) {
251
253
  const spec = FIELD_SPECS[field.type];
252
254
  if (spec && spec.defaultProducer) {
253
255
  return spec.defaultProducer(field);
@@ -30,6 +30,10 @@ function copy(value, seen = new Map()) {
30
30
  return result;
31
31
  }
32
32
 
33
+ const managedReferenceCaches = new WeakMap();
34
+ const managedFieldCaches = new WeakMap();
35
+ const managedRepeatableCaches = new WeakMap();
36
+
33
37
  /** Internal adapter; the public synchronous API is engine.triggerDetailed(). */
34
38
  export function createManagedEventDispatcher(manager, constructionScope) {
35
39
  const instancePath = copy(constructionScope?.instancePath);
@@ -37,18 +41,27 @@ export function createManagedEventDispatcher(manager, constructionScope) {
37
41
  if (scopeId !== undefined && (typeof scopeId !== 'string' || !scopeId.trim()))
38
42
  throw new Error('eventScope.scopeId must be a nonempty string when supplied.');
39
43
  const resolver = manager.contextResolver;
40
- const fields = new Map(resolver.fieldOwnership);
44
+ if (!managedFieldCaches.has(resolver))
45
+ managedFieldCaches.set(resolver, new Map(resolver.fieldOwnership));
46
+ const fields = managedFieldCaches.get(resolver);
47
+ if (!managedRepeatableCaches.has(resolver))
48
+ managedRepeatableCaches.set(
49
+ resolver,
50
+ new Map(
51
+ [...resolver.repeatableSectionTree.values()].map((entry) => [
52
+ JSON.stringify([entry.parentPath, entry.preferredKey]),
53
+ entry,
54
+ ])
55
+ )
56
+ );
57
+ const repeatableIndex = managedRepeatableCaches.get(resolver);
41
58
  const statusField = manager.schema?.status_field;
42
59
  if (statusField?.enabled && typeof statusField.data_name === 'string')
43
60
  fields.set(statusField.data_name, { field: statusField, parentPath: [] });
44
61
  if (!Array.isArray(instancePath)) throw new Error('eventScope.instancePath must be an array.');
45
62
  const ownershipPath = [];
46
63
  for (const segment of instancePath) {
47
- const repeatable = [...resolver.repeatableSectionTree.values()].find(
48
- (entry) =>
49
- entry.preferredKey === segment?.repeatableKey &&
50
- JSON.stringify(entry.parentPath) === JSON.stringify(ownershipPath)
51
- );
64
+ const repeatable = repeatableIndex.get(JSON.stringify([ownershipPath, segment?.repeatableKey]));
52
65
  if (
53
66
  !repeatable ||
54
67
  JSON.stringify(repeatable.parentPath) !== JSON.stringify(ownershipPath) ||
@@ -70,7 +83,8 @@ export function createManagedEventDispatcher(manager, constructionScope) {
70
83
  const readable = (path) =>
71
84
  path.length <= ownershipPath.length && path.every((key, index) => ownershipPath[index] === key);
72
85
  const ownedField = (name) => fields.get(name) || resolver.repeatableSectionTree.get(name);
73
- const referenceCache = new WeakMap();
86
+ if (!managedReferenceCaches.has(manager)) managedReferenceCaches.set(manager, new WeakMap());
87
+ const referenceCache = managedReferenceCaches.get(manager);
74
88
  return function dispatch(eventType, fieldKey, metadata = {}) {
75
89
  const result = { operations: [], validationIssues: [], issues: [] };
76
90
  const issue = (code, message) =>
@@ -149,8 +163,12 @@ export function createManagedEventDispatcher(manager, constructionScope) {
149
163
  let collected;
150
164
  try {
151
165
  const source = manager.callbackSources.getSource(callback);
152
- if (referenceCache.get(callback)?.source !== source)
153
- referenceCache.set(callback, { source, references: getEventFieldReferences(source) });
166
+ if (referenceCache.get(callback)?.source !== source) {
167
+ const references =
168
+ manager.referenceSourceCache?.get(source) ?? getEventFieldReferences(source);
169
+ manager.referenceSourceCache?.set(source, references);
170
+ referenceCache.set(callback, { source, references });
171
+ }
154
172
  let rejectedReference = false;
155
173
  for (const name of referenceCache.get(callback).references) {
156
174
  const field = fields.get(name);