@carbonenginejs/runtime-utils 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/NOTICE +23 -23
- package/README.md +69 -69
- package/THIRD-PARTY-NOTICES.md +64 -64
- package/docs/README.md +74 -73
- package/docs/architecture.md +101 -101
- package/docs/concepts/foundation-consolidation.md +85 -85
- package/docs/concepts/model-lifecycle.md +154 -0
- package/docs/const-kb.md +85 -87
- package/docs/core-types/DECORATOR-TODOS.md +25 -25
- package/docs/core-types/README.md +241 -168
- package/docs/reference/api.md +105 -102
- package/docs/reference/classes/README.md +135 -135
- package/package.json +139 -139
- package/src/arrays.js +5 -5
- package/src/audio/audioFormats.js +34 -34
- package/src/audio/index.js +1 -1
- package/src/box3.js +331 -331
- package/src/bytes.js +56 -56
- package/src/compression.js +56 -56
- package/src/constants/index.js +7 -7
- package/src/constants/trinity.js +13 -13
- package/src/constants.js +15 -15
- package/src/curve.js +79 -79
- package/src/d3d/dxgiFormats.js +46 -46
- package/src/d3d/index.js +2 -2
- package/src/d3d/primitiveTopology.js +11 -11
- package/src/document/CjsCarbonDocument.js +212 -212
- package/src/document/CjsClassRegistry.js +373 -373
- package/src/document/CjsDocumentDehydrator.js +142 -142
- package/src/document/CjsDocumentHydrator.js +156 -156
- package/src/document/CjsStructRegistry.js +348 -348
- package/src/document/hydrationAdapter.js +129 -129
- package/src/document/index.js +6 -6
- package/src/errors/CjsError.js +286 -286
- package/src/errors/index.js +5 -5
- package/src/geometry/box.js +22 -22
- package/src/geometry/cylinder.js +22 -22
- package/src/geometry/helpers/earcut.js +1 -1
- package/src/geometry/helpers/misc.js +3 -3
- package/src/geometry/index.js +8 -8
- package/src/geometry/json.js +67 -67
- package/src/geometry/lathe.js +44 -44
- package/src/geometry/plane.js +14 -14
- package/src/geometry/shape.js +4 -4
- package/src/geometry/sphere.js +24 -24
- package/src/geometry/torus.js +14 -14
- package/src/graphics/colorSpaces.js +22 -22
- package/src/graphics/index.js +4 -4
- package/src/graphics/pixelFormats.js +158 -158
- package/src/graphics/textureDimensions.js +22 -22
- package/src/graphics/trinityEnums.js +87 -87
- package/src/index.js +62 -62
- package/src/is.js +117 -117
- package/src/json.js +23 -23
- package/src/lifecycle/CjsLifecycleState.js +77 -77
- package/src/lifecycle/index.js +1 -1
- package/src/lne3.js +70 -70
- package/src/lookup.js +48 -48
- package/src/mat3.js +51 -51
- package/src/mat4.js +699 -699
- package/src/math/index.js +25 -25
- package/src/math/scalar.js +63 -63
- package/src/media/index.js +1 -1
- package/src/media/mediaTypes.js +50 -50
- package/src/mesh.js +424 -424
- package/src/model/CjsEventEmitter.js +333 -333
- package/src/model/CjsModel.js +1744 -1463
- package/src/model/CjsModelState.js +72 -72
- package/src/model/index.js +4 -4
- package/src/model/sourceRecordUtils.js +54 -54
- package/src/noise.js +310 -310
- package/src/num.js +827 -827
- package/src/object.js +39 -39
- package/src/path.js +53 -53
- package/src/pln.js +125 -125
- package/src/pool.js +9 -9
- package/src/quat.js +144 -144
- package/src/ray3.js +188 -188
- package/src/renderContext/formats.js +145 -145
- package/src/renderContext/index.js +5 -5
- package/src/renderContext/presentation.js +125 -125
- package/src/renderContext/resources.js +27 -27
- package/src/renderContext/upscaling.js +22 -22
- package/src/renderContext/window.js +20 -20
- package/src/runtime/CjsRuntimeState.js +50 -50
- package/src/schema/CjsSchema.js +141 -23
- package/src/schema/index.js +4 -4
- package/src/shader/index.js +1 -1
- package/src/shader/shaderStages.js +37 -37
- package/src/sph3.js +181 -181
- package/src/tangent.js +288 -288
- package/src/text.js +40 -40
- package/src/tri3.js +98 -98
- package/src/types/carbonTypes.js +635 -635
- package/src/types/index.js +2 -2
- package/src/utils.js +58 -58
- package/src/validation.js +46 -46
- package/src/vec2.js +229 -229
- package/src/vec3.js +1188 -1188
- package/src/vec4.js +347 -347
- package/src/vertex.js +108 -108
- package/src/webgpu/index.js +1 -1
- package/src/webgpu/textureFormats.js +121 -121
package/src/model/CjsModel.js
CHANGED
|
@@ -1,1589 +1,1870 @@
|
|
|
1
|
-
import { coerceCarbonMathInto, exportCarbonValue, normalizeCarbonValue } from "../types/index.js";
|
|
2
|
-
import { CjsSchema } from "../schema/index.js";
|
|
3
|
-
import { getRuntimeState } from "../runtime/CjsRuntimeState.js";
|
|
4
|
-
import { CjsModelState } from "./CjsModelState.js";
|
|
5
|
-
import { CjsEventEmitter } from "./CjsEventEmitter.js";
|
|
6
|
-
|
|
1
|
+
import { coerceCarbonMathInto, exportCarbonValue, normalizeCarbonValue } from "../types/index.js";
|
|
2
|
+
import { CjsSchema } from "../schema/index.js";
|
|
3
|
+
import { getRuntimeState } from "../runtime/CjsRuntimeState.js";
|
|
4
|
+
import { CjsModelState } from "./CjsModelState.js";
|
|
5
|
+
import { CjsEventEmitter } from "./CjsEventEmitter.js";
|
|
6
|
+
|
|
7
7
|
const MAX_UPDATE_PASSES = 32;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
|
|
8
|
+
const CHILD_COLLECTION_KINDS = new Set([ "array", "list" ]);
|
|
9
|
+
const CHILD_LIST_EVENT = {
|
|
10
|
+
UNLOAD_START: 0x07,
|
|
11
|
+
INSERTED: 0x08,
|
|
12
|
+
REMOVED: 0x09
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Shared base for schema-backed CarbonEngineJS runtime classes.
|
|
17
|
+
*
|
|
18
|
+
* Source fields are exported; runtime caches and bookkeeping are not.
|
|
19
|
+
*/
|
|
20
|
+
export class CjsModel extends CjsEventEmitter
|
|
21
|
+
{
|
|
22
|
+
/**
|
|
23
|
+
* Identifies this class as a schema-backed model.
|
|
24
|
+
*
|
|
25
|
+
* Declared statically so CjsSchema can recognise a model class from a field
|
|
26
|
+
* declaration alone, without importing CjsModel - which it cannot do, since
|
|
27
|
+
* this module already imports CjsSchema. Mirrors `CjsResource.isResource`.
|
|
28
|
+
*/
|
|
29
|
+
static isModel = true;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates a schema-backed model with initialized runtime state.
|
|
33
|
+
*/
|
|
34
|
+
constructor()
|
|
35
|
+
{
|
|
36
|
+
super();
|
|
37
|
+
const className = CjsSchema.getClassName(this.constructor);
|
|
38
|
+
if (!className)
|
|
39
|
+
{
|
|
40
|
+
throw new TypeError("CjsModel subclasses require an explicit CjsSchema className.");
|
|
41
|
+
}
|
|
42
|
+
initializeModelState(this);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Exports the model's schema fields to a new plain object.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} [options={}]
|
|
49
|
+
* @returns {object}
|
|
50
|
+
*/
|
|
51
|
+
GetValues(options = {})
|
|
52
|
+
{
|
|
53
|
+
return CjsModel.get(this, {}, options);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Applies a plain value bag through the canonical schema-backed setter.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} [values={}]
|
|
60
|
+
* @param {object} [options={}]
|
|
61
|
+
* @returns {Set<string>|boolean} The changed fields, or a boolean result.
|
|
62
|
+
*/
|
|
63
|
+
SetValues(values = {}, options = {})
|
|
64
|
+
{
|
|
65
|
+
return CjsModel.set(this, values, options);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Copies the exported fields of another model into this model.
|
|
70
|
+
*
|
|
71
|
+
* @param {CjsModel} value
|
|
72
|
+
* @param {object} [options={}]
|
|
73
|
+
* @returns {CjsModel} This model.
|
|
74
|
+
*/
|
|
75
|
+
Copy(value, options = {})
|
|
76
|
+
{
|
|
77
|
+
return CjsModel.copy(this, value, options);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Constructs a new model of this instance's class from its schema values.
|
|
82
|
+
*
|
|
83
|
+
* @param {object} [options={}]
|
|
84
|
+
* @returns {CjsModel}
|
|
85
|
+
*/
|
|
86
|
+
Clone(options = {})
|
|
87
|
+
{
|
|
88
|
+
return this.constructor.clone(this, options);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Deep-merges ordered value sources and applies the result once.
|
|
93
|
+
*
|
|
94
|
+
* Plain objects merge recursively; arrays, typed arrays, and other values
|
|
95
|
+
* replace the preceding value.
|
|
96
|
+
*
|
|
97
|
+
* @param {Array<Object|CjsModel>} [values=[]]
|
|
98
|
+
* @param {object} [options={}]
|
|
99
|
+
* @returns {Set<string>|boolean} The result returned by {@link CjsModel.set}.
|
|
100
|
+
*/
|
|
95
101
|
Merge(values = [], options = {})
|
|
96
102
|
{
|
|
97
103
|
return CjsModel.merge(this, values, options);
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* Calling this IS the "I made changes, apply please" contract: it always
|
|
105
|
-
* runs at least one hook pass, dirty or not, so direct/untracked
|
|
106
|
-
* mutations (the cooperative-pipeline reality) can be applied
|
|
107
|
-
* explicitly. Class Update/per-frame methods typically gate on
|
|
108
|
-
* `__state.IsDirty()` before calling.
|
|
109
|
-
*
|
|
110
|
-
* @param {object} [options={}]
|
|
111
|
-
* @param {string|Iterable<string>} [options.property] Fields the caller changed directly; their declared flag/rebuild tokens are added first.
|
|
112
|
-
* @param {string|Iterable<string>} [options.properties] Alias of `property`.
|
|
113
|
-
* @param {*} [options.source=this] Origin forwarded to the hook and event (binding feedback control).
|
|
114
|
-
* @param {boolean} [options.skipEvents=false] Prevents the final modified event.
|
|
115
|
-
* @returns {boolean} False when the hook rejected the update (dirty is retained).
|
|
116
|
-
* @throws {Error} If local changes do not settle within the update-pass limit.
|
|
117
|
-
*/
|
|
118
|
-
UpdateValues(options = {})
|
|
119
|
-
{
|
|
120
|
-
addExplicitUpdateProperties(this, options.property ?? options.properties);
|
|
121
|
-
if (this.__state.updating) return true;
|
|
122
|
-
|
|
123
|
-
const source = options.source ?? this;
|
|
124
|
-
this.__state.updating = true;
|
|
125
|
-
|
|
126
|
-
try
|
|
127
|
-
{
|
|
128
|
-
for (let pass = 0; ; pass++)
|
|
129
|
-
{
|
|
130
|
-
if (pass >= MAX_UPDATE_PASSES)
|
|
131
|
-
{
|
|
132
|
-
throw new Error(`${CjsSchema.getClassName(this.constructor)}.UpdateValues exceeded ${MAX_UPDATE_PASSES} local settle passes.`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
this.__state.dirty = false;
|
|
136
|
-
|
|
137
|
-
if (this.OnModified({ ...options, source }) === false)
|
|
138
|
-
{
|
|
139
|
-
this.__state.dirty = true;
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (!this.__state.dirty) break;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
catch (err)
|
|
147
|
-
{
|
|
148
|
-
this.__state.dirty = true;
|
|
149
|
-
throw err;
|
|
150
|
-
}
|
|
151
|
-
finally
|
|
152
|
-
{
|
|
153
|
-
this.__state.updating = false;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (options.skipEvents !== true && this.__state.suppressEvents === 0)
|
|
157
|
-
{
|
|
158
|
-
this.EmitEvent("modified", this, { source });
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return true;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* The settle hook: reproduces the meaningful consequences of the
|
|
166
|
-
* corresponding Carbon INotify::OnModified implementation.
|
|
167
|
-
*
|
|
168
|
-
* Invoked only by UpdateValues. Receives the mutation options bag
|
|
169
|
-
* (source, caller context, skipEvents, ...). There is no changed-property
|
|
170
|
-
* list - the pipeline is cooperative and cannot guarantee one - so
|
|
171
|
-
* overrides are written broad-safe: consult own state, compare cached
|
|
172
|
-
* derivations, and rely on `__state.flags`/`__state.rebuild` tokens for
|
|
173
|
-
* targeted signals. Returning `false` rejects the update and retains the
|
|
174
|
-
* dirty mark.
|
|
175
|
-
*
|
|
176
|
-
* @param {object} [options={}]
|
|
177
|
-
* @returns {boolean} Whether the update may complete.
|
|
178
|
-
*/
|
|
179
|
-
OnModified(options = {})
|
|
180
|
-
{
|
|
181
|
-
return true;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Visits this model and its schema-backed child models without revisiting cycles.
|
|
186
|
-
*
|
|
187
|
-
* In pre-order traversal, returning `false` prunes that model's descendants.
|
|
188
|
-
* Visitor return values are ignored in post-order traversal.
|
|
189
|
-
*
|
|
190
|
-
* @param {function(CjsModel): (boolean|void)} visitor
|
|
191
|
-
* @param {object} [options={}]
|
|
192
|
-
* @param {Set<CjsModel>} [options.visited] Existing cycle-detection set.
|
|
193
|
-
* @param {"pre"|"post"} [options.order="pre"]
|
|
194
|
-
* @param {boolean} [options.reverse=false] Reverses field and list-item order.
|
|
195
|
-
* @param {boolean} [options.ownedOnly=false] Traverses only owned relationships.
|
|
196
|
-
* @returns {CjsModel} This model.
|
|
197
|
-
* @throws {TypeError} If `visitor` is not a function.
|
|
198
|
-
*/
|
|
199
|
-
Traverse(visitor, options = {})
|
|
200
|
-
{
|
|
201
|
-
if (typeof visitor !== "function")
|
|
202
|
-
{
|
|
203
|
-
throw new TypeError("CjsModel.Traverse requires a visitor function.");
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const visited = options.visited instanceof Set ? options.visited : new Set();
|
|
207
|
-
const order = options.order === "post" ? "post" : "pre";
|
|
208
|
-
const reverse = options.reverse === true;
|
|
209
|
-
|
|
210
|
-
const visit = model =>
|
|
211
|
-
{
|
|
212
|
-
if (!(model instanceof CjsModel) || visited.has(model)) return;
|
|
213
|
-
visited.add(model);
|
|
214
|
-
|
|
215
|
-
let descend = true;
|
|
216
|
-
if (order === "pre") descend = visitor(model) !== false;
|
|
217
|
-
|
|
218
|
-
if (descend)
|
|
219
|
-
{
|
|
220
|
-
// Only the fields declared to hold child models, precomputed per
|
|
221
|
-
// class - not every field, type-tested per value per visit.
|
|
222
|
-
const children = CjsSchema.getSchema(model.constructor).children;
|
|
223
|
-
const start = reverse ? children.length - 1 : 0;
|
|
224
|
-
const end = reverse ? -1 : children.length;
|
|
225
|
-
const step = reverse ? -1 : 1;
|
|
226
|
-
|
|
227
|
-
for (let i = start; i !== end; i += step)
|
|
228
|
-
{
|
|
229
|
-
const child = children[i];
|
|
230
|
-
if (options.ownedOnly === true && !child.owned) continue;
|
|
231
|
-
const value = model[child.name];
|
|
232
|
-
|
|
233
|
-
if (Array.isArray(value))
|
|
234
|
-
{
|
|
235
|
-
const itemStart = reverse ? value.length - 1 : 0;
|
|
236
|
-
const itemEnd = reverse ? -1 : value.length;
|
|
237
|
-
for (let j = itemStart; j !== itemEnd; j += step) visit(value[j]);
|
|
238
|
-
}
|
|
239
|
-
else
|
|
240
|
-
{
|
|
241
|
-
visit(value);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (order === "post") visitor(model);
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
visit(this);
|
|
250
|
-
return this;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Collects unique resources reported by this model graph into an array.
|
|
107
|
+
* Constructs one item from a schema-backed child collection's declared
|
|
108
|
+
* item type, then adds it through the ordinary child-mutation path.
|
|
255
109
|
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
110
|
+
* Domain classes expose named factories such as `CreateAttachment`; this
|
|
111
|
+
* programmatic helper keeps property-string mutation out of their instance
|
|
112
|
+
* API.
|
|
259
113
|
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
* `OnGetResources()` is the escape hatch for resources a model holds
|
|
266
|
-
* outside its schema, such as private fields. It takes no arguments and
|
|
267
|
-
* always returns an iterable of resources - never a bare resource and never
|
|
268
|
-
* nothing. Most models do not implement it.
|
|
269
|
-
*
|
|
270
|
-
* @param {Array<*>} [out=[]] Output array, whose contents are replaced.
|
|
271
|
-
* @returns {Array<*>} The supplied output array.
|
|
114
|
+
* @param {CjsModel} target Owning model instance.
|
|
115
|
+
* @param {string} property Schema `array` or `list` field name.
|
|
116
|
+
* @param {object} [values={}] Plain child values.
|
|
117
|
+
* @param {object} [options={}] Hydration and mutation options.
|
|
118
|
+
* @returns {*} The constructed and added child.
|
|
272
119
|
*/
|
|
273
|
-
|
|
120
|
+
static createChild(target, property, values = {}, options = {})
|
|
274
121
|
{
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
for (const field of CjsSchema.getSchema(model.constructor).resources)
|
|
280
|
-
{
|
|
281
|
-
const value = model[field.name];
|
|
282
|
-
if (Array.isArray(value))
|
|
283
|
-
{
|
|
284
|
-
for (const item of value) AddResource(resources, item);
|
|
285
|
-
}
|
|
286
|
-
else
|
|
287
|
-
{
|
|
288
|
-
AddResource(resources, value);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (typeof model.OnGetResources === "function")
|
|
293
|
-
{
|
|
294
|
-
AddResources(resources, model.OnGetResources());
|
|
295
|
-
}
|
|
296
|
-
return true;
|
|
122
|
+
const { field } = getChildCollection(target, property);
|
|
123
|
+
const imported = importSourceValue([ values ], field, {
|
|
124
|
+
...options,
|
|
125
|
+
ownerConstructor: target.constructor
|
|
297
126
|
});
|
|
127
|
+
const child = imported[0];
|
|
298
128
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
return
|
|
129
|
+
assertChildObject(child, field.name);
|
|
130
|
+
CjsModel.addChild(target, field.name, child, options);
|
|
131
|
+
return child;
|
|
302
132
|
}
|
|
303
133
|
|
|
304
134
|
/**
|
|
305
|
-
*
|
|
135
|
+
* Appends an existing object to a schema-backed child collection.
|
|
306
136
|
*
|
|
307
|
-
* The
|
|
308
|
-
*
|
|
309
|
-
*
|
|
137
|
+
* The mutation invokes Carbon-shaped `OnListModified` when present,
|
|
138
|
+
* records the field's declared flag/rebuild consequences, emits one
|
|
139
|
+
* `childadded` event, and settles the parent unless suppressed by options.
|
|
310
140
|
*
|
|
311
|
-
* @
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
{
|
|
315
|
-
this.__state.MarkDirty();
|
|
316
|
-
return this;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Clears the dirty mark without settling. Rarely correct outside tests
|
|
321
|
-
* and teardown - the settle clears it itself.
|
|
322
|
-
*
|
|
323
|
-
* @returns {CjsModel} This model.
|
|
324
|
-
*/
|
|
325
|
-
ClearDirty()
|
|
326
|
-
{
|
|
327
|
-
this.__state.ClearDirty();
|
|
328
|
-
return this;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Checks whether a settle is owed.
|
|
333
|
-
*
|
|
334
|
-
* @returns {boolean}
|
|
335
|
-
*/
|
|
336
|
-
IsDirty()
|
|
337
|
-
{
|
|
338
|
-
return this.__state.IsDirty();
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Gets the shared schema registry and decorator facade.
|
|
343
|
-
*
|
|
344
|
-
* @returns {typeof CjsSchema}
|
|
345
|
-
*/
|
|
346
|
-
static get schema()
|
|
347
|
-
{
|
|
348
|
-
return CjsSchema;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Exports a model's schema fields into an output object.
|
|
353
|
-
*
|
|
354
|
-
* All options default off, leaving the plain output identical to the
|
|
355
|
-
* historical shape. Options propagate recursively to nested models.
|
|
356
|
-
*
|
|
357
|
-
* @param {CjsModel} value
|
|
358
|
-
* @param {object} [out={}]
|
|
141
|
+
* @param {CjsModel} target Owning model instance.
|
|
142
|
+
* @param {string} property Schema `array` or `list` field name.
|
|
143
|
+
* @param {object} child Existing child object.
|
|
359
144
|
* @param {object} [options={}]
|
|
360
|
-
* @
|
|
361
|
-
* @param {boolean} [options.typeTags] Emits `_type` only where the concrete
|
|
362
|
-
* class is not derivable from the declared field type (the root and
|
|
363
|
-
* polymorphic slots).
|
|
364
|
-
* @param {boolean} [options.forceTypeTags] Emits `_type` on every model.
|
|
365
|
-
* @param {boolean} [options.refs] Tracks shared models: repeats export as
|
|
366
|
-
* `{ _ref }` and their first occurrence carries `_id`. Also guards
|
|
367
|
-
* against cyclic graphs.
|
|
368
|
-
* @param {boolean} [options.forceIDs] Emits `_id` on every model.
|
|
369
|
-
* @param {boolean} [options.keyedLists] Exports a list as a name-keyed
|
|
370
|
-
* object when every item is a model with a unique non-empty `name`;
|
|
371
|
-
* the redundant item `name` field is dropped in that form. Empty
|
|
372
|
-
* lists stay arrays.
|
|
373
|
-
* @param {string} [options.enumFormat] Enum-backed field emission:
|
|
374
|
-
* "values" (default, numeric), "names" (exact member-name strings),
|
|
375
|
-
* or "identity" (`[name, "OwnerClass.EnumName"]` tuples). Unknown
|
|
376
|
-
* numeric values export as raw numbers in every mode.
|
|
377
|
-
* @returns {object} The supplied output object.
|
|
378
|
-
* @throws {TypeError} If the source or output target is invalid.
|
|
145
|
+
* @returns {object} The appended child.
|
|
379
146
|
*/
|
|
380
|
-
static
|
|
147
|
+
static addChild(target, property, child, options = {})
|
|
381
148
|
{
|
|
382
|
-
|
|
383
|
-
{
|
|
384
|
-
throw new TypeError("CjsModel.get requires a CjsModel source.");
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (!out || typeof out !== "object" || Array.isArray(out) || ArrayBuffer.isView(out))
|
|
388
|
-
{
|
|
389
|
-
throw new TypeError("CjsModel.get requires an object output target.");
|
|
390
|
-
}
|
|
149
|
+
const { field, collection } = getChildCollection(target, property);
|
|
391
150
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
for (const field of getModelFields(value))
|
|
395
|
-
{
|
|
396
|
-
out[field.name] = exportSourceValue(value[field.name], options);
|
|
397
|
-
}
|
|
151
|
+
assertChildObject(child, field.name);
|
|
152
|
+
assertChildCallback(options.onAdded, "onAdded");
|
|
398
153
|
|
|
399
|
-
|
|
400
|
-
|
|
154
|
+
const index = collection.length;
|
|
155
|
+
collection.push(child);
|
|
156
|
+
recordChildMutation(target, field, options);
|
|
157
|
+
notifyListModified(target, CHILD_LIST_EVENT.INSERTED, index, 0, child, collection);
|
|
401
158
|
|
|
402
|
-
|
|
159
|
+
const payload = createChildEventPayload(target, field.name, child, index, options);
|
|
160
|
+
invokeChildCallback(options.onAdded, target, payload, "onAdded");
|
|
161
|
+
emitChildEvent(target, "childadded", payload, options);
|
|
162
|
+
settleChildMutation(target, field, options);
|
|
163
|
+
return child;
|
|
403
164
|
}
|
|
404
165
|
|
|
405
166
|
/**
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* Reserved metadata keys are honored, never treated as fields: a string
|
|
409
|
-
* `values._type` must name the target's class or one of its base classes;
|
|
410
|
-
* `values._id` registers the target for `{ _ref }` resolution; a
|
|
411
|
-
* `{ _ref }` incoming field value resolves to the registered instance
|
|
412
|
-
* (shared identity) and throws when the id never resolves.
|
|
167
|
+
* Detaches the first matching object from a schema-backed child collection.
|
|
168
|
+
* Removal never destroys the child.
|
|
413
169
|
*
|
|
414
|
-
* @param {CjsModel}
|
|
415
|
-
* @param {
|
|
170
|
+
* @param {CjsModel} target Owning model instance.
|
|
171
|
+
* @param {string} property Schema `array` or `list` field name.
|
|
172
|
+
* @param {object} child Existing child object.
|
|
416
173
|
* @param {object} [options={}]
|
|
417
|
-
* @
|
|
418
|
-
* @param {boolean} [options.notify=true] Tracks schema notification flags.
|
|
419
|
-
* @param {boolean} [options.skipUpdate=false] Leaves dirty changes unsettled.
|
|
420
|
-
* @param {boolean} [options.skipEvents=false] Suppresses direct modified events.
|
|
421
|
-
* @param {boolean} [options.returnBoolean=false] Returns a boolean instead of changed fields.
|
|
422
|
-
* @param {*} [options.source=out] Origin included in update callbacks and events.
|
|
423
|
-
* @returns {Set<string>|boolean} Changed fields, or a boolean result.
|
|
424
|
-
* @throws {TypeError} If the target is not a model.
|
|
174
|
+
* @returns {boolean} Whether the child was present and removed.
|
|
425
175
|
*/
|
|
426
|
-
static
|
|
176
|
+
static removeChild(target, property, child, options = {})
|
|
427
177
|
{
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
throw new TypeError("CjsModel.set requires a CjsModel target.");
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (!values || typeof values !== "object") return false;
|
|
434
|
-
|
|
435
|
-
if (typeof values._type === "string")
|
|
436
|
-
{
|
|
437
|
-
assertTargetTypeMatches(out, values._type, options);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// One import operation context is shared across the whole call tree so
|
|
441
|
-
// `_id` registrations and `{ _ref }` resolutions see the same identity
|
|
442
|
-
// table. The outermost call owns finalization of forward references.
|
|
443
|
-
const ownsImportContext = !options.importContext;
|
|
444
|
-
const importOptions = {
|
|
445
|
-
...options,
|
|
446
|
-
importContext: options.importContext ?? createImportContext(),
|
|
447
|
-
ownerConstructor: out.constructor
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
if (values._id !== undefined && values._id !== null)
|
|
451
|
-
{
|
|
452
|
-
importOptions.importContext.register(values._id, out);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const enumTranslations = validateEnumInputs(out, values);
|
|
456
|
-
|
|
457
|
-
const changed = new Set();
|
|
458
|
-
for (const field of getModelFields(out))
|
|
459
|
-
{
|
|
460
|
-
if (!isWritableModelField(field)) continue;
|
|
178
|
+
const { field, collection } = getChildCollection(target, property);
|
|
179
|
+
const index = collection.indexOf(child);
|
|
461
180
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
{
|
|
465
|
-
const oldValue = out[field.name];
|
|
466
|
-
const incoming = enumTranslations.has(field.name) ? enumTranslations.get(field.name) : values[key];
|
|
181
|
+
if (index === -1) return false;
|
|
182
|
+
assertChildCallback(options.onRemoved, "onRemoved");
|
|
467
183
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
didChange = applyIncomingReference(out, field, incoming, importOptions);
|
|
472
|
-
}
|
|
473
|
-
else
|
|
474
|
-
{
|
|
475
|
-
// Registered struct fields have value semantics. Constructors may
|
|
476
|
-
// install their canonical struct instance up front; populate that
|
|
477
|
-
// instance rather than replacing it with an imported object.
|
|
478
|
-
const structChanged = applyIncomingStructInPlace(oldValue, incoming, field, importOptions);
|
|
184
|
+
collection.splice(index, 1);
|
|
185
|
+
recordChildMutation(target, field, options);
|
|
186
|
+
notifyListModified(target, CHILD_LIST_EVENT.REMOVED, index, 0, child, collection);
|
|
479
187
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
{
|
|
486
|
-
// Fast path: a math field with an existing compatible typed array
|
|
487
|
-
// is coerced IN PLACE (no allocation, buffer reference preserved).
|
|
488
|
-
const mathChanged = coerceCarbonMathInto(oldValue, incoming, field);
|
|
489
|
-
|
|
490
|
-
if (mathChanged !== null)
|
|
491
|
-
{
|
|
492
|
-
didChange = field.io?.always === true || mathChanged;
|
|
493
|
-
}
|
|
494
|
-
else
|
|
495
|
-
{
|
|
496
|
-
const newValue = importSourceValue(incoming, field, importOptions);
|
|
497
|
-
didChange = field.io?.always === true || !areEquivalentSourceValues(oldValue, newValue);
|
|
498
|
-
if (didChange) out[field.name] = newValue;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
if (didChange)
|
|
504
|
-
{
|
|
505
|
-
changed.add(field.name);
|
|
506
|
-
if (options.markDirty !== false)
|
|
507
|
-
{
|
|
508
|
-
out.__state.dirty = true;
|
|
509
|
-
// Write-time token adds: the knowledge of WHICH field
|
|
510
|
-
// changed lives here, so declared consequences land
|
|
511
|
-
// here (props are not tracked in state).
|
|
512
|
-
if (options.notify !== false)
|
|
513
|
-
{
|
|
514
|
-
addDeclaredFieldTokens(out, field);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if (ownsImportContext)
|
|
522
|
-
{
|
|
523
|
-
importOptions.importContext.finalize();
|
|
524
|
-
importOptions.importContext.initializeCreated(importOptions);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
if (changed.size && options.markDirty === false)
|
|
528
|
-
{
|
|
529
|
-
if (options.skipUpdate !== true && options.skipEvents !== true && out.__state.suppressEvents === 0)
|
|
530
|
-
{
|
|
531
|
-
out.EmitEvent("modified", out, createModifiedPayload(changed, options.source ?? out));
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
else if (changed.size && options.skipUpdate !== true && !out.__state.updating)
|
|
535
|
-
{
|
|
536
|
-
out.UpdateValues(options);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
return options.returnBoolean === true ? changed.size > 0 : changed.size ? changed : false;
|
|
188
|
+
const payload = createChildEventPayload(target, field.name, child, index, options);
|
|
189
|
+
invokeChildCallback(options.onRemoved, target, payload, "onRemoved");
|
|
190
|
+
emitChildEvent(target, "childremoved", payload, options);
|
|
191
|
+
settleChildMutation(target, field, options);
|
|
192
|
+
return true;
|
|
540
193
|
}
|
|
541
194
|
|
|
542
195
|
/**
|
|
543
|
-
*
|
|
196
|
+
* Removes a child and then performs an explicit deletion action.
|
|
197
|
+
*
|
|
198
|
+
* `options.delete` owns domain-specific teardown when supplied. Without
|
|
199
|
+
* that explicit hook the child is detached and left to ordinary
|
|
200
|
+
* JavaScript lifetime management. Deletion emits both `childremoved` and
|
|
201
|
+
* `childdeleted`.
|
|
544
202
|
*
|
|
545
|
-
* @param {CjsModel}
|
|
546
|
-
* @param {
|
|
203
|
+
* @param {CjsModel} target Owning model instance.
|
|
204
|
+
* @param {string} property Schema `array` or `list` field name.
|
|
205
|
+
* @param {object} child Existing child object.
|
|
547
206
|
* @param {object} [options={}]
|
|
548
|
-
* @
|
|
549
|
-
* @
|
|
207
|
+
* @param {Function} [options.delete] Explicit child teardown callback.
|
|
208
|
+
* @returns {boolean} Whether the child was present and deleted.
|
|
550
209
|
*/
|
|
551
|
-
static
|
|
210
|
+
static deleteChild(target, property, child, options = {})
|
|
552
211
|
{
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
throw new TypeError("CjsModel.copy requires a CjsModel target.");
|
|
556
|
-
}
|
|
212
|
+
const { field, collection } = getChildCollection(target, property);
|
|
213
|
+
const index = collection.indexOf(child);
|
|
557
214
|
|
|
558
|
-
if (
|
|
559
|
-
{
|
|
560
|
-
throw new TypeError("CjsModel.copy requires a CjsModel source.");
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
CjsModel.set(out, CjsModel.get(value, {}, options), options);
|
|
564
|
-
return out;
|
|
565
|
-
}
|
|
215
|
+
if (index === -1) return false;
|
|
566
216
|
|
|
567
|
-
|
|
568
|
-
* Deep-merges ordered value sources and applies the result with one set call.
|
|
569
|
-
*
|
|
570
|
-
* @param {CjsModel} out
|
|
571
|
-
* @param {Array<object|CjsModel>} [values=[]]
|
|
572
|
-
* @param {object} [options={}]
|
|
573
|
-
* @returns {Set<string>|boolean} The result returned by {@link CjsModel.set}.
|
|
574
|
-
* @throws {TypeError} If the target, source array, or options are invalid.
|
|
575
|
-
*/
|
|
576
|
-
static merge(out, values = [], options = {})
|
|
577
|
-
{
|
|
578
|
-
if (!(out instanceof CjsModel))
|
|
217
|
+
if (options.delete !== undefined && typeof options.delete !== "function")
|
|
579
218
|
{
|
|
580
|
-
throw new TypeError("CjsModel
|
|
219
|
+
throw new TypeError("CjsModel child delete option must be a function.");
|
|
581
220
|
}
|
|
221
|
+
assertChildCallback(options.onDeleted, "onDeleted");
|
|
582
222
|
|
|
583
|
-
|
|
584
|
-
{
|
|
585
|
-
throw new TypeError("CjsModel.merge requires an array of value sources.");
|
|
586
|
-
}
|
|
223
|
+
CjsModel.removeChild(target, field.name, child, { ...options, skipUpdate: true });
|
|
587
224
|
|
|
588
|
-
if (
|
|
225
|
+
if (typeof options.delete === "function")
|
|
589
226
|
{
|
|
590
|
-
|
|
227
|
+
options.delete.call(target, child, options);
|
|
591
228
|
}
|
|
592
229
|
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
230
|
+
const payload = createChildEventPayload(target, field.name, child, index, options);
|
|
231
|
+
invokeChildCallback(options.onDeleted, target, payload, "onDeleted");
|
|
232
|
+
emitChildEvent(target, "childdeleted", payload, options);
|
|
233
|
+
settleChildMutation(target, field, options);
|
|
234
|
+
return true;
|
|
596
235
|
}
|
|
597
236
|
|
|
598
237
|
/**
|
|
599
|
-
*
|
|
238
|
+
* Detaches every object from a schema-backed child collection without
|
|
239
|
+
* destroying the children.
|
|
600
240
|
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
241
|
+
* Carbon-shaped `OnListModified` receives its unload-start callback while
|
|
242
|
+
* the collection is still populated. The public domain method decides
|
|
243
|
+
* whether clearing or per-child deletion is appropriate.
|
|
603
244
|
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
* `values._id` registers the instance in the import operation context
|
|
607
|
-
* before any field descends, so `{ _ref }` values elsewhere in the same
|
|
608
|
-
* operation — including cycles and self-references — resolve to this
|
|
609
|
-
* instance. The outermost call finalizes forward references before
|
|
610
|
-
* initialization; an unresolved `_ref` throws.
|
|
611
|
-
*
|
|
612
|
-
* @param {object} [values={}]
|
|
245
|
+
* @param {CjsModel} target Owning model instance.
|
|
246
|
+
* @param {string} property Schema `array` or `list` field name.
|
|
613
247
|
* @param {object} [options={}]
|
|
614
|
-
* @returns {
|
|
615
|
-
* @throws {Error} If any owned model explicitly fails initialization.
|
|
248
|
+
* @returns {boolean} Whether any children were cleared.
|
|
616
249
|
*/
|
|
617
|
-
static
|
|
250
|
+
static clearChildren(target, property, options = {})
|
|
618
251
|
{
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
throw new TypeError(`${CjsSchema.getClassName(this) || this.name}.from cannot construct from a { _ref } value; references resolve only inside the owning import operation.`);
|
|
622
|
-
}
|
|
252
|
+
const { field, collection } = getChildCollection(target, property);
|
|
253
|
+
const count = collection.length;
|
|
623
254
|
|
|
624
|
-
if (
|
|
625
|
-
|
|
626
|
-
const Constructor = resolveRegisteredModelClass(values._type, options);
|
|
627
|
-
if (Constructor !== this)
|
|
628
|
-
{
|
|
629
|
-
if (!(Constructor.prototype instanceof this))
|
|
630
|
-
{
|
|
631
|
-
throw new TypeError(`_type "${values._type}" is not ${CjsSchema.getClassName(this) || this.name} or one of its registered subclasses.`);
|
|
632
|
-
}
|
|
633
|
-
return Constructor.from(values, options);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
const ownsImportContext = !options.importContext;
|
|
638
|
-
const importOptions = ownsImportContext
|
|
639
|
-
? { ...options, importContext: createImportContext() }
|
|
640
|
-
: options;
|
|
641
|
-
|
|
642
|
-
const result = new this();
|
|
643
|
-
|
|
644
|
-
importOptions.importContext.registerCreated(result);
|
|
645
|
-
|
|
646
|
-
// Register-before-descent: the instance is visible to `_ref` lookups
|
|
647
|
-
// before its own fields import, so back-references and cycles work.
|
|
648
|
-
if (values && typeof values === "object" && values._id !== undefined && values._id !== null)
|
|
649
|
-
{
|
|
650
|
-
importOptions.importContext.register(values._id, result);
|
|
651
|
-
}
|
|
255
|
+
if (!count) return false;
|
|
256
|
+
assertChildCallback(options.onCleared, "onCleared");
|
|
652
257
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
result.SetValues(values, {
|
|
657
|
-
...importOptions,
|
|
658
|
-
skipEvents: true,
|
|
659
|
-
skipUpdate: true
|
|
660
|
-
});
|
|
258
|
+
recordChildMutation(target, field, options);
|
|
259
|
+
notifyListModified(target, CHILD_LIST_EVENT.UNLOAD_START, 0, 0, null, collection);
|
|
260
|
+
collection.length = 0;
|
|
661
261
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
finally
|
|
672
|
-
{
|
|
673
|
-
result.__state.suppressEvents--;
|
|
674
|
-
}
|
|
675
|
-
return result;
|
|
262
|
+
const payload = {
|
|
263
|
+
property: field.name,
|
|
264
|
+
count,
|
|
265
|
+
source: options.source ?? target
|
|
266
|
+
};
|
|
267
|
+
invokeChildCallback(options.onCleared, target, payload, "onCleared");
|
|
268
|
+
emitChildEvent(target, "childrencleared", payload, options);
|
|
269
|
+
settleChildMutation(target, field, options);
|
|
270
|
+
return true;
|
|
676
271
|
}
|
|
677
272
|
|
|
678
273
|
/**
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
if (
|
|
797
|
-
{
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
{
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
}
|
|
864
|
-
|
|
274
|
+
* Applies pending changes: drives the OnModified hook until the model
|
|
275
|
+
* settles, clears the dirty mark, and emits one final modified event.
|
|
276
|
+
*
|
|
277
|
+
* Calling this IS the "I made changes, apply please" contract: it always
|
|
278
|
+
* runs at least one hook pass, dirty or not, so direct/untracked
|
|
279
|
+
* mutations (the cooperative-pipeline reality) can be applied
|
|
280
|
+
* explicitly. Class Update/per-frame methods typically gate on
|
|
281
|
+
* `__state.IsDirty()` before calling.
|
|
282
|
+
*
|
|
283
|
+
* @param {object} [options={}]
|
|
284
|
+
* @param {string|Iterable<string>} [options.property] Fields the caller changed directly; their declared flag/rebuild tokens are added first.
|
|
285
|
+
* @param {string|Iterable<string>} [options.properties] Alias of `property`.
|
|
286
|
+
* @param {*} [options.source=this] Origin forwarded to the hook and event (binding feedback control).
|
|
287
|
+
* @param {boolean} [options.skipEvents=false] Prevents the final modified event.
|
|
288
|
+
* @returns {boolean} False when the hook rejected the update (dirty is retained).
|
|
289
|
+
* @throws {Error} If local changes do not settle within the update-pass limit.
|
|
290
|
+
*/
|
|
291
|
+
UpdateValues(options = {})
|
|
292
|
+
{
|
|
293
|
+
addExplicitUpdateProperties(this, options.property ?? options.properties);
|
|
294
|
+
if (this.__state.updating) return true;
|
|
295
|
+
|
|
296
|
+
const source = options.source ?? this;
|
|
297
|
+
this.__state.updating = true;
|
|
298
|
+
|
|
299
|
+
try
|
|
300
|
+
{
|
|
301
|
+
for (let pass = 0; ; pass++)
|
|
302
|
+
{
|
|
303
|
+
if (pass >= MAX_UPDATE_PASSES)
|
|
304
|
+
{
|
|
305
|
+
throw new Error(`${CjsSchema.getClassName(this.constructor)}.UpdateValues exceeded ${MAX_UPDATE_PASSES} local settle passes.`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this.__state.dirty = false;
|
|
309
|
+
|
|
310
|
+
if (this.OnModified({ ...options, source }) === false)
|
|
311
|
+
{
|
|
312
|
+
this.__state.dirty = true;
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!this.__state.dirty) break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
catch (err)
|
|
320
|
+
{
|
|
321
|
+
this.__state.dirty = true;
|
|
322
|
+
throw err;
|
|
323
|
+
}
|
|
324
|
+
finally
|
|
325
|
+
{
|
|
326
|
+
this.__state.updating = false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (options.skipEvents !== true && this.__state.suppressEvents === 0)
|
|
330
|
+
{
|
|
331
|
+
this.EmitEvent("modified", this, { source });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* The settle hook: reproduces the meaningful consequences of the
|
|
339
|
+
* corresponding Carbon INotify::OnModified implementation.
|
|
340
|
+
*
|
|
341
|
+
* Invoked only by UpdateValues. Receives the mutation options bag
|
|
342
|
+
* (source, caller context, skipEvents, ...). There is no changed-property
|
|
343
|
+
* list - the pipeline is cooperative and cannot guarantee one - so
|
|
344
|
+
* overrides are written broad-safe: consult own state, compare cached
|
|
345
|
+
* derivations, and rely on `__state.flags`/`__state.rebuild` tokens for
|
|
346
|
+
* targeted signals. Returning `false` rejects the update and retains the
|
|
347
|
+
* dirty mark.
|
|
348
|
+
*
|
|
349
|
+
* @param {object} [options={}]
|
|
350
|
+
* @returns {boolean} Whether the update may complete.
|
|
351
|
+
*/
|
|
352
|
+
OnModified(options = {})
|
|
353
|
+
{
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Visits this model and its schema-backed child models without revisiting cycles.
|
|
359
|
+
*
|
|
360
|
+
* In pre-order traversal, returning `false` prunes that model's descendants.
|
|
361
|
+
* Visitor return values are ignored in post-order traversal.
|
|
362
|
+
*
|
|
363
|
+
* @param {function(CjsModel): (boolean|void)} visitor
|
|
364
|
+
* @param {object} [options={}]
|
|
365
|
+
* @param {Set<CjsModel>} [options.visited] Existing cycle-detection set.
|
|
366
|
+
* @param {"pre"|"post"} [options.order="pre"]
|
|
367
|
+
* @param {boolean} [options.reverse=false] Reverses field and list-item order.
|
|
368
|
+
* @param {boolean} [options.ownedOnly=false] Traverses only owned relationships.
|
|
369
|
+
* @returns {CjsModel} This model.
|
|
370
|
+
* @throws {TypeError} If `visitor` is not a function.
|
|
371
|
+
*/
|
|
372
|
+
Traverse(visitor, options = {})
|
|
373
|
+
{
|
|
374
|
+
if (typeof visitor !== "function")
|
|
375
|
+
{
|
|
376
|
+
throw new TypeError("CjsModel.Traverse requires a visitor function.");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const visited = options.visited instanceof Set ? options.visited : new Set();
|
|
380
|
+
const order = options.order === "post" ? "post" : "pre";
|
|
381
|
+
const reverse = options.reverse === true;
|
|
382
|
+
|
|
383
|
+
const visit = model =>
|
|
384
|
+
{
|
|
385
|
+
if (!(model instanceof CjsModel) || visited.has(model)) return;
|
|
386
|
+
visited.add(model);
|
|
387
|
+
|
|
388
|
+
let descend = true;
|
|
389
|
+
if (order === "pre") descend = visitor(model) !== false;
|
|
390
|
+
|
|
391
|
+
if (descend)
|
|
392
|
+
{
|
|
393
|
+
// Only the fields declared to hold child models, precomputed per
|
|
394
|
+
// class - not every field, type-tested per value per visit.
|
|
395
|
+
const children = CjsSchema.getSchema(model.constructor).children;
|
|
396
|
+
const start = reverse ? children.length - 1 : 0;
|
|
397
|
+
const end = reverse ? -1 : children.length;
|
|
398
|
+
const step = reverse ? -1 : 1;
|
|
399
|
+
|
|
400
|
+
for (let i = start; i !== end; i += step)
|
|
401
|
+
{
|
|
402
|
+
const child = children[i];
|
|
403
|
+
if (options.ownedOnly === true && !child.owned) continue;
|
|
404
|
+
const value = model[child.name];
|
|
405
|
+
|
|
406
|
+
if (Array.isArray(value))
|
|
407
|
+
{
|
|
408
|
+
const itemStart = reverse ? value.length - 1 : 0;
|
|
409
|
+
const itemEnd = reverse ? -1 : value.length;
|
|
410
|
+
for (let j = itemStart; j !== itemEnd; j += step) visit(value[j]);
|
|
411
|
+
}
|
|
412
|
+
else
|
|
413
|
+
{
|
|
414
|
+
visit(value);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (order === "post") visitor(model);
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
visit(this);
|
|
423
|
+
return this;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Collects unique resources reported by this model graph into an array.
|
|
428
|
+
*
|
|
429
|
+
* Every model in the graph is visited: reporting resources does not hide a
|
|
430
|
+
* model's descendants, because an under-reported dependency set would let
|
|
431
|
+
* readiness checks pass while a child's resources were still loading.
|
|
432
|
+
*
|
|
433
|
+
* Resources held in schema fields are collected automatically - they are
|
|
434
|
+
* already declared, as `@type.objectRef("TriGeometryRes")` and friends, so
|
|
435
|
+
* restating them in a hook would be the hand-written relay chain this
|
|
436
|
+
* traversal exists to replace.
|
|
437
|
+
*
|
|
438
|
+
* `OnGetResources()` is the escape hatch for resources a model holds
|
|
439
|
+
* outside its schema, such as private fields. It takes no arguments and
|
|
440
|
+
* always returns an iterable of resources - never a bare resource and never
|
|
441
|
+
* nothing. Most models do not implement it.
|
|
442
|
+
*
|
|
443
|
+
* @param {Array<*>} [out=[]] Output array, whose contents are replaced.
|
|
444
|
+
* @returns {Array<*>} The supplied output array.
|
|
445
|
+
*/
|
|
446
|
+
GetResources(out = [])
|
|
447
|
+
{
|
|
448
|
+
const resources = new Set();
|
|
449
|
+
|
|
450
|
+
this.Traverse(model =>
|
|
451
|
+
{
|
|
452
|
+
for (const field of CjsSchema.getSchema(model.constructor).resources)
|
|
453
|
+
{
|
|
454
|
+
const value = model[field.name];
|
|
455
|
+
if (Array.isArray(value))
|
|
456
|
+
{
|
|
457
|
+
for (const item of value) AddResource(resources, item);
|
|
458
|
+
}
|
|
459
|
+
else
|
|
460
|
+
{
|
|
461
|
+
AddResource(resources, value);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (typeof model.OnGetResources === "function")
|
|
466
|
+
{
|
|
467
|
+
AddResources(resources, model.OnGetResources());
|
|
468
|
+
}
|
|
469
|
+
return true;
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
out.length = 0;
|
|
473
|
+
out.push(...resources);
|
|
474
|
+
return out;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Marks the model as changed; the next settle applies it.
|
|
479
|
+
*
|
|
480
|
+
* The cooperative-pipeline contract: anything mutating outside
|
|
481
|
+
* `SetValues` (direct writes, Object.assign, reader adapters) owes this
|
|
482
|
+
* call or an explicit `UpdateValues()`.
|
|
483
|
+
*
|
|
484
|
+
* @returns {CjsModel} This model.
|
|
485
|
+
*/
|
|
486
|
+
MarkDirty()
|
|
487
|
+
{
|
|
488
|
+
this.__state.MarkDirty();
|
|
489
|
+
return this;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Clears the dirty mark without settling. Rarely correct outside tests
|
|
494
|
+
* and teardown - the settle clears it itself.
|
|
495
|
+
*
|
|
496
|
+
* @returns {CjsModel} This model.
|
|
497
|
+
*/
|
|
498
|
+
ClearDirty()
|
|
499
|
+
{
|
|
500
|
+
this.__state.ClearDirty();
|
|
501
|
+
return this;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Checks whether a settle is owed.
|
|
506
|
+
*
|
|
507
|
+
* @returns {boolean}
|
|
508
|
+
*/
|
|
509
|
+
IsDirty()
|
|
510
|
+
{
|
|
511
|
+
return this.__state.IsDirty();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Gets the shared schema registry and decorator facade.
|
|
516
|
+
*
|
|
517
|
+
* @returns {typeof CjsSchema}
|
|
518
|
+
*/
|
|
519
|
+
static get schema()
|
|
520
|
+
{
|
|
521
|
+
return CjsSchema;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Exports a model's schema fields into an output object.
|
|
526
|
+
*
|
|
527
|
+
* All options default off, leaving the plain output identical to the
|
|
528
|
+
* historical shape. Options propagate recursively to nested models.
|
|
529
|
+
*
|
|
530
|
+
* @param {CjsModel} value
|
|
531
|
+
* @param {object} [out={}]
|
|
532
|
+
* @param {object} [options={}]
|
|
533
|
+
* @param {boolean} [options.persistOnly] Exports only persisted fields.
|
|
534
|
+
* @param {boolean} [options.typeTags] Emits `_type` only where the concrete
|
|
535
|
+
* class is not derivable from the declared field type (the root and
|
|
536
|
+
* polymorphic slots).
|
|
537
|
+
* @param {boolean} [options.forceTypeTags] Emits `_type` on every model.
|
|
538
|
+
* @param {boolean} [options.refs] Tracks shared models: repeats export as
|
|
539
|
+
* `{ _ref }` and their first occurrence carries `_id`. Also guards
|
|
540
|
+
* against cyclic graphs.
|
|
541
|
+
* @param {boolean} [options.forceIDs] Emits `_id` on every model.
|
|
542
|
+
* @param {boolean} [options.keyedLists] Exports a list as a name-keyed
|
|
543
|
+
* object when every item is a model with a unique non-empty `name`;
|
|
544
|
+
* the redundant item `name` field is dropped in that form. Empty
|
|
545
|
+
* lists stay arrays.
|
|
546
|
+
* @param {string} [options.enumFormat] Enum-backed field emission:
|
|
547
|
+
* "values" (default, numeric), "names" (exact member-name strings),
|
|
548
|
+
* or "identity" (`[name, "OwnerClass.EnumName"]` tuples). Unknown
|
|
549
|
+
* numeric values export as raw numbers in every mode.
|
|
550
|
+
* @returns {object} The supplied output object.
|
|
551
|
+
* @throws {TypeError} If the source or output target is invalid.
|
|
552
|
+
*/
|
|
553
|
+
static get(value, out = {}, options = {})
|
|
554
|
+
{
|
|
555
|
+
if (!(value instanceof CjsModel))
|
|
556
|
+
{
|
|
557
|
+
throw new TypeError("CjsModel.get requires a CjsModel source.");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (!out || typeof out !== "object" || Array.isArray(out) || ArrayBuffer.isView(out))
|
|
561
|
+
{
|
|
562
|
+
throw new TypeError("CjsModel.get requires an object output target.");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (!hasAdvancedExportOptions(options))
|
|
566
|
+
{
|
|
567
|
+
for (const field of getModelFields(value))
|
|
568
|
+
{
|
|
569
|
+
out[field.name] = exportSourceValue(value[field.name], options);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return out;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return exportModelInto(value, out, null, options, createExportContext(value, options));
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Applies schema-backed values to a model and processes resulting updates.
|
|
580
|
+
*
|
|
581
|
+
* Reserved metadata keys are honored, never treated as fields: a string
|
|
582
|
+
* `values._type` must name the target's class or one of its base classes;
|
|
583
|
+
* `values._id` registers the target for `{ _ref }` resolution; a
|
|
584
|
+
* `{ _ref }` incoming field value resolves to the registered instance
|
|
585
|
+
* (shared identity) and throws when the id never resolves.
|
|
586
|
+
*
|
|
587
|
+
* @param {CjsModel} out
|
|
588
|
+
* @param {object} [values={}]
|
|
589
|
+
* @param {object} [options={}]
|
|
590
|
+
* @param {boolean} [options.markDirty=true] Tracks changed properties and notification flags.
|
|
591
|
+
* @param {boolean} [options.notify=true] Tracks schema notification flags.
|
|
592
|
+
* @param {boolean} [options.skipUpdate=false] Leaves dirty changes unsettled.
|
|
593
|
+
* @param {boolean} [options.skipEvents=false] Suppresses direct modified events.
|
|
594
|
+
* @param {boolean} [options.returnBoolean=false] Returns a boolean instead of changed fields.
|
|
595
|
+
* @param {*} [options.source=out] Origin included in update callbacks and events.
|
|
596
|
+
* @returns {Set<string>|boolean} Changed fields, or a boolean result.
|
|
597
|
+
* @throws {TypeError} If the target is not a model.
|
|
598
|
+
*/
|
|
599
|
+
static set(out, values = {}, options = {})
|
|
600
|
+
{
|
|
601
|
+
if (!(out instanceof CjsModel))
|
|
602
|
+
{
|
|
603
|
+
throw new TypeError("CjsModel.set requires a CjsModel target.");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (!values || typeof values !== "object") return false;
|
|
607
|
+
|
|
608
|
+
if (typeof values._type === "string")
|
|
609
|
+
{
|
|
610
|
+
assertTargetTypeMatches(out, values._type, options);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// One import operation context is shared across the whole call tree so
|
|
614
|
+
// `_id` registrations and `{ _ref }` resolutions see the same identity
|
|
615
|
+
// table. The outermost call owns finalization of forward references.
|
|
616
|
+
const ownsImportContext = !options.importContext;
|
|
617
|
+
const importOptions = {
|
|
618
|
+
...options,
|
|
619
|
+
importContext: options.importContext ?? createImportContext(),
|
|
620
|
+
ownerConstructor: out.constructor
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
if (values._id !== undefined && values._id !== null)
|
|
624
|
+
{
|
|
625
|
+
importOptions.importContext.register(values._id, out);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const enumTranslations = validateEnumInputs(out, values);
|
|
629
|
+
|
|
630
|
+
const changed = new Set();
|
|
631
|
+
for (const field of getModelFields(out))
|
|
632
|
+
{
|
|
633
|
+
if (!isWritableModelField(field)) continue;
|
|
634
|
+
|
|
635
|
+
const key = findIncomingKey(values, field);
|
|
636
|
+
if (key !== null)
|
|
637
|
+
{
|
|
638
|
+
const oldValue = out[field.name];
|
|
639
|
+
const incoming = enumTranslations.has(field.name) ? enumTranslations.get(field.name) : values[key];
|
|
640
|
+
|
|
641
|
+
let didChange;
|
|
642
|
+
if (isReferenceValue(incoming))
|
|
643
|
+
{
|
|
644
|
+
didChange = applyIncomingReference(out, field, incoming, importOptions);
|
|
645
|
+
}
|
|
646
|
+
else
|
|
647
|
+
{
|
|
648
|
+
// Registered struct fields have value semantics. Constructors may
|
|
649
|
+
// install their canonical struct instance up front; populate that
|
|
650
|
+
// instance rather than replacing it with an imported object.
|
|
651
|
+
const structChanged = applyIncomingStructInPlace(oldValue, incoming, field, importOptions);
|
|
652
|
+
|
|
653
|
+
if (structChanged !== null)
|
|
654
|
+
{
|
|
655
|
+
didChange = field.io?.always === true || structChanged;
|
|
656
|
+
}
|
|
657
|
+
else
|
|
658
|
+
{
|
|
659
|
+
// Fast path: a math field with an existing compatible typed array
|
|
660
|
+
// is coerced IN PLACE (no allocation, buffer reference preserved).
|
|
661
|
+
const mathChanged = coerceCarbonMathInto(oldValue, incoming, field);
|
|
662
|
+
|
|
663
|
+
if (mathChanged !== null)
|
|
664
|
+
{
|
|
665
|
+
didChange = field.io?.always === true || mathChanged;
|
|
666
|
+
}
|
|
667
|
+
else
|
|
668
|
+
{
|
|
669
|
+
const newValue = importSourceValue(incoming, field, importOptions);
|
|
670
|
+
didChange = field.io?.always === true || !areEquivalentSourceValues(oldValue, newValue);
|
|
671
|
+
if (didChange) out[field.name] = newValue;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (didChange)
|
|
677
|
+
{
|
|
678
|
+
changed.add(field.name);
|
|
679
|
+
if (options.markDirty !== false)
|
|
680
|
+
{
|
|
681
|
+
out.__state.dirty = true;
|
|
682
|
+
// Write-time token adds: the knowledge of WHICH field
|
|
683
|
+
// changed lives here, so declared consequences land
|
|
684
|
+
// here (props are not tracked in state).
|
|
685
|
+
if (options.notify !== false)
|
|
686
|
+
{
|
|
687
|
+
addDeclaredFieldTokens(out, field);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (ownsImportContext)
|
|
695
|
+
{
|
|
696
|
+
importOptions.importContext.finalize();
|
|
697
|
+
importOptions.importContext.initializeCreated(importOptions);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (changed.size && options.markDirty === false)
|
|
701
|
+
{
|
|
702
|
+
if (options.skipUpdate !== true && options.skipEvents !== true && out.__state.suppressEvents === 0)
|
|
703
|
+
{
|
|
704
|
+
out.EmitEvent("modified", out, createModifiedPayload(changed, options.source ?? out));
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
else if (changed.size && options.skipUpdate !== true && !out.__state.updating)
|
|
708
|
+
{
|
|
709
|
+
out.UpdateValues(options);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return options.returnBoolean === true ? changed.size > 0 : changed.size ? changed : false;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Copies all exported fields from one model into another.
|
|
717
|
+
*
|
|
718
|
+
* @param {CjsModel} out
|
|
719
|
+
* @param {CjsModel} value
|
|
720
|
+
* @param {object} [options={}]
|
|
721
|
+
* @returns {CjsModel} The target model.
|
|
722
|
+
* @throws {TypeError} If either argument is not a model.
|
|
723
|
+
*/
|
|
724
|
+
static copy(out, value, options = {})
|
|
725
|
+
{
|
|
726
|
+
if (!(out instanceof CjsModel))
|
|
727
|
+
{
|
|
728
|
+
throw new TypeError("CjsModel.copy requires a CjsModel target.");
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (!(value instanceof CjsModel))
|
|
732
|
+
{
|
|
733
|
+
throw new TypeError("CjsModel.copy requires a CjsModel source.");
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
CjsModel.set(out, CjsModel.get(value, {}, options), options);
|
|
737
|
+
return out;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Deep-merges ordered value sources and applies the result with one set call.
|
|
742
|
+
*
|
|
743
|
+
* @param {CjsModel} out
|
|
744
|
+
* @param {Array<object|CjsModel>} [values=[]]
|
|
745
|
+
* @param {object} [options={}]
|
|
746
|
+
* @returns {Set<string>|boolean} The result returned by {@link CjsModel.set}.
|
|
747
|
+
* @throws {TypeError} If the target, source array, or options are invalid.
|
|
748
|
+
*/
|
|
749
|
+
static merge(out, values = [], options = {})
|
|
750
|
+
{
|
|
751
|
+
if (!(out instanceof CjsModel))
|
|
752
|
+
{
|
|
753
|
+
throw new TypeError("CjsModel.merge requires a CjsModel target.");
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (!Array.isArray(values))
|
|
757
|
+
{
|
|
758
|
+
throw new TypeError("CjsModel.merge requires an array of value sources.");
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (!options || typeof options !== "object" || Array.isArray(options) || ArrayBuffer.isView(options))
|
|
762
|
+
{
|
|
763
|
+
throw new TypeError("CjsModel.merge requires an options object.");
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const merged = {};
|
|
767
|
+
for (const value of values) mergeValueBag(merged, value, options);
|
|
768
|
+
return CjsModel.set(out, merged, options);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Constructs, populates, initializes, and cleans an owned model graph.
|
|
773
|
+
*
|
|
774
|
+
* The invoked constructor must support zero arguments. Initial population
|
|
775
|
+
* suppresses updates and events; owned children initialize before parents.
|
|
776
|
+
*
|
|
777
|
+
* A string `values._type` selects the concrete constructor: it must name
|
|
778
|
+
* this class or a registered subclass, otherwise a TypeError is thrown. A
|
|
779
|
+
* `values._id` registers the instance in the import operation context
|
|
780
|
+
* before any field descends, so `{ _ref }` values elsewhere in the same
|
|
781
|
+
* operation — including cycles and self-references — resolve to this
|
|
782
|
+
* instance. The outermost call finalizes forward references before
|
|
783
|
+
* initialization; an unresolved `_ref` throws.
|
|
784
|
+
*
|
|
785
|
+
* @param {object} [values={}]
|
|
786
|
+
* @param {object} [options={}]
|
|
787
|
+
* @returns {CjsModel} An instance of the invoked model constructor.
|
|
788
|
+
* @throws {Error} If any owned model explicitly fails initialization.
|
|
789
|
+
*/
|
|
790
|
+
static from(values = {}, options = {})
|
|
791
|
+
{
|
|
792
|
+
if (isReferenceValue(values))
|
|
793
|
+
{
|
|
794
|
+
throw new TypeError(`${CjsSchema.getClassName(this) || this.name}.from cannot construct from a { _ref } value; references resolve only inside the owning import operation.`);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (values && typeof values === "object" && typeof values._type === "string")
|
|
798
|
+
{
|
|
799
|
+
const Constructor = resolveRegisteredModelClass(values._type, options);
|
|
800
|
+
if (Constructor !== this)
|
|
801
|
+
{
|
|
802
|
+
if (!(Constructor.prototype instanceof this))
|
|
803
|
+
{
|
|
804
|
+
throw new TypeError(`_type "${values._type}" is not ${CjsSchema.getClassName(this) || this.name} or one of its registered subclasses.`);
|
|
805
|
+
}
|
|
806
|
+
return Constructor.from(values, options);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const ownsImportContext = !options.importContext;
|
|
811
|
+
const importOptions = ownsImportContext
|
|
812
|
+
? { ...options, importContext: createImportContext() }
|
|
813
|
+
: options;
|
|
814
|
+
|
|
815
|
+
const result = new this();
|
|
816
|
+
|
|
817
|
+
importOptions.importContext.registerCreated(result);
|
|
818
|
+
|
|
819
|
+
// Register-before-descent: the instance is visible to `_ref` lookups
|
|
820
|
+
// before its own fields import, so back-references and cycles work.
|
|
821
|
+
if (values && typeof values === "object" && values._id !== undefined && values._id !== null)
|
|
822
|
+
{
|
|
823
|
+
importOptions.importContext.register(values._id, result);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
result.__state.suppressEvents++;
|
|
827
|
+
try
|
|
828
|
+
{
|
|
829
|
+
result.SetValues(values, {
|
|
830
|
+
...importOptions,
|
|
831
|
+
skipEvents: true,
|
|
832
|
+
skipUpdate: true
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
if (ownsImportContext)
|
|
836
|
+
{
|
|
837
|
+
importOptions.importContext.finalize();
|
|
838
|
+
importOptions.importContext.initializeCreated({
|
|
839
|
+
...importOptions,
|
|
840
|
+
initChildren: true
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
finally
|
|
845
|
+
{
|
|
846
|
+
result.__state.suppressEvents--;
|
|
847
|
+
}
|
|
848
|
+
return result;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Constructs a model from another model-like value or a raw value bag.
|
|
853
|
+
*
|
|
854
|
+
* @param {CjsModel|object|null} value
|
|
855
|
+
* @param {object} [options={}]
|
|
856
|
+
* @returns {CjsModel} An instance of the invoked model constructor.
|
|
857
|
+
*/
|
|
858
|
+
static clone(value, options = {})
|
|
859
|
+
{
|
|
860
|
+
if (!value || typeof value.GetValues !== "function")
|
|
861
|
+
{
|
|
862
|
+
return this.from(value || {}, options);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return this.from(value.GetValues(options), options);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
CjsSchema.define(CjsModel, { className: "CjsModel" });
|
|
871
|
+
|
|
872
|
+
export const carbon = CjsSchema.carbon;
|
|
873
|
+
export { CjsSchema };
|
|
874
|
+
export const impl = CjsSchema.impl;
|
|
875
|
+
export const io = CjsSchema.io;
|
|
876
|
+
export const jessica = CjsSchema.jessica;
|
|
877
|
+
export const schema = CjsSchema;
|
|
878
|
+
export const type = CjsSchema.type;
|
|
879
|
+
|
|
880
|
+
function mergeValueBag(out, value, options = {})
|
|
881
|
+
{
|
|
882
|
+
const source = value instanceof CjsModel ? CjsModel.get(value, {}, options) : value;
|
|
883
|
+
if (!isPlainRecord(source)) return out;
|
|
884
|
+
|
|
885
|
+
for (const [key, incoming] of Object.entries(source))
|
|
886
|
+
{
|
|
887
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
888
|
+
|
|
889
|
+
const normalized = incoming instanceof CjsModel ? CjsModel.get(incoming, {}, options) : incoming;
|
|
890
|
+
if (isPlainRecord(normalized))
|
|
891
|
+
{
|
|
892
|
+
if (!isPlainRecord(out[key])) out[key] = {};
|
|
893
|
+
mergeValueBag(out[key], normalized, options);
|
|
894
|
+
}
|
|
895
|
+
else
|
|
896
|
+
{
|
|
897
|
+
out[key] = normalized;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return out;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function isPlainRecord(value)
|
|
904
|
+
{
|
|
905
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || ArrayBuffer.isView(value)) return false;
|
|
906
|
+
const prototype = Object.getPrototypeOf(value);
|
|
907
|
+
return prototype === Object.prototype || prototype === null;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function getModelFields(target)
|
|
911
|
+
{
|
|
912
|
+
const schema = CjsSchema.getSchema(target.constructor);
|
|
913
|
+
return schema.fields.map(schemaFieldToModelField);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function initializeModelState(target)
|
|
917
|
+
{
|
|
918
|
+
// Models own their runtime-state shape: __state is a CjsModelState,
|
|
919
|
+
// created at construction before anything else (the event emitter's
|
|
920
|
+
// lazily-added `events` map lives on the same instance as an expando).
|
|
921
|
+
const existing = getRuntimeState(target);
|
|
922
|
+
if (existing instanceof CjsModelState) return existing;
|
|
923
|
+
if (existing)
|
|
924
|
+
{
|
|
925
|
+
throw new TypeError("CjsModel requires __state to be a CjsModelState.");
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const state = new CjsModelState();
|
|
929
|
+
Object.defineProperty(target, "__state", {
|
|
930
|
+
value: state,
|
|
931
|
+
enumerable: false,
|
|
932
|
+
configurable: false,
|
|
933
|
+
writable: false
|
|
934
|
+
});
|
|
935
|
+
return state;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function initializeOwnedGraph(root, options = {})
|
|
939
|
+
{
|
|
940
|
+
root.Traverse(value =>
|
|
941
|
+
{
|
|
942
|
+
value.__state.suppressEvents++;
|
|
943
|
+
|
|
944
|
+
try
|
|
945
|
+
{
|
|
946
|
+
if (value.__state instanceof CjsModelState)
|
|
947
|
+
{
|
|
948
|
+
// Construction: everything is new, so every declared consequence
|
|
949
|
+
// applies - all flag/rebuild tokens are added, and the object is
|
|
950
|
+
// marked for one settle.
|
|
951
|
+
addAllDeclaredTokens(value);
|
|
952
|
+
value.__state.dirty = true;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Initialize arguments belong to the class's Carbon/adapted contract.
|
|
956
|
+
// Owned-graph traversal is coordinated here and must not occupy arg 0.
|
|
957
|
+
// A conforming Initialize performs its own final
|
|
958
|
+
// UpdateValues({ skipEvents: true }), leaving nothing dirty.
|
|
959
|
+
if (typeof value.Initialize === "function")
|
|
960
|
+
{
|
|
961
|
+
if (value.Initialize() === false)
|
|
962
|
+
{
|
|
963
|
+
throw new Error(`${CjsSchema.getClassName(value.constructor)}.from initialization failed.`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Settle anything Initialize did not (including the no-Initialize
|
|
968
|
+
// case): the one construction settle, events suppressed.
|
|
969
|
+
if (value.__state instanceof CjsModelState && value.__state.dirty)
|
|
970
|
+
{
|
|
971
|
+
value.UpdateValues({
|
|
972
|
+
...options,
|
|
973
|
+
source: options.source ?? value,
|
|
974
|
+
skipEvents: true
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
finally
|
|
979
|
+
{
|
|
980
|
+
value.__state.suppressEvents--;
|
|
981
|
+
}
|
|
982
|
+
}, {
|
|
983
|
+
order: "post",
|
|
984
|
+
reverse: true,
|
|
985
|
+
ownedOnly: true,
|
|
986
|
+
visited: options.visited
|
|
987
|
+
});
|
|
988
|
+
return root;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function AddResource(target, value)
|
|
992
|
+
{
|
|
993
|
+
if (value?.isResource === true) target.add(value);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
function AddResources(target, values)
|
|
998
|
+
{
|
|
999
|
+
if (typeof values === "string" || typeof values?.[Symbol.iterator] !== "function")
|
|
1000
|
+
{
|
|
1001
|
+
throw new TypeError("CjsModel.OnGetResources must return an iterable of resources.");
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Empty slots are the model's own unset fields, not a contract violation.
|
|
1005
|
+
for (const value of values)
|
|
1006
|
+
{
|
|
1007
|
+
if (value !== null && value !== undefined) target.add(value);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function schemaFieldToModelField(field)
|
|
1012
|
+
{
|
|
1013
|
+
return {
|
|
1014
|
+
...field,
|
|
1015
|
+
jsType: field.type || field.jsType || null
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function isWritableModelField(field)
|
|
1020
|
+
{
|
|
1021
|
+
const io = field?.io;
|
|
1022
|
+
if (!io) return true;
|
|
1023
|
+
if (io.write || io.persist || io.persistOnly) return true;
|
|
1024
|
+
if (io.read && !io.write) return false;
|
|
1025
|
+
return true;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function findIncomingKey(values, field)
|
|
1029
|
+
{
|
|
1030
|
+
for (const key of incomingKeyCandidates(field))
|
|
1031
|
+
{
|
|
1032
|
+
if (Object.prototype.hasOwnProperty.call(values, key)) return key;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
865
1038
|
function incomingKeyCandidates(field)
|
|
866
1039
|
{
|
|
867
|
-
const aliases = field.aliases === undefined
|
|
868
|
-
? field.alias === undefined ? [] : [field.alias]
|
|
869
|
-
: Array.isArray(field.aliases) ? field.aliases : [field.aliases];
|
|
1040
|
+
const aliases = field.aliases === undefined
|
|
1041
|
+
? field.alias === undefined ? [] : [field.alias]
|
|
1042
|
+
: Array.isArray(field.aliases) ? field.aliases : [field.aliases];
|
|
870
1043
|
return [field.name, ...aliases].filter(value => typeof value === "string" && value.length);
|
|
871
1044
|
}
|
|
872
1045
|
|
|
873
|
-
|
|
874
|
-
// Duplicate adds are no-ops (Sets). Nothing in the model layer ever clears
|
|
875
|
-
// these stores - getters clear flags, work methods clear rebuild tokens.
|
|
876
|
-
function addDeclaredFieldTokens(target, field)
|
|
877
|
-
{
|
|
878
|
-
const io = field?.io;
|
|
879
|
-
if (!io) return;
|
|
880
|
-
if (io.flag) for (const token of io.flag) target.__state.flags.add(token);
|
|
881
|
-
if (io.rebuild) for (const token of io.rebuild) target.__state.rebuild.add(token);
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
// Construction / broad invalidation: every declared token applies.
|
|
885
|
-
function addAllDeclaredTokens(target)
|
|
1046
|
+
function getChildCollection(target, property)
|
|
886
1047
|
{
|
|
887
|
-
|
|
888
|
-
for (const field of fields) addDeclaredFieldTokens(target, field);
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
// Direct-mutation courtesy: a caller that knows which fields it touched
|
|
892
|
-
// (bindings) passes them so declared consequences stay precise.
|
|
893
|
-
function addExplicitUpdateProperties(target, properties)
|
|
894
|
-
{
|
|
895
|
-
if (properties === null || properties === undefined) return;
|
|
896
|
-
target.__state.dirty = true;
|
|
897
|
-
for (const property of typeof properties === "string" ? [properties] : properties)
|
|
898
|
-
{
|
|
899
|
-
const field = CjsSchema.getField(target.constructor, property);
|
|
900
|
-
if (field) addDeclaredFieldTokens(target, field);
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
function createModifiedPayload(properties, source)
|
|
905
|
-
{
|
|
906
|
-
return Object.freeze({
|
|
907
|
-
properties: new Set(properties),
|
|
908
|
-
source
|
|
909
|
-
});
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
function areEquivalentSourceValues(a, b)
|
|
913
|
-
{
|
|
914
|
-
if (Object.is(a, b)) return true;
|
|
915
|
-
|
|
916
|
-
if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b))
|
|
1048
|
+
if (!(target instanceof CjsModel))
|
|
917
1049
|
{
|
|
918
|
-
|
|
919
|
-
for (let i = 0; i < a.length; i++)
|
|
920
|
-
{
|
|
921
|
-
if (!Object.is(a[i], b[i])) return false;
|
|
922
|
-
}
|
|
923
|
-
return true;
|
|
1050
|
+
throw new TypeError("CjsModel child collection target must be a CjsModel instance.");
|
|
924
1051
|
}
|
|
925
1052
|
|
|
926
|
-
if (
|
|
927
|
-
{
|
|
928
|
-
if (a.length !== b.length) return false;
|
|
929
|
-
for (let i = 0; i < a.length; i++)
|
|
930
|
-
{
|
|
931
|
-
if (!areEquivalentSourceValues(a[i], b[i])) return false;
|
|
932
|
-
}
|
|
933
|
-
return true;
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
return false;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
export function exportSourceValue(value, options = {})
|
|
940
|
-
{
|
|
941
|
-
if (value instanceof CjsModel) return value.GetValues(options);
|
|
942
|
-
return exportCarbonValue(value);
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
function hasAdvancedExportOptions(options)
|
|
946
|
-
{
|
|
947
|
-
return !!(options && (options.persistOnly || options.typeTags || options.forceTypeTags
|
|
948
|
-
|| options.refs || options.forceIDs || options.keyedLists
|
|
949
|
-
|| (options.enumFormat && options.enumFormat !== "values")));
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
// Enum-aware value handling: @schema.enum("X") resolves through the owning
|
|
953
|
-
// class's PascalCase static `Constructor.X`, lazily and leaf-first.
|
|
954
|
-
const ENUM_REVERSE_CACHE = new WeakMap();
|
|
955
|
-
|
|
956
|
-
function resolveEnumStaticForField(Constructor, field)
|
|
957
|
-
{
|
|
958
|
-
const name = field?.enum?.enumType;
|
|
959
|
-
if (!name) return null;
|
|
960
|
-
const members = Constructor?.[name];
|
|
961
|
-
if (!members || typeof members !== "object") return null;
|
|
962
|
-
return { name, members };
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
function enumMemberName(members, value)
|
|
966
|
-
{
|
|
967
|
-
let reverse = ENUM_REVERSE_CACHE.get(members);
|
|
968
|
-
if (!reverse)
|
|
1053
|
+
if (typeof property !== "string" || !property)
|
|
969
1054
|
{
|
|
970
|
-
|
|
971
|
-
for (const key of Object.keys(members))
|
|
972
|
-
{
|
|
973
|
-
// Duplicate values: first-declared key wins.
|
|
974
|
-
if (!reverse.has(members[key])) reverse.set(members[key], key);
|
|
975
|
-
}
|
|
976
|
-
ENUM_REVERSE_CACHE.set(members, reverse);
|
|
1055
|
+
throw new TypeError("CjsModel child collection property must be a non-empty string.");
|
|
977
1056
|
}
|
|
978
|
-
return reverse.get(value);
|
|
979
|
-
}
|
|
980
1057
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
let current = Constructor;
|
|
984
|
-
while (typeof current === "function")
|
|
1058
|
+
const field = CjsSchema.getField(target.constructor, property);
|
|
1059
|
+
if (!field)
|
|
985
1060
|
{
|
|
986
|
-
|
|
987
|
-
{
|
|
988
|
-
return `${CjsSchema.getClassName(current) || current.name}.${name}`;
|
|
989
|
-
}
|
|
990
|
-
current = Object.getPrototypeOf(current);
|
|
1061
|
+
throw new TypeError(`${CjsSchema.getClassName(target.constructor)} has no schema field named ${JSON.stringify(property)}.`);
|
|
991
1062
|
}
|
|
992
|
-
return `${CjsSchema.getClassName(Constructor) || Constructor.name}.${name}`;
|
|
993
|
-
}
|
|
994
1063
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
// (element 0 only, so identity tuples round-trip).
|
|
998
|
-
function translateEnumInput(value, members)
|
|
999
|
-
{
|
|
1000
|
-
if (Array.isArray(value))
|
|
1001
|
-
{
|
|
1002
|
-
if (!value.length) return undefined;
|
|
1003
|
-
return translateEnumInput(value[0], members);
|
|
1004
|
-
}
|
|
1005
|
-
if (typeof value === "string")
|
|
1006
|
-
{
|
|
1007
|
-
return Object.hasOwn(members, value) ? members[value] : undefined;
|
|
1008
|
-
}
|
|
1009
|
-
if (typeof value === "number")
|
|
1064
|
+
const fieldType = field.type || field.jsType;
|
|
1065
|
+
if (!CHILD_COLLECTION_KINDS.has(fieldType?.kind))
|
|
1010
1066
|
{
|
|
1011
|
-
|
|
1067
|
+
throw new TypeError(`${field.name} must be a schema array or list child collection.`);
|
|
1012
1068
|
}
|
|
1013
|
-
return undefined;
|
|
1014
|
-
}
|
|
1015
1069
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
function validateEnumInputs(out, values)
|
|
1019
|
-
{
|
|
1020
|
-
const translations = new Map();
|
|
1021
|
-
let issues = null;
|
|
1022
|
-
for (const field of getModelFields(out))
|
|
1023
|
-
{
|
|
1024
|
-
if (!isWritableModelField(field)) continue;
|
|
1025
|
-
const key = findIncomingKey(values, field);
|
|
1026
|
-
if (key === null) continue;
|
|
1027
|
-
const spec = resolveEnumStaticForField(out.constructor, field);
|
|
1028
|
-
if (!spec) continue;
|
|
1029
|
-
const raw = values[key];
|
|
1030
|
-
if (raw === null || raw === undefined || raw instanceof CjsModel) continue;
|
|
1031
|
-
const translated = translateEnumInput(raw, spec.members);
|
|
1032
|
-
if (translated === undefined)
|
|
1033
|
-
{
|
|
1034
|
-
issues = issues || [];
|
|
1035
|
-
issues.push(`${field.name}: ${JSON.stringify(raw)} is not a member of ${enumIdentity(out.constructor, spec.name)}`);
|
|
1036
|
-
}
|
|
1037
|
-
else
|
|
1038
|
-
{
|
|
1039
|
-
translations.set(field.name, translated);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
if (issues)
|
|
1070
|
+
const collection = target[field.name];
|
|
1071
|
+
if (!Array.isArray(collection))
|
|
1043
1072
|
{
|
|
1044
|
-
throw new TypeError(
|
|
1073
|
+
throw new TypeError(`${field.name} must contain an ordinary JavaScript Array.`);
|
|
1045
1074
|
}
|
|
1046
|
-
return translations;
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
function exportEnumFieldValue(value, spec, Constructor, options)
|
|
1050
|
-
{
|
|
1051
|
-
if (typeof value !== "number") return value;
|
|
1052
|
-
const memberName = enumMemberName(spec.members, value);
|
|
1053
|
-
if (memberName === undefined) return value;
|
|
1054
|
-
if (options.enumFormat === "names") return memberName;
|
|
1055
|
-
return [memberName, enumIdentity(Constructor, spec.name)];
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
function isPersistedModelField(field)
|
|
1059
|
-
{
|
|
1060
|
-
const io = field?.io;
|
|
1061
|
-
return !!(io && (io.persist || io.persistOnly));
|
|
1062
|
-
}
|
|
1063
1075
|
|
|
1064
|
-
|
|
1065
|
-
{
|
|
1066
|
-
if (!fieldType) return null;
|
|
1067
|
-
if (typeof fieldType === "string") return fieldType;
|
|
1068
|
-
if (fieldType.kind === "array" || fieldType.kind === "list")
|
|
1069
|
-
{
|
|
1070
|
-
const item = fieldType.itemType ?? null;
|
|
1071
|
-
return typeof item === "string" ? item : item?.className ?? null;
|
|
1072
|
-
}
|
|
1073
|
-
return fieldType.className ?? null;
|
|
1076
|
+
return { field, collection };
|
|
1074
1077
|
}
|
|
1075
1078
|
|
|
1076
|
-
function
|
|
1079
|
+
function assertChildObject(child, property)
|
|
1077
1080
|
{
|
|
1078
|
-
if (!
|
|
1079
|
-
|
|
1080
|
-
let nextId = 1;
|
|
1081
|
-
const idByModel = new Map();
|
|
1082
|
-
const context = {
|
|
1083
|
-
emitted: new Set(),
|
|
1084
|
-
idByModel,
|
|
1085
|
-
getId(model)
|
|
1086
|
-
{
|
|
1087
|
-
let id = idByModel.get(model);
|
|
1088
|
-
if (id === undefined)
|
|
1089
|
-
{
|
|
1090
|
-
id = nextId++;
|
|
1091
|
-
idByModel.set(model, id);
|
|
1092
|
-
}
|
|
1093
|
-
return id;
|
|
1094
|
-
}
|
|
1095
|
-
};
|
|
1096
|
-
|
|
1097
|
-
if (options.refs)
|
|
1081
|
+
if (!child || typeof child !== "object" || Array.isArray(child) || ArrayBuffer.isView(child))
|
|
1098
1082
|
{
|
|
1099
|
-
|
|
1100
|
-
const counts = new Map();
|
|
1101
|
-
(function walk(value)
|
|
1102
|
-
{
|
|
1103
|
-
if (Array.isArray(value))
|
|
1104
|
-
{
|
|
1105
|
-
for (const item of value) walk(item);
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
if (!(value instanceof CjsModel)) return;
|
|
1109
|
-
const count = (counts.get(value) ?? 0) + 1;
|
|
1110
|
-
counts.set(value, count);
|
|
1111
|
-
if (count > 1) return;
|
|
1112
|
-
for (const field of getModelFields(value))
|
|
1113
|
-
{
|
|
1114
|
-
if (options.persistOnly && !isPersistedModelField(field)) continue;
|
|
1115
|
-
walk(value[field.name]);
|
|
1116
|
-
}
|
|
1117
|
-
})(root);
|
|
1118
|
-
for (const [model, count] of counts)
|
|
1119
|
-
{
|
|
1120
|
-
if (count > 1) idByModel.set(model, nextId++);
|
|
1121
|
-
}
|
|
1083
|
+
throw new TypeError(`${property} requires a non-null child object.`);
|
|
1122
1084
|
}
|
|
1123
|
-
|
|
1124
|
-
return context;
|
|
1125
1085
|
}
|
|
1126
1086
|
|
|
1127
|
-
function
|
|
1087
|
+
function assertChildCallback(callback, optionName)
|
|
1128
1088
|
{
|
|
1129
|
-
if (
|
|
1089
|
+
if (callback !== undefined && callback !== null && typeof callback !== "function")
|
|
1130
1090
|
{
|
|
1131
|
-
|
|
1091
|
+
throw new TypeError(`CjsModel child ${optionName} option must be a function.`);
|
|
1132
1092
|
}
|
|
1133
|
-
|
|
1134
|
-
const className = CjsSchema.getClassName(model.constructor);
|
|
1135
|
-
if (options.forceTypeTags || (options.typeTags && className && className !== declaredClassName))
|
|
1136
|
-
{
|
|
1137
|
-
out._type = className;
|
|
1138
|
-
}
|
|
1139
|
-
if (context && (options.forceIDs || context.idByModel.has(model)))
|
|
1140
|
-
{
|
|
1141
|
-
out._id = context.getId(model);
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
const enumMode = options.enumFormat && options.enumFormat !== "values";
|
|
1145
|
-
for (const field of getModelFields(model))
|
|
1146
|
-
{
|
|
1147
|
-
if (options.persistOnly && !isPersistedModelField(field)) continue;
|
|
1148
|
-
if (enumMode)
|
|
1149
|
-
{
|
|
1150
|
-
const spec = resolveEnumStaticForField(model.constructor, field);
|
|
1151
|
-
if (spec)
|
|
1152
|
-
{
|
|
1153
|
-
out[field.name] = exportEnumFieldValue(model[field.name], spec, model.constructor, options);
|
|
1154
|
-
continue;
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
out[field.name] = exportAdvancedValue(
|
|
1158
|
-
model[field.name],
|
|
1159
|
-
declaredExportClassName(field.jsType || field.type || null),
|
|
1160
|
-
options,
|
|
1161
|
-
context
|
|
1162
|
-
);
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
return out;
|
|
1166
1093
|
}
|
|
1167
1094
|
|
|
1168
|
-
function
|
|
1095
|
+
function recordChildMutation(target, field, options)
|
|
1169
1096
|
{
|
|
1170
|
-
if (
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
{
|
|
1174
|
-
return { _ref: context.getId(value) };
|
|
1175
|
-
}
|
|
1176
|
-
return exportModelInto(value, {}, declaredClassName, options, context);
|
|
1177
|
-
}
|
|
1178
|
-
if (Array.isArray(value))
|
|
1179
|
-
{
|
|
1180
|
-
if (options.keyedLists)
|
|
1181
|
-
{
|
|
1182
|
-
const keyed = exportKeyedList(value, declaredClassName, options, context);
|
|
1183
|
-
if (keyed) return keyed;
|
|
1184
|
-
}
|
|
1185
|
-
return value.map(item => exportAdvancedValue(item, declaredClassName, options, context));
|
|
1186
|
-
}
|
|
1187
|
-
return exportCarbonValue(value);
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
function exportKeyedList(list, declaredClassName, options, context)
|
|
1191
|
-
{
|
|
1192
|
-
if (!list.length) return null;
|
|
1193
|
-
|
|
1194
|
-
const seen = new Set();
|
|
1195
|
-
for (const item of list)
|
|
1196
|
-
{
|
|
1197
|
-
if (!(item instanceof CjsModel)) return null;
|
|
1198
|
-
const name = item.name;
|
|
1199
|
-
if (typeof name !== "string" || name === "" || seen.has(name)) return null;
|
|
1200
|
-
seen.add(name);
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
const out = {};
|
|
1204
|
-
for (const item of list)
|
|
1205
|
-
{
|
|
1206
|
-
const exported = exportAdvancedValue(item, declaredClassName, options, context);
|
|
1207
|
-
if (exported && typeof exported === "object" && exported._ref === undefined)
|
|
1208
|
-
{
|
|
1209
|
-
delete exported.name;
|
|
1210
|
-
}
|
|
1211
|
-
out[item.name] = exported;
|
|
1212
|
-
}
|
|
1213
|
-
return out;
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
export function importSourceValue(value, field = null, options = {})
|
|
1217
|
-
{
|
|
1218
|
-
if (value instanceof CjsModel) return value;
|
|
1219
|
-
|
|
1220
|
-
if (isReferenceValue(value))
|
|
1221
|
-
{
|
|
1222
|
-
const resolved = resolveIncomingReference(value, options);
|
|
1223
|
-
if (resolved instanceof CjsPendingReference)
|
|
1224
|
-
{
|
|
1225
|
-
throw new TypeError(`Forward { _ref: ${JSON.stringify(value._ref)} } cannot be deferred in this position.`);
|
|
1226
|
-
}
|
|
1227
|
-
return resolved;
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
const schemaType = getSchemaType(options.ownerConstructor, field?.name);
|
|
1231
|
-
const declaredClassName = getSchemaClassName(schemaType, options);
|
|
1232
|
-
if (value && typeof value === "object" && !(value instanceof CjsModel) && !Array.isArray(value) && !ArrayBuffer.isView(value))
|
|
1233
|
-
{
|
|
1234
|
-
// A registered `_type` selects the concrete class in singular
|
|
1235
|
-
// schema-typed positions. Carbon contracts may be declared through
|
|
1236
|
-
// interface names with no runtime inheritance, so the declared name
|
|
1237
|
-
// is a fallback, not a constraint the concrete class must extend.
|
|
1238
|
-
const explicitClassName = isSingularSchemaKind(schemaType) && typeof value._type === "string"
|
|
1239
|
-
? getSchemaClassName(value._type, options)
|
|
1240
|
-
: null;
|
|
1241
|
-
const className = explicitClassName || declaredClassName;
|
|
1242
|
-
if (className)
|
|
1243
|
-
{
|
|
1244
|
-
return createModelValue(className, value, options);
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
if ((schemaType?.kind === "array" || schemaType?.kind === "list") && schemaType.itemType && Array.isArray(value))
|
|
1249
|
-
{
|
|
1250
|
-
const itemClassName = getSchemaClassName(schemaType.itemType, options);
|
|
1251
|
-
const result = [];
|
|
1252
|
-
for (let i = 0; i < value.length; i++)
|
|
1253
|
-
{
|
|
1254
|
-
const item = value[i];
|
|
1255
|
-
if (isReferenceValue(item))
|
|
1256
|
-
{
|
|
1257
|
-
result.push(importReferenceInto(item, options, result, i));
|
|
1258
|
-
continue;
|
|
1259
|
-
}
|
|
1260
|
-
if (!item || typeof item !== "object" || item instanceof CjsModel || ArrayBuffer.isView(item))
|
|
1261
|
-
{
|
|
1262
|
-
result.push(importSourceValue(item, null, options));
|
|
1263
|
-
continue;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
const explicitClassName = typeof item._type === "string"
|
|
1267
|
-
? getSchemaClassName(item._type, options)
|
|
1268
|
-
: null;
|
|
1269
|
-
if (explicitClassName)
|
|
1270
|
-
{
|
|
1271
|
-
result.push(createModelValue(explicitClassName, item, options));
|
|
1272
|
-
continue;
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
result.push(itemClassName
|
|
1276
|
-
? createModelValue(itemClassName, item, options)
|
|
1277
|
-
: importSourceValue(item, null, options));
|
|
1278
|
-
}
|
|
1279
|
-
return result;
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
// List fields also accept name-keyed object maps for unique-named items;
|
|
1283
|
-
// the map is a wholesale list replacement, mirroring array semantics.
|
|
1284
|
-
const effectiveType = field?.jsType || field?.type || schemaType;
|
|
1285
|
-
if ((effectiveType?.kind === "array" || effectiveType?.kind === "list")
|
|
1286
|
-
&& value && typeof value === "object" && !Array.isArray(value) && !ArrayBuffer.isView(value))
|
|
1287
|
-
{
|
|
1288
|
-
return importListMapValue(value, effectiveType, options);
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
if (field) return normalizeCarbonValue(value, field);
|
|
1292
|
-
if (ArrayBuffer.isView(value)) return normalizeCarbonValue(value, { jsType: { kind: "typedArray", js: value.constructor.name } });
|
|
1293
|
-
if (typeof value === "bigint") return value;
|
|
1294
|
-
if (Array.isArray(value))
|
|
1295
|
-
{
|
|
1296
|
-
const result = [];
|
|
1297
|
-
for (let i = 0; i < value.length; i++)
|
|
1298
|
-
{
|
|
1299
|
-
const item = value[i];
|
|
1300
|
-
result.push(isReferenceValue(item)
|
|
1301
|
-
? importReferenceInto(item, options, result, i)
|
|
1302
|
-
: importSourceValue(item, null, options));
|
|
1303
|
-
}
|
|
1304
|
-
return result;
|
|
1305
|
-
}
|
|
1306
|
-
if (value && typeof value === "object" && !(value instanceof CjsModel))
|
|
1307
|
-
{
|
|
1308
|
-
const result = {};
|
|
1309
|
-
for (const [key, item] of Object.entries(value))
|
|
1310
|
-
{
|
|
1311
|
-
result[key] = isReferenceValue(item)
|
|
1312
|
-
? importReferenceInto(item, options, result, key)
|
|
1313
|
-
: importSourceValue(item, null, options);
|
|
1314
|
-
}
|
|
1315
|
-
return result;
|
|
1316
|
-
}
|
|
1317
|
-
return value;
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
function applyIncomingStructInPlace(current, incoming, field, options)
|
|
1321
|
-
{
|
|
1322
|
-
const schemaType = getSchemaType(options.ownerConstructor, field?.name) || field?.type || field?.jsType;
|
|
1323
|
-
if (schemaType?.kind !== "struct" || !(current instanceof CjsModel)) return null;
|
|
1324
|
-
if (incoming === null || incoming === undefined) return false;
|
|
1325
|
-
if (typeof incoming !== "object" || Array.isArray(incoming) || ArrayBuffer.isView(incoming))
|
|
1326
|
-
{
|
|
1327
|
-
throw new TypeError(`${field.name} requires an object value for registered struct ${schemaType.className || "unknown"}.`);
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
const values = incoming instanceof CjsModel ? incoming.GetValues() : incoming;
|
|
1331
|
-
const changed = current.SetValues(values, options);
|
|
1332
|
-
return changed instanceof Set ? changed.size > 0 : changed === true;
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
function importListMapValue(value, schemaType, options)
|
|
1336
|
-
{
|
|
1337
|
-
const itemClassName = schemaType.itemType ? getSchemaClassName(schemaType.itemType, options) : null;
|
|
1338
|
-
const result = [];
|
|
1339
|
-
for (const key of Object.keys(value))
|
|
1340
|
-
{
|
|
1341
|
-
const item = value[key];
|
|
1342
|
-
if (item === undefined || item === null) continue;
|
|
1343
|
-
|
|
1344
|
-
if (item instanceof CjsModel)
|
|
1345
|
-
{
|
|
1346
|
-
if (typeof item.name === "string" && item.name === "")
|
|
1347
|
-
{
|
|
1348
|
-
item.SetValues({ name: key });
|
|
1349
|
-
}
|
|
1350
|
-
result.push(item);
|
|
1351
|
-
continue;
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
if (isReferenceValue(item))
|
|
1355
|
-
{
|
|
1356
|
-
// Shared items keep their own name; the map key is not restamped
|
|
1357
|
-
// onto an instance owned by another position in the graph.
|
|
1358
|
-
result.push(importReferenceInto(item, options, result, result.length));
|
|
1359
|
-
continue;
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
if (typeof item !== "object" || Array.isArray(item) || ArrayBuffer.isView(item))
|
|
1363
|
-
{
|
|
1364
|
-
throw new TypeError(`List field maps require object or model values; "${key}" cannot become a list item.`);
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
const explicitClassName = typeof item._type === "string"
|
|
1368
|
-
? getSchemaClassName(item._type, options)
|
|
1369
|
-
: null;
|
|
1370
|
-
const className = explicitClassName || itemClassName;
|
|
1371
|
-
if (!className)
|
|
1372
|
-
{
|
|
1373
|
-
throw new TypeError(`List field maps cannot resolve a model class for "${key}".`);
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
const values = item.name === undefined ? { ...item, name: key } : item;
|
|
1377
|
-
result.push(createModelValue(className, values, options));
|
|
1378
|
-
}
|
|
1379
|
-
return result;
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
function getSchemaType(Constructor, fieldName)
|
|
1383
|
-
{
|
|
1384
|
-
if (!Constructor || !fieldName) return null;
|
|
1385
|
-
return CjsSchema.getField(Constructor, fieldName)?.type || null;
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
function getSchemaClassName(schemaType, options = {})
|
|
1389
|
-
{
|
|
1390
|
-
if (!schemaType) return null;
|
|
1391
|
-
if (typeof schemaType === "string")
|
|
1392
|
-
{
|
|
1393
|
-
const Schema = options.registry || CjsModel.schema;
|
|
1394
|
-
return Schema.GetConstructor(schemaType) ? schemaType : null;
|
|
1395
|
-
}
|
|
1396
|
-
if (schemaType.kind === "model")
|
|
1397
|
-
{
|
|
1398
|
-
return schemaType.className || null;
|
|
1399
|
-
}
|
|
1400
|
-
if (schemaType.kind === "objectRef" || schemaType.kind === "struct")
|
|
1401
|
-
{
|
|
1402
|
-
const Schema = options.registry || CjsModel.schema;
|
|
1403
|
-
return schemaType.className && Schema.GetConstructor(schemaType.className)
|
|
1404
|
-
? schemaType.className
|
|
1405
|
-
: null;
|
|
1406
|
-
}
|
|
1407
|
-
return null;
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
function createModelValue(className, values, options)
|
|
1411
|
-
{
|
|
1412
|
-
const Schema = options.registry || CjsModel.schema;
|
|
1413
|
-
const Constructor = Schema.GetConstructor(className);
|
|
1414
|
-
if (!Constructor)
|
|
1415
|
-
{
|
|
1416
|
-
throw new TypeError(`No CjsModel class is registered for schema type ${className}.`);
|
|
1417
|
-
}
|
|
1418
|
-
if (Constructor !== CjsModel && !(Constructor.prototype instanceof CjsModel))
|
|
1419
|
-
{
|
|
1420
|
-
throw new TypeError(`Registered schema type ${className} is not a CjsModel.`);
|
|
1421
|
-
}
|
|
1422
|
-
if (typeof Constructor.from !== "function")
|
|
1423
|
-
{
|
|
1424
|
-
throw new TypeError(`Registered CjsModel ${className} does not provide from().`);
|
|
1425
|
-
}
|
|
1426
|
-
return Constructor.from(values, options);
|
|
1097
|
+
if (options.markDirty === false) return;
|
|
1098
|
+
target.__state.dirty = true;
|
|
1099
|
+
if (options.notify !== false) addDeclaredFieldTokens(target, field);
|
|
1427
1100
|
}
|
|
1428
1101
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
/** Represents one unresolved model reference during a single import operation. */
|
|
1432
|
-
class CjsPendingReference
|
|
1102
|
+
function notifyListModified(target, event, index, secondIndex, child, collection)
|
|
1433
1103
|
{
|
|
1434
|
-
|
|
1435
|
-
constructor(id, expectedClassName = null)
|
|
1104
|
+
if (typeof target.OnListModified === "function")
|
|
1436
1105
|
{
|
|
1437
|
-
|
|
1438
|
-
this.expectedClassName = expectedClassName;
|
|
1106
|
+
target.OnListModified(event, index, secondIndex, child, collection);
|
|
1439
1107
|
}
|
|
1440
|
-
|
|
1441
1108
|
}
|
|
1442
1109
|
|
|
1443
|
-
function
|
|
1110
|
+
function createChildEventPayload(target, property, child, index, options)
|
|
1444
1111
|
{
|
|
1445
|
-
const byId = new Map();
|
|
1446
|
-
const created = [];
|
|
1447
|
-
const pending = [];
|
|
1448
1112
|
return {
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
},
|
|
1454
|
-
register(id, instance)
|
|
1455
|
-
{
|
|
1456
|
-
const existing = byId.get(id);
|
|
1457
|
-
if (existing === instance) return;
|
|
1458
|
-
if (existing !== undefined)
|
|
1459
|
-
{
|
|
1460
|
-
throw new TypeError(`Duplicate _id ${JSON.stringify(id)} in imported values.`);
|
|
1461
|
-
}
|
|
1462
|
-
byId.set(id, instance);
|
|
1463
|
-
},
|
|
1464
|
-
defer(id, assign)
|
|
1465
|
-
{
|
|
1466
|
-
pending.push({ id, assign });
|
|
1467
|
-
},
|
|
1468
|
-
finalize()
|
|
1469
|
-
{
|
|
1470
|
-
const unresolved = new Set();
|
|
1471
|
-
for (const entry of pending)
|
|
1472
|
-
{
|
|
1473
|
-
const instance = byId.get(entry.id);
|
|
1474
|
-
if (instance === undefined)
|
|
1475
|
-
{
|
|
1476
|
-
unresolved.add(entry.id);
|
|
1477
|
-
continue;
|
|
1478
|
-
}
|
|
1479
|
-
entry.assign(instance);
|
|
1480
|
-
}
|
|
1481
|
-
pending.length = 0;
|
|
1482
|
-
if (unresolved.size)
|
|
1483
|
-
{
|
|
1484
|
-
throw new TypeError(`Unresolved _ref ids: ${Array.from(unresolved, id => JSON.stringify(id)).join(", ")}. Every { _ref } must match a { _id } in the same import operation.`);
|
|
1485
|
-
}
|
|
1486
|
-
},
|
|
1487
|
-
initializeCreated(options)
|
|
1488
|
-
{
|
|
1489
|
-
const visited = new Set();
|
|
1490
|
-
|
|
1491
|
-
for (let index = created.length - 1; index >= 0; index--)
|
|
1492
|
-
{
|
|
1493
|
-
initializeOwnedGraph(created[index], { ...options, visited });
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
created.length = 0;
|
|
1497
|
-
}
|
|
1113
|
+
property,
|
|
1114
|
+
child,
|
|
1115
|
+
index,
|
|
1116
|
+
source: options.source ?? target
|
|
1498
1117
|
};
|
|
1499
1118
|
}
|
|
1500
1119
|
|
|
1501
|
-
function
|
|
1120
|
+
function invokeChildCallback(callback, target, payload, optionName)
|
|
1502
1121
|
{
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1122
|
+
if (callback === undefined || callback === null) return;
|
|
1123
|
+
assertChildCallback(callback, optionName);
|
|
1124
|
+
callback.call(target, payload);
|
|
1506
1125
|
}
|
|
1507
1126
|
|
|
1508
|
-
|
|
1509
|
-
// a CjsPendingReference for the finalize pass (forward references). Resolved
|
|
1510
|
-
// references assign like direct instances: no declared-type constraint, since
|
|
1511
|
-
// Carbon contracts may be declared through interface names that have no
|
|
1512
|
-
// runtime inheritance relationship with the concrete class.
|
|
1513
|
-
function resolveIncomingReference(value, options)
|
|
1127
|
+
function emitChildEvent(target, eventName, payload, options)
|
|
1514
1128
|
{
|
|
1515
|
-
|
|
1516
|
-
const context = options.importContext;
|
|
1517
|
-
if (!context)
|
|
1129
|
+
if (options.skipEvents !== true && target.__state.suppressEvents === 0)
|
|
1518
1130
|
{
|
|
1519
|
-
|
|
1131
|
+
target.EmitEvent(eventName, target, payload);
|
|
1520
1132
|
}
|
|
1521
|
-
const resolved = context.byId.get(id);
|
|
1522
|
-
if (resolved === undefined) return new CjsPendingReference(id);
|
|
1523
|
-
return resolved;
|
|
1524
1133
|
}
|
|
1525
1134
|
|
|
1526
|
-
|
|
1527
|
-
// the owning operation's finalize pass. Deferred slots hold null until then.
|
|
1528
|
-
function importReferenceInto(value, options, target, key)
|
|
1135
|
+
function settleChildMutation(target, field, options)
|
|
1529
1136
|
{
|
|
1530
|
-
|
|
1531
|
-
if (resolved instanceof CjsPendingReference)
|
|
1532
|
-
{
|
|
1533
|
-
options.importContext.defer(resolved.id, instance =>
|
|
1534
|
-
{
|
|
1535
|
-
target[key] = instance;
|
|
1536
|
-
});
|
|
1537
|
-
return null;
|
|
1538
|
-
}
|
|
1539
|
-
return resolved;
|
|
1540
|
-
}
|
|
1137
|
+
if (options.skipUpdate === true) return;
|
|
1541
1138
|
|
|
1542
|
-
|
|
1543
|
-
// field changed. Forward references keep the current value until finalize.
|
|
1544
|
-
function applyIncomingReference(out, field, incoming, options)
|
|
1545
|
-
{
|
|
1546
|
-
const fieldName = field.name;
|
|
1547
|
-
const resolved = resolveIncomingReference(incoming, options);
|
|
1548
|
-
|
|
1549
|
-
if (resolved instanceof CjsPendingReference)
|
|
1139
|
+
if (options.markDirty === false)
|
|
1550
1140
|
{
|
|
1551
|
-
options.
|
|
1141
|
+
if (options.skipEvents !== true && target.__state.suppressEvents === 0)
|
|
1552
1142
|
{
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1143
|
+
target.EmitEvent("modified", target, createModifiedPayload(
|
|
1144
|
+
new Set([ field.name ]),
|
|
1145
|
+
options.source ?? target
|
|
1146
|
+
));
|
|
1147
|
+
}
|
|
1148
|
+
return;
|
|
1556
1149
|
}
|
|
1557
1150
|
|
|
1558
|
-
if (
|
|
1559
|
-
out[fieldName] = resolved;
|
|
1560
|
-
return true;
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
function resolveRegisteredModelClass(typeName, options = {})
|
|
1564
|
-
{
|
|
1565
|
-
const Schema = options.registry || CjsModel.schema;
|
|
1566
|
-
const Constructor = Schema.GetConstructor(typeName);
|
|
1567
|
-
if (!Constructor)
|
|
1568
|
-
{
|
|
1569
|
-
throw new TypeError(`No CjsModel class is registered for _type "${typeName}".`);
|
|
1570
|
-
}
|
|
1571
|
-
return Constructor;
|
|
1151
|
+
if (!target.__state.updating) target.UpdateValues(options);
|
|
1572
1152
|
}
|
|
1573
1153
|
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1154
|
+
// Adds one field's declared @io.flag / @io.rebuild tokens to their stores.
|
|
1155
|
+
// Duplicate adds are no-ops (Sets). Nothing in the model layer ever clears
|
|
1156
|
+
// these stores - getters clear flags, work methods clear rebuild tokens.
|
|
1157
|
+
function addDeclaredFieldTokens(target, field)
|
|
1158
|
+
{
|
|
1159
|
+
const io = field?.io;
|
|
1160
|
+
if (!io) return;
|
|
1161
|
+
if (io.flag) for (const token of io.flag) target.__state.flags.add(token);
|
|
1162
|
+
if (io.rebuild) for (const token of io.rebuild) target.__state.rebuild.add(token);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Construction / broad invalidation: every declared token applies.
|
|
1166
|
+
function addAllDeclaredTokens(target)
|
|
1167
|
+
{
|
|
1168
|
+
const fields = CjsSchema.getSchema(target.constructor)?.fields || [];
|
|
1169
|
+
for (const field of fields) addDeclaredFieldTokens(target, field);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// Direct-mutation courtesy: a caller that knows which fields it touched
|
|
1173
|
+
// (bindings) passes them so declared consequences stay precise.
|
|
1174
|
+
function addExplicitUpdateProperties(target, properties)
|
|
1175
|
+
{
|
|
1176
|
+
if (properties === null || properties === undefined) return;
|
|
1177
|
+
target.__state.dirty = true;
|
|
1178
|
+
for (const property of typeof properties === "string" ? [properties] : properties)
|
|
1179
|
+
{
|
|
1180
|
+
const field = CjsSchema.getField(target.constructor, property);
|
|
1181
|
+
if (field) addDeclaredFieldTokens(target, field);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function createModifiedPayload(properties, source)
|
|
1186
|
+
{
|
|
1187
|
+
return Object.freeze({
|
|
1188
|
+
properties: new Set(properties),
|
|
1189
|
+
source
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function areEquivalentSourceValues(a, b)
|
|
1194
|
+
{
|
|
1195
|
+
if (Object.is(a, b)) return true;
|
|
1196
|
+
|
|
1197
|
+
if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b))
|
|
1198
|
+
{
|
|
1199
|
+
if (a.constructor !== b.constructor || a.length !== b.length) return false;
|
|
1200
|
+
for (let i = 0; i < a.length; i++)
|
|
1201
|
+
{
|
|
1202
|
+
if (!Object.is(a[i], b[i])) return false;
|
|
1203
|
+
}
|
|
1204
|
+
return true;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
if (Array.isArray(a) && Array.isArray(b))
|
|
1208
|
+
{
|
|
1209
|
+
if (a.length !== b.length) return false;
|
|
1210
|
+
for (let i = 0; i < a.length; i++)
|
|
1211
|
+
{
|
|
1212
|
+
if (!areEquivalentSourceValues(a[i], b[i])) return false;
|
|
1213
|
+
}
|
|
1214
|
+
return true;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
export function exportSourceValue(value, options = {})
|
|
1221
|
+
{
|
|
1222
|
+
if (value instanceof CjsModel) return value.GetValues(options);
|
|
1223
|
+
return exportCarbonValue(value);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function hasAdvancedExportOptions(options)
|
|
1227
|
+
{
|
|
1228
|
+
return !!(options && (options.persistOnly || options.typeTags || options.forceTypeTags
|
|
1229
|
+
|| options.refs || options.forceIDs || options.keyedLists
|
|
1230
|
+
|| (options.enumFormat && options.enumFormat !== "values")));
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Enum-aware value handling: @schema.enum("X") resolves through the owning
|
|
1234
|
+
// class's PascalCase static `Constructor.X`, lazily and leaf-first.
|
|
1235
|
+
const ENUM_REVERSE_CACHE = new WeakMap();
|
|
1236
|
+
|
|
1237
|
+
function resolveEnumStaticForField(Constructor, field)
|
|
1238
|
+
{
|
|
1239
|
+
const name = field?.enum?.enumType;
|
|
1240
|
+
if (!name) return null;
|
|
1241
|
+
const members = Constructor?.[name];
|
|
1242
|
+
if (!members || typeof members !== "object") return null;
|
|
1243
|
+
return { name, members };
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function enumMemberName(members, value)
|
|
1247
|
+
{
|
|
1248
|
+
let reverse = ENUM_REVERSE_CACHE.get(members);
|
|
1249
|
+
if (!reverse)
|
|
1250
|
+
{
|
|
1251
|
+
reverse = new Map();
|
|
1252
|
+
for (const key of Object.keys(members))
|
|
1253
|
+
{
|
|
1254
|
+
// Duplicate values: first-declared key wins.
|
|
1255
|
+
if (!reverse.has(members[key])) reverse.set(members[key], key);
|
|
1256
|
+
}
|
|
1257
|
+
ENUM_REVERSE_CACHE.set(members, reverse);
|
|
1258
|
+
}
|
|
1259
|
+
return reverse.get(value);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function enumIdentity(Constructor, name)
|
|
1263
|
+
{
|
|
1264
|
+
let current = Constructor;
|
|
1265
|
+
while (typeof current === "function")
|
|
1266
|
+
{
|
|
1267
|
+
if (Object.hasOwn(current, name))
|
|
1268
|
+
{
|
|
1269
|
+
return `${CjsSchema.getClassName(current) || current.name}.${name}`;
|
|
1270
|
+
}
|
|
1271
|
+
current = Object.getPrototypeOf(current);
|
|
1272
|
+
}
|
|
1273
|
+
return `${CjsSchema.getClassName(Constructor) || Constructor.name}.${name}`;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// Returns the validated numeric member value, or undefined when the input is
|
|
1277
|
+
// not a member. Accepts numeric values, exact member-name strings, and arrays
|
|
1278
|
+
// (element 0 only, so identity tuples round-trip).
|
|
1279
|
+
function translateEnumInput(value, members)
|
|
1280
|
+
{
|
|
1281
|
+
if (Array.isArray(value))
|
|
1282
|
+
{
|
|
1283
|
+
if (!value.length) return undefined;
|
|
1284
|
+
return translateEnumInput(value[0], members);
|
|
1285
|
+
}
|
|
1286
|
+
if (typeof value === "string")
|
|
1287
|
+
{
|
|
1288
|
+
return Object.hasOwn(members, value) ? members[value] : undefined;
|
|
1289
|
+
}
|
|
1290
|
+
if (typeof value === "number")
|
|
1291
|
+
{
|
|
1292
|
+
return enumMemberName(members, value) === undefined ? undefined : value;
|
|
1293
|
+
}
|
|
1294
|
+
return undefined;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Atomic pre-validation: every enum-backed incoming value is checked before
|
|
1298
|
+
// any mutation; one TypeError reports every invalid property.
|
|
1299
|
+
function validateEnumInputs(out, values)
|
|
1300
|
+
{
|
|
1301
|
+
const translations = new Map();
|
|
1302
|
+
let issues = null;
|
|
1303
|
+
for (const field of getModelFields(out))
|
|
1304
|
+
{
|
|
1305
|
+
if (!isWritableModelField(field)) continue;
|
|
1306
|
+
const key = findIncomingKey(values, field);
|
|
1307
|
+
if (key === null) continue;
|
|
1308
|
+
const spec = resolveEnumStaticForField(out.constructor, field);
|
|
1309
|
+
if (!spec) continue;
|
|
1310
|
+
const raw = values[key];
|
|
1311
|
+
if (raw === null || raw === undefined || raw instanceof CjsModel) continue;
|
|
1312
|
+
const translated = translateEnumInput(raw, spec.members);
|
|
1313
|
+
if (translated === undefined)
|
|
1314
|
+
{
|
|
1315
|
+
issues = issues || [];
|
|
1316
|
+
issues.push(`${field.name}: ${JSON.stringify(raw)} is not a member of ${enumIdentity(out.constructor, spec.name)}`);
|
|
1317
|
+
}
|
|
1318
|
+
else
|
|
1319
|
+
{
|
|
1320
|
+
translations.set(field.name, translated);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
if (issues)
|
|
1324
|
+
{
|
|
1325
|
+
throw new TypeError(`Invalid enum values for ${CjsSchema.getClassName(out.constructor) || "model"} - ${issues.join("; ")}`);
|
|
1326
|
+
}
|
|
1327
|
+
return translations;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function exportEnumFieldValue(value, spec, Constructor, options)
|
|
1331
|
+
{
|
|
1332
|
+
if (typeof value !== "number") return value;
|
|
1333
|
+
const memberName = enumMemberName(spec.members, value);
|
|
1334
|
+
if (memberName === undefined) return value;
|
|
1335
|
+
if (options.enumFormat === "names") return memberName;
|
|
1336
|
+
return [memberName, enumIdentity(Constructor, spec.name)];
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function isPersistedModelField(field)
|
|
1340
|
+
{
|
|
1341
|
+
const io = field?.io;
|
|
1342
|
+
return !!(io && (io.persist || io.persistOnly));
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function declaredExportClassName(fieldType)
|
|
1346
|
+
{
|
|
1347
|
+
if (!fieldType) return null;
|
|
1348
|
+
if (typeof fieldType === "string") return fieldType;
|
|
1349
|
+
if (fieldType.kind === "array" || fieldType.kind === "list")
|
|
1350
|
+
{
|
|
1351
|
+
const item = fieldType.itemType ?? null;
|
|
1352
|
+
return typeof item === "string" ? item : item?.className ?? null;
|
|
1353
|
+
}
|
|
1354
|
+
return fieldType.className ?? null;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function createExportContext(root, options)
|
|
1358
|
+
{
|
|
1359
|
+
if (!options.refs && !options.forceIDs) return null;
|
|
1360
|
+
|
|
1361
|
+
let nextId = 1;
|
|
1362
|
+
const idByModel = new Map();
|
|
1363
|
+
const context = {
|
|
1364
|
+
emitted: new Set(),
|
|
1365
|
+
idByModel,
|
|
1366
|
+
getId(model)
|
|
1367
|
+
{
|
|
1368
|
+
let id = idByModel.get(model);
|
|
1369
|
+
if (id === undefined)
|
|
1370
|
+
{
|
|
1371
|
+
id = nextId++;
|
|
1372
|
+
idByModel.set(model, id);
|
|
1373
|
+
}
|
|
1374
|
+
return id;
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
|
|
1378
|
+
if (options.refs)
|
|
1379
|
+
{
|
|
1380
|
+
// Pre-count occurrences so only genuinely shared models receive ids.
|
|
1381
|
+
const counts = new Map();
|
|
1382
|
+
(function walk(value)
|
|
1383
|
+
{
|
|
1384
|
+
if (Array.isArray(value))
|
|
1385
|
+
{
|
|
1386
|
+
for (const item of value) walk(item);
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
if (!(value instanceof CjsModel)) return;
|
|
1390
|
+
const count = (counts.get(value) ?? 0) + 1;
|
|
1391
|
+
counts.set(value, count);
|
|
1392
|
+
if (count > 1) return;
|
|
1393
|
+
for (const field of getModelFields(value))
|
|
1394
|
+
{
|
|
1395
|
+
if (options.persistOnly && !isPersistedModelField(field)) continue;
|
|
1396
|
+
walk(value[field.name]);
|
|
1397
|
+
}
|
|
1398
|
+
})(root);
|
|
1399
|
+
for (const [model, count] of counts)
|
|
1400
|
+
{
|
|
1401
|
+
if (count > 1) idByModel.set(model, nextId++);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
return context;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function exportModelInto(model, out, declaredClassName, options, context)
|
|
1409
|
+
{
|
|
1410
|
+
if (context)
|
|
1411
|
+
{
|
|
1412
|
+
context.emitted.add(model);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
const className = CjsSchema.getClassName(model.constructor);
|
|
1416
|
+
if (options.forceTypeTags || (options.typeTags && className && className !== declaredClassName))
|
|
1417
|
+
{
|
|
1418
|
+
out._type = className;
|
|
1419
|
+
}
|
|
1420
|
+
if (context && (options.forceIDs || context.idByModel.has(model)))
|
|
1421
|
+
{
|
|
1422
|
+
out._id = context.getId(model);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const enumMode = options.enumFormat && options.enumFormat !== "values";
|
|
1426
|
+
for (const field of getModelFields(model))
|
|
1427
|
+
{
|
|
1428
|
+
if (options.persistOnly && !isPersistedModelField(field)) continue;
|
|
1429
|
+
if (enumMode)
|
|
1430
|
+
{
|
|
1431
|
+
const spec = resolveEnumStaticForField(model.constructor, field);
|
|
1432
|
+
if (spec)
|
|
1433
|
+
{
|
|
1434
|
+
out[field.name] = exportEnumFieldValue(model[field.name], spec, model.constructor, options);
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
out[field.name] = exportAdvancedValue(
|
|
1439
|
+
model[field.name],
|
|
1440
|
+
declaredExportClassName(field.jsType || field.type || null),
|
|
1441
|
+
options,
|
|
1442
|
+
context
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
return out;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
function exportAdvancedValue(value, declaredClassName, options, context)
|
|
1450
|
+
{
|
|
1451
|
+
if (value instanceof CjsModel)
|
|
1452
|
+
{
|
|
1453
|
+
if (context && options.refs && context.emitted.has(value))
|
|
1454
|
+
{
|
|
1455
|
+
return { _ref: context.getId(value) };
|
|
1456
|
+
}
|
|
1457
|
+
return exportModelInto(value, {}, declaredClassName, options, context);
|
|
1458
|
+
}
|
|
1459
|
+
if (Array.isArray(value))
|
|
1460
|
+
{
|
|
1461
|
+
if (options.keyedLists)
|
|
1462
|
+
{
|
|
1463
|
+
const keyed = exportKeyedList(value, declaredClassName, options, context);
|
|
1464
|
+
if (keyed) return keyed;
|
|
1465
|
+
}
|
|
1466
|
+
return value.map(item => exportAdvancedValue(item, declaredClassName, options, context));
|
|
1467
|
+
}
|
|
1468
|
+
return exportCarbonValue(value);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function exportKeyedList(list, declaredClassName, options, context)
|
|
1472
|
+
{
|
|
1473
|
+
if (!list.length) return null;
|
|
1474
|
+
|
|
1475
|
+
const seen = new Set();
|
|
1476
|
+
for (const item of list)
|
|
1477
|
+
{
|
|
1478
|
+
if (!(item instanceof CjsModel)) return null;
|
|
1479
|
+
const name = item.name;
|
|
1480
|
+
if (typeof name !== "string" || name === "" || seen.has(name)) return null;
|
|
1481
|
+
seen.add(name);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
const out = {};
|
|
1485
|
+
for (const item of list)
|
|
1486
|
+
{
|
|
1487
|
+
const exported = exportAdvancedValue(item, declaredClassName, options, context);
|
|
1488
|
+
if (exported && typeof exported === "object" && exported._ref === undefined)
|
|
1489
|
+
{
|
|
1490
|
+
delete exported.name;
|
|
1491
|
+
}
|
|
1492
|
+
out[item.name] = exported;
|
|
1493
|
+
}
|
|
1494
|
+
return out;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
export function importSourceValue(value, field = null, options = {})
|
|
1498
|
+
{
|
|
1499
|
+
if (value instanceof CjsModel) return value;
|
|
1500
|
+
|
|
1501
|
+
if (isReferenceValue(value))
|
|
1502
|
+
{
|
|
1503
|
+
const resolved = resolveIncomingReference(value, options);
|
|
1504
|
+
if (resolved instanceof CjsPendingReference)
|
|
1505
|
+
{
|
|
1506
|
+
throw new TypeError(`Forward { _ref: ${JSON.stringify(value._ref)} } cannot be deferred in this position.`);
|
|
1507
|
+
}
|
|
1508
|
+
return resolved;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
const schemaType = getSchemaType(options.ownerConstructor, field?.name);
|
|
1512
|
+
const declaredClassName = getSchemaClassName(schemaType, options);
|
|
1513
|
+
if (value && typeof value === "object" && !(value instanceof CjsModel) && !Array.isArray(value) && !ArrayBuffer.isView(value))
|
|
1514
|
+
{
|
|
1515
|
+
// A registered `_type` selects the concrete class in singular
|
|
1516
|
+
// schema-typed positions. Carbon contracts may be declared through
|
|
1517
|
+
// interface names with no runtime inheritance, so the declared name
|
|
1518
|
+
// is a fallback, not a constraint the concrete class must extend.
|
|
1519
|
+
const explicitClassName = isSingularSchemaKind(schemaType) && typeof value._type === "string"
|
|
1520
|
+
? getSchemaClassName(value._type, options)
|
|
1521
|
+
: null;
|
|
1522
|
+
const className = explicitClassName || declaredClassName;
|
|
1523
|
+
if (className)
|
|
1524
|
+
{
|
|
1525
|
+
return createModelValue(className, value, options);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
if ((schemaType?.kind === "array" || schemaType?.kind === "list") && schemaType.itemType && Array.isArray(value))
|
|
1530
|
+
{
|
|
1531
|
+
const itemClassName = getSchemaClassName(schemaType.itemType, options);
|
|
1532
|
+
const result = [];
|
|
1533
|
+
for (let i = 0; i < value.length; i++)
|
|
1534
|
+
{
|
|
1535
|
+
const item = value[i];
|
|
1536
|
+
if (isReferenceValue(item))
|
|
1537
|
+
{
|
|
1538
|
+
result.push(importReferenceInto(item, options, result, i));
|
|
1539
|
+
continue;
|
|
1540
|
+
}
|
|
1541
|
+
if (!item || typeof item !== "object" || item instanceof CjsModel || ArrayBuffer.isView(item))
|
|
1542
|
+
{
|
|
1543
|
+
result.push(importSourceValue(item, null, options));
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
const explicitClassName = typeof item._type === "string"
|
|
1548
|
+
? getSchemaClassName(item._type, options)
|
|
1549
|
+
: null;
|
|
1550
|
+
if (explicitClassName)
|
|
1551
|
+
{
|
|
1552
|
+
result.push(createModelValue(explicitClassName, item, options));
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
result.push(itemClassName
|
|
1557
|
+
? createModelValue(itemClassName, item, options)
|
|
1558
|
+
: importSourceValue(item, null, options));
|
|
1559
|
+
}
|
|
1560
|
+
return result;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// List fields also accept name-keyed object maps for unique-named items;
|
|
1564
|
+
// the map is a wholesale list replacement, mirroring array semantics.
|
|
1565
|
+
const effectiveType = field?.jsType || field?.type || schemaType;
|
|
1566
|
+
if ((effectiveType?.kind === "array" || effectiveType?.kind === "list")
|
|
1567
|
+
&& value && typeof value === "object" && !Array.isArray(value) && !ArrayBuffer.isView(value))
|
|
1568
|
+
{
|
|
1569
|
+
return importListMapValue(value, effectiveType, options);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
if (field) return normalizeCarbonValue(value, field);
|
|
1573
|
+
if (ArrayBuffer.isView(value)) return normalizeCarbonValue(value, { jsType: { kind: "typedArray", js: value.constructor.name } });
|
|
1574
|
+
if (typeof value === "bigint") return value;
|
|
1575
|
+
if (Array.isArray(value))
|
|
1576
|
+
{
|
|
1577
|
+
const result = [];
|
|
1578
|
+
for (let i = 0; i < value.length; i++)
|
|
1579
|
+
{
|
|
1580
|
+
const item = value[i];
|
|
1581
|
+
result.push(isReferenceValue(item)
|
|
1582
|
+
? importReferenceInto(item, options, result, i)
|
|
1583
|
+
: importSourceValue(item, null, options));
|
|
1584
|
+
}
|
|
1585
|
+
return result;
|
|
1586
|
+
}
|
|
1587
|
+
if (value && typeof value === "object" && !(value instanceof CjsModel))
|
|
1588
|
+
{
|
|
1589
|
+
const result = {};
|
|
1590
|
+
for (const [key, item] of Object.entries(value))
|
|
1591
|
+
{
|
|
1592
|
+
result[key] = isReferenceValue(item)
|
|
1593
|
+
? importReferenceInto(item, options, result, key)
|
|
1594
|
+
: importSourceValue(item, null, options);
|
|
1595
|
+
}
|
|
1596
|
+
return result;
|
|
1597
|
+
}
|
|
1598
|
+
return value;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
function applyIncomingStructInPlace(current, incoming, field, options)
|
|
1602
|
+
{
|
|
1603
|
+
const schemaType = getSchemaType(options.ownerConstructor, field?.name) || field?.type || field?.jsType;
|
|
1604
|
+
if (schemaType?.kind !== "struct" || !(current instanceof CjsModel)) return null;
|
|
1605
|
+
if (incoming === null || incoming === undefined) return false;
|
|
1606
|
+
if (typeof incoming !== "object" || Array.isArray(incoming) || ArrayBuffer.isView(incoming))
|
|
1607
|
+
{
|
|
1608
|
+
throw new TypeError(`${field.name} requires an object value for registered struct ${schemaType.className || "unknown"}.`);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
const values = incoming instanceof CjsModel ? incoming.GetValues() : incoming;
|
|
1612
|
+
const changed = current.SetValues(values, options);
|
|
1613
|
+
return changed instanceof Set ? changed.size > 0 : changed === true;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function importListMapValue(value, schemaType, options)
|
|
1617
|
+
{
|
|
1618
|
+
const itemClassName = schemaType.itemType ? getSchemaClassName(schemaType.itemType, options) : null;
|
|
1619
|
+
const result = [];
|
|
1620
|
+
for (const key of Object.keys(value))
|
|
1621
|
+
{
|
|
1622
|
+
const item = value[key];
|
|
1623
|
+
if (item === undefined || item === null) continue;
|
|
1624
|
+
|
|
1625
|
+
if (item instanceof CjsModel)
|
|
1626
|
+
{
|
|
1627
|
+
if (typeof item.name === "string" && item.name === "")
|
|
1628
|
+
{
|
|
1629
|
+
item.SetValues({ name: key });
|
|
1630
|
+
}
|
|
1631
|
+
result.push(item);
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
if (isReferenceValue(item))
|
|
1636
|
+
{
|
|
1637
|
+
// Shared items keep their own name; the map key is not restamped
|
|
1638
|
+
// onto an instance owned by another position in the graph.
|
|
1639
|
+
result.push(importReferenceInto(item, options, result, result.length));
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
if (typeof item !== "object" || Array.isArray(item) || ArrayBuffer.isView(item))
|
|
1644
|
+
{
|
|
1645
|
+
throw new TypeError(`List field maps require object or model values; "${key}" cannot become a list item.`);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
const explicitClassName = typeof item._type === "string"
|
|
1649
|
+
? getSchemaClassName(item._type, options)
|
|
1650
|
+
: null;
|
|
1651
|
+
const className = explicitClassName || itemClassName;
|
|
1652
|
+
if (!className)
|
|
1653
|
+
{
|
|
1654
|
+
throw new TypeError(`List field maps cannot resolve a model class for "${key}".`);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
const values = item.name === undefined ? { ...item, name: key } : item;
|
|
1658
|
+
result.push(createModelValue(className, values, options));
|
|
1659
|
+
}
|
|
1660
|
+
return result;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
function getSchemaType(Constructor, fieldName)
|
|
1664
|
+
{
|
|
1665
|
+
if (!Constructor || !fieldName) return null;
|
|
1666
|
+
return CjsSchema.getField(Constructor, fieldName)?.type || null;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
function getSchemaClassName(schemaType, options = {})
|
|
1670
|
+
{
|
|
1671
|
+
if (!schemaType) return null;
|
|
1672
|
+
if (typeof schemaType === "string")
|
|
1673
|
+
{
|
|
1674
|
+
const Schema = options.registry || CjsModel.schema;
|
|
1675
|
+
return Schema.GetConstructor(schemaType) ? schemaType : null;
|
|
1676
|
+
}
|
|
1677
|
+
if (schemaType.kind === "model")
|
|
1678
|
+
{
|
|
1679
|
+
return schemaType.className || null;
|
|
1680
|
+
}
|
|
1681
|
+
if (schemaType.kind === "objectRef" || schemaType.kind === "struct")
|
|
1682
|
+
{
|
|
1683
|
+
const Schema = options.registry || CjsModel.schema;
|
|
1684
|
+
return schemaType.className && Schema.GetConstructor(schemaType.className)
|
|
1685
|
+
? schemaType.className
|
|
1686
|
+
: null;
|
|
1687
|
+
}
|
|
1688
|
+
return null;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
function createModelValue(className, values, options)
|
|
1692
|
+
{
|
|
1693
|
+
const Schema = options.registry || CjsModel.schema;
|
|
1694
|
+
const Constructor = Schema.GetConstructor(className);
|
|
1695
|
+
if (!Constructor)
|
|
1696
|
+
{
|
|
1697
|
+
throw new TypeError(`No CjsModel class is registered for schema type ${className}.`);
|
|
1698
|
+
}
|
|
1699
|
+
if (Constructor !== CjsModel && !(Constructor.prototype instanceof CjsModel))
|
|
1700
|
+
{
|
|
1701
|
+
throw new TypeError(`Registered schema type ${className} is not a CjsModel.`);
|
|
1702
|
+
}
|
|
1703
|
+
if (typeof Constructor.from !== "function")
|
|
1704
|
+
{
|
|
1705
|
+
throw new TypeError(`Registered CjsModel ${className} does not provide from().`);
|
|
1706
|
+
}
|
|
1707
|
+
return Constructor.from(values, options);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// --- Import operation context: `_id`/`_ref` identity across one call tree ---
|
|
1711
|
+
|
|
1712
|
+
/** Represents one unresolved model reference during a single import operation. */
|
|
1713
|
+
class CjsPendingReference
|
|
1714
|
+
{
|
|
1715
|
+
|
|
1716
|
+
constructor(id, expectedClassName = null)
|
|
1717
|
+
{
|
|
1718
|
+
this.id = id;
|
|
1719
|
+
this.expectedClassName = expectedClassName;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function createImportContext()
|
|
1725
|
+
{
|
|
1726
|
+
const byId = new Map();
|
|
1727
|
+
const created = [];
|
|
1728
|
+
const pending = [];
|
|
1729
|
+
return {
|
|
1730
|
+
byId,
|
|
1731
|
+
registerCreated(instance)
|
|
1732
|
+
{
|
|
1733
|
+
created.push(instance);
|
|
1734
|
+
},
|
|
1735
|
+
register(id, instance)
|
|
1736
|
+
{
|
|
1737
|
+
const existing = byId.get(id);
|
|
1738
|
+
if (existing === instance) return;
|
|
1739
|
+
if (existing !== undefined)
|
|
1740
|
+
{
|
|
1741
|
+
throw new TypeError(`Duplicate _id ${JSON.stringify(id)} in imported values.`);
|
|
1742
|
+
}
|
|
1743
|
+
byId.set(id, instance);
|
|
1744
|
+
},
|
|
1745
|
+
defer(id, assign)
|
|
1746
|
+
{
|
|
1747
|
+
pending.push({ id, assign });
|
|
1748
|
+
},
|
|
1749
|
+
finalize()
|
|
1750
|
+
{
|
|
1751
|
+
const unresolved = new Set();
|
|
1752
|
+
for (const entry of pending)
|
|
1753
|
+
{
|
|
1754
|
+
const instance = byId.get(entry.id);
|
|
1755
|
+
if (instance === undefined)
|
|
1756
|
+
{
|
|
1757
|
+
unresolved.add(entry.id);
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
entry.assign(instance);
|
|
1761
|
+
}
|
|
1762
|
+
pending.length = 0;
|
|
1763
|
+
if (unresolved.size)
|
|
1764
|
+
{
|
|
1765
|
+
throw new TypeError(`Unresolved _ref ids: ${Array.from(unresolved, id => JSON.stringify(id)).join(", ")}. Every { _ref } must match a { _id } in the same import operation.`);
|
|
1766
|
+
}
|
|
1767
|
+
},
|
|
1768
|
+
initializeCreated(options)
|
|
1769
|
+
{
|
|
1770
|
+
const visited = new Set();
|
|
1771
|
+
|
|
1772
|
+
for (let index = created.length - 1; index >= 0; index--)
|
|
1773
|
+
{
|
|
1774
|
+
initializeOwnedGraph(created[index], { ...options, visited });
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
created.length = 0;
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
function isReferenceValue(value)
|
|
1783
|
+
{
|
|
1784
|
+
return !!value && typeof value === "object" && !Array.isArray(value)
|
|
1785
|
+
&& !ArrayBuffer.isView(value) && !(value instanceof CjsModel)
|
|
1786
|
+
&& value._ref !== undefined;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// Resolves a `{ _ref }` immediately when the target is registered, or returns
|
|
1790
|
+
// a CjsPendingReference for the finalize pass (forward references). Resolved
|
|
1791
|
+
// references assign like direct instances: no declared-type constraint, since
|
|
1792
|
+
// Carbon contracts may be declared through interface names that have no
|
|
1793
|
+
// runtime inheritance relationship with the concrete class.
|
|
1794
|
+
function resolveIncomingReference(value, options)
|
|
1795
|
+
{
|
|
1796
|
+
const id = value._ref;
|
|
1797
|
+
const context = options.importContext;
|
|
1798
|
+
if (!context)
|
|
1799
|
+
{
|
|
1800
|
+
throw new TypeError(`Cannot resolve { _ref: ${JSON.stringify(id)} } outside an import operation; import the graph through SetValues/from so identity is tracked.`);
|
|
1801
|
+
}
|
|
1802
|
+
const resolved = context.byId.get(id);
|
|
1803
|
+
if (resolved === undefined) return new CjsPendingReference(id);
|
|
1804
|
+
return resolved;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// Resolves a reference into a container slot, deferring forward references to
|
|
1808
|
+
// the owning operation's finalize pass. Deferred slots hold null until then.
|
|
1809
|
+
function importReferenceInto(value, options, target, key)
|
|
1810
|
+
{
|
|
1811
|
+
const resolved = resolveIncomingReference(value, options);
|
|
1812
|
+
if (resolved instanceof CjsPendingReference)
|
|
1813
|
+
{
|
|
1814
|
+
options.importContext.defer(resolved.id, instance =>
|
|
1815
|
+
{
|
|
1816
|
+
target[key] = instance;
|
|
1817
|
+
});
|
|
1818
|
+
return null;
|
|
1819
|
+
}
|
|
1820
|
+
return resolved;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// Applies a `{ _ref }` incoming value to a model field, returning whether the
|
|
1824
|
+
// field changed. Forward references keep the current value until finalize.
|
|
1825
|
+
function applyIncomingReference(out, field, incoming, options)
|
|
1826
|
+
{
|
|
1827
|
+
const fieldName = field.name;
|
|
1828
|
+
const resolved = resolveIncomingReference(incoming, options);
|
|
1829
|
+
|
|
1830
|
+
if (resolved instanceof CjsPendingReference)
|
|
1831
|
+
{
|
|
1832
|
+
options.importContext.defer(resolved.id, instance =>
|
|
1833
|
+
{
|
|
1834
|
+
out[fieldName] = instance;
|
|
1835
|
+
});
|
|
1836
|
+
return true;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
if (field.io?.always !== true && Object.is(out[fieldName], resolved)) return false;
|
|
1840
|
+
out[fieldName] = resolved;
|
|
1841
|
+
return true;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
function resolveRegisteredModelClass(typeName, options = {})
|
|
1845
|
+
{
|
|
1846
|
+
const Schema = options.registry || CjsModel.schema;
|
|
1847
|
+
const Constructor = Schema.GetConstructor(typeName);
|
|
1848
|
+
if (!Constructor)
|
|
1849
|
+
{
|
|
1850
|
+
throw new TypeError(`No CjsModel class is registered for _type "${typeName}".`);
|
|
1851
|
+
}
|
|
1852
|
+
return Constructor;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
function isSingularSchemaKind(schemaType)
|
|
1856
|
+
{
|
|
1857
|
+
if (!schemaType) return false;
|
|
1858
|
+
if (typeof schemaType === "string") return true;
|
|
1859
|
+
return schemaType.kind !== "array" && schemaType.kind !== "list"
|
|
1860
|
+
&& schemaType.kind !== "map" && schemaType.kind !== "set";
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
function assertTargetTypeMatches(out, typeName, options = {})
|
|
1864
|
+
{
|
|
1865
|
+
const Constructor = resolveRegisteredModelClass(typeName, options);
|
|
1866
|
+
if (!(out instanceof Constructor))
|
|
1867
|
+
{
|
|
1868
|
+
throw new TypeError(`Values with _type "${typeName}" cannot apply to a ${CjsSchema.getClassName(out.constructor) || "model"} target.`);
|
|
1869
|
+
}
|
|
1870
|
+
}
|