form0-core 0.6.0 → 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.
@@ -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);
@@ -0,0 +1,126 @@
1
+ import { ContextResolver } from './context-resolver.js';
2
+ import { expandBuildingPlanSchema } from '../schema/building-plan-expander.js';
3
+ import { validateSchemaWithContext } from '../schema/schema-validator.js';
4
+ import { buildCalculationDependencyPlan } from '../utilities/calculation-dependencies.js';
5
+ import { analyzeRepeatableCollections } from '../utilities/repeatable-collections.js';
6
+ import { createConditionContext } from './condition-context.js';
7
+ import { copyRecordData } from '../utilities/record-data.js';
8
+
9
+ /** Shared immutable schema preparation; never owns live row engines. */
10
+ export function prepareRecordContext(inputSchema, warningSystem = null) {
11
+ const sourceSchema = copyRecordData(inputSchema);
12
+ const { schema } = expandBuildingPlanSchema(sourceSchema);
13
+ const resolver = new ContextResolver(schema.form);
14
+ validateSchemaWithContext(schema.form, resolver);
15
+ const calculationPlan = buildCalculationDependencyPlan(schema.form, resolver);
16
+ const conditionContext = createConditionContext(schema.form, resolver, warningSystem);
17
+ const templates = new Map();
18
+ const repeatableByPath = new Map(
19
+ [...resolver.repeatableSectionTree.values()].map((entry) => [
20
+ JSON.stringify(entry.currentPath),
21
+ entry,
22
+ ])
23
+ );
24
+ const calculations = new Map();
25
+ for (const [name, node] of calculationPlan.nodesByDataName) {
26
+ calculations.set(name, {
27
+ ...node,
28
+ collection: analyzeRepeatableCollections(node.field.calculate, resolver, name),
29
+ });
30
+ }
31
+ function template(elements, path) {
32
+ const fields = [];
33
+ const chains = new Map();
34
+ function project(items, ancestors = []) {
35
+ return items.map((field) => {
36
+ fields.push(field);
37
+ chains.set(field.data_name, ancestors);
38
+ if (field.type === 'RepeatableSection') {
39
+ const key = resolver.repeatableSectionTree.get(field.data_name).preferredKey;
40
+ template(field.elements || [], [...path, key]);
41
+ return { ...field, elements: [] };
42
+ }
43
+ if (field.type === 'Section' || field.type === 'BuildingPlanSection')
44
+ return {
45
+ ...field,
46
+ elements: project(field.elements || [], [...ancestors, field.data_name]),
47
+ };
48
+ return field;
49
+ });
50
+ }
51
+ const localForm = { elements: project(elements) };
52
+ const status = path.length === 0 ? schema.form.status_field : null;
53
+ if (status?.enabled) {
54
+ fields.push(status);
55
+ chains.set(status.data_name, []);
56
+ }
57
+ const result = {
58
+ path,
59
+ fields,
60
+ chains,
61
+ form: localForm,
62
+ conditions: { ...conditionContext, fields },
63
+ };
64
+ templates.set(JSON.stringify(path), result);
65
+ return result;
66
+ }
67
+ template(schema.form.elements, []);
68
+ return {
69
+ schema,
70
+ sourceSchema,
71
+ resolver,
72
+ calculationPlan,
73
+ templates,
74
+ calculations,
75
+ repeatableByPath,
76
+ };
77
+ }
78
+
79
+ export function visitRecordScopes(snapshot, context, visit) {
80
+ function walk(record, path, instancePath, ancestors, participating) {
81
+ const template = context.templates.get(JSON.stringify(path));
82
+ const scope = { record, template, instancePath, ancestors, participating };
83
+ visit(scope);
84
+ for (const field of template.fields) {
85
+ if (field.type !== 'RepeatableSection') continue;
86
+ const info = context.resolver.repeatableSectionTree.get(field.data_name);
87
+ const rows = record.repeatable?.[info.preferredKey] ?? [];
88
+ if (!Array.isArray(rows))
89
+ throw new Error(`RepeatableSection '${field.data_name}' must contain an array of rows.`);
90
+ const identities = new Set();
91
+ rows.forEach((row, index) => {
92
+ if (!row || typeof row !== 'object' || Array.isArray(row))
93
+ throw new Error('Repeatable rows must be record objects.');
94
+ const id = row.id ?? row.record_id;
95
+ const identified =
96
+ (typeof id === 'string' && id.trim() !== '') ||
97
+ (typeof id === 'number' && Number.isFinite(id));
98
+ if (identified) {
99
+ const identity = `${typeof id}:${id}`;
100
+ if (identities.has(identity))
101
+ throw new Error(`Duplicate row identity in '${field.data_name}'.`);
102
+ identities.add(identity);
103
+ }
104
+ const segment = {
105
+ repeatableKey: info.preferredKey,
106
+ instanceId: identified ? id : null,
107
+ index,
108
+ identity: identified ? 'id' : 'position',
109
+ };
110
+ const visible =
111
+ record.visible?.[field.data_name] !== false &&
112
+ (template.chains.get(field.data_name) || []).every(
113
+ (name) => record.visible?.[name] !== false
114
+ );
115
+ walk(
116
+ row,
117
+ info.currentPath,
118
+ [...instancePath, segment],
119
+ [...ancestors, scope],
120
+ participating && visible
121
+ );
122
+ });
123
+ }
124
+ }
125
+ walk(snapshot, [], [], [], true);
126
+ }