@axi-engine/fields 0.1.5 → 0.2.1
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/README.md +13 -7
- package/dist/index.d.mts +699 -214
- package/dist/index.d.ts +699 -214
- package/dist/index.js +782 -381
- package/dist/index.mjs +762 -372
- package/package.json +3 -6
package/dist/index.js
CHANGED
|
@@ -20,36 +20,41 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
-
BaseFields: () => BaseFields,
|
|
24
23
|
ClampMaxPolicy: () => ClampMaxPolicy,
|
|
24
|
+
ClampMaxPolicySerializerHandler: () => ClampMaxPolicySerializerHandler,
|
|
25
25
|
ClampMinPolicy: () => ClampMinPolicy,
|
|
26
|
+
ClampMinPolicySerializerHandler: () => ClampMinPolicySerializerHandler,
|
|
26
27
|
ClampPolicy: () => ClampPolicy,
|
|
27
|
-
|
|
28
|
+
ClampPolicySerializerHandler: () => ClampPolicySerializerHandler,
|
|
29
|
+
DefaultBooleanField: () => DefaultBooleanField,
|
|
30
|
+
DefaultField: () => DefaultField,
|
|
31
|
+
DefaultFields: () => DefaultFields,
|
|
32
|
+
DefaultFieldsFactory: () => DefaultFieldsFactory,
|
|
33
|
+
DefaultNumericField: () => DefaultNumericField,
|
|
34
|
+
DefaultStringField: () => DefaultStringField,
|
|
35
|
+
DefaultTreeNodeFactory: () => DefaultTreeNodeFactory,
|
|
36
|
+
FieldRegistry: () => FieldRegistry,
|
|
37
|
+
FieldSerializer: () => FieldSerializer,
|
|
28
38
|
FieldTree: () => FieldTree,
|
|
39
|
+
FieldTreeSerializer: () => FieldTreeSerializer,
|
|
29
40
|
Fields: () => Fields,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
FieldsSerializer: () => FieldsSerializer,
|
|
42
|
+
Policies: () => Policies,
|
|
43
|
+
PolicySerializer: () => PolicySerializer,
|
|
33
44
|
clampMaxPolicy: () => clampMaxPolicy,
|
|
34
45
|
clampMinPolicy: () => clampMinPolicy,
|
|
35
46
|
clampPolicy: () => clampPolicy
|
|
36
47
|
});
|
|
37
48
|
module.exports = __toCommonJS(index_exports);
|
|
38
49
|
|
|
39
|
-
// src/
|
|
40
|
-
var
|
|
41
|
-
FieldsNodeType2["fieldTree"] = "FieldTree";
|
|
42
|
-
FieldsNodeType2["fields"] = "Fields";
|
|
43
|
-
return FieldsNodeType2;
|
|
44
|
-
})(FieldsNodeType || {});
|
|
45
|
-
|
|
46
|
-
// src/field-policies.ts
|
|
47
|
-
var _ClampPolicy = class _ClampPolicy {
|
|
50
|
+
// src/policies/clamp-policy.ts
|
|
51
|
+
var ClampPolicy = class _ClampPolicy {
|
|
48
52
|
constructor(min, max) {
|
|
49
53
|
this.min = min;
|
|
50
54
|
this.max = max;
|
|
51
|
-
this.id = _ClampPolicy.id;
|
|
52
55
|
}
|
|
56
|
+
static id = "clamp";
|
|
57
|
+
id = _ClampPolicy.id;
|
|
53
58
|
apply(val) {
|
|
54
59
|
return Math.max(this.min, Math.min(this.max, val));
|
|
55
60
|
}
|
|
@@ -58,27 +63,17 @@ var _ClampPolicy = class _ClampPolicy {
|
|
|
58
63
|
this.max = max;
|
|
59
64
|
}
|
|
60
65
|
};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
apply(val) {
|
|
69
|
-
return Math.max(this.min, val);
|
|
70
|
-
}
|
|
71
|
-
updateBounds(min) {
|
|
72
|
-
this.min = min;
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
_ClampMinPolicy.id = "clampMin";
|
|
76
|
-
var ClampMinPolicy = _ClampMinPolicy;
|
|
77
|
-
var _ClampMaxPolicy = class _ClampMaxPolicy {
|
|
66
|
+
function clampPolicy(min, max) {
|
|
67
|
+
return new ClampPolicy(min, max);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/policies/clamp-max-policy.ts
|
|
71
|
+
var ClampMaxPolicy = class _ClampMaxPolicy {
|
|
78
72
|
constructor(max) {
|
|
79
73
|
this.max = max;
|
|
80
|
-
this.id = _ClampMaxPolicy.id;
|
|
81
74
|
}
|
|
75
|
+
static id = "clampMax";
|
|
76
|
+
id = _ClampMaxPolicy.id;
|
|
82
77
|
apply(val) {
|
|
83
78
|
return Math.min(this.max, val);
|
|
84
79
|
}
|
|
@@ -86,58 +81,33 @@ var _ClampMaxPolicy = class _ClampMaxPolicy {
|
|
|
86
81
|
this.max = max;
|
|
87
82
|
}
|
|
88
83
|
};
|
|
89
|
-
_ClampMaxPolicy.id = "clampMax";
|
|
90
|
-
var ClampMaxPolicy = _ClampMaxPolicy;
|
|
91
|
-
function clampPolicy(min, max) {
|
|
92
|
-
return new ClampPolicy(min, max);
|
|
93
|
-
}
|
|
94
|
-
function clampMinPolicy(min) {
|
|
95
|
-
return new ClampMinPolicy(min);
|
|
96
|
-
}
|
|
97
84
|
function clampMaxPolicy(max) {
|
|
98
85
|
return new ClampMaxPolicy(max);
|
|
99
86
|
}
|
|
100
87
|
|
|
101
|
-
// src/
|
|
102
|
-
var
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
* Creates an instance of a Field.
|
|
106
|
-
* @param name A unique identifier for the field.
|
|
107
|
-
* @param initialVal The initial value of the field.
|
|
108
|
-
* @param options Optional configuration for the field.
|
|
109
|
-
* @param options.policies An array of policies to apply to the field's value on every `set` operation.
|
|
110
|
-
*/
|
|
111
|
-
constructor(name, initialVal, options) {
|
|
112
|
-
this.policies = /* @__PURE__ */ new Map();
|
|
113
|
-
this._val = (0, import_signals_core.signal)(initialVal);
|
|
114
|
-
this.name = name;
|
|
115
|
-
options?.policies?.forEach((policy) => this.policies.set(policy.id, policy));
|
|
116
|
-
this.set(initialVal);
|
|
88
|
+
// src/policies/clamp-min-policy.ts
|
|
89
|
+
var ClampMinPolicy = class _ClampMinPolicy {
|
|
90
|
+
constructor(min) {
|
|
91
|
+
this.min = min;
|
|
117
92
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
get val() {
|
|
123
|
-
return this._val.value;
|
|
93
|
+
static id = "clampMin";
|
|
94
|
+
id = _ClampMinPolicy.id;
|
|
95
|
+
apply(val) {
|
|
96
|
+
return Math.max(this.min, val);
|
|
124
97
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
* Subscribe to this signal to react to value changes.
|
|
128
|
-
*/
|
|
129
|
-
get signal() {
|
|
130
|
-
return this._val;
|
|
98
|
+
updateBounds(min) {
|
|
99
|
+
this.min = min;
|
|
131
100
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
101
|
+
};
|
|
102
|
+
function clampMinPolicy(min) {
|
|
103
|
+
return new ClampMinPolicy(min);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/policies/policies.ts
|
|
107
|
+
var Policies = class {
|
|
108
|
+
policies = /* @__PURE__ */ new Map();
|
|
109
|
+
get items() {
|
|
110
|
+
return this.policies;
|
|
141
111
|
}
|
|
142
112
|
/**
|
|
143
113
|
* Retrieves a specific policy instance by its ID.
|
|
@@ -146,7 +116,7 @@ var Field = class {
|
|
|
146
116
|
* @param id The unique ID of the policy to retrieve.
|
|
147
117
|
* @returns The policy instance, or `undefined` if not found.
|
|
148
118
|
*/
|
|
149
|
-
|
|
119
|
+
get(id) {
|
|
150
120
|
return this.policies.get(id);
|
|
151
121
|
}
|
|
152
122
|
/**
|
|
@@ -155,17 +125,18 @@ var Field = class {
|
|
|
155
125
|
* If a policy with the same ID already exists, its `destroy` method will be called before it is replaced.
|
|
156
126
|
* @param policy The policy instance to add.
|
|
157
127
|
*/
|
|
158
|
-
|
|
128
|
+
add(policy) {
|
|
159
129
|
const existed = this.policies.get(policy.id);
|
|
160
130
|
existed?.destroy?.();
|
|
161
131
|
this.policies.set(policy.id, policy);
|
|
132
|
+
return this;
|
|
162
133
|
}
|
|
163
134
|
/**
|
|
164
135
|
* Removes a policy from the field by its ID and call `destroy` method.
|
|
165
136
|
* @param policyId The unique ID of the policy to remove.
|
|
166
137
|
* @returns `true` if the policy was found and removed, otherwise `false`.
|
|
167
138
|
*/
|
|
168
|
-
|
|
139
|
+
remove(policyId) {
|
|
169
140
|
const policyToRemove = this.policies.get(policyId);
|
|
170
141
|
if (!policyToRemove) {
|
|
171
142
|
return false;
|
|
@@ -173,11 +144,14 @@ var Field = class {
|
|
|
173
144
|
policyToRemove.destroy?.();
|
|
174
145
|
return this.policies.delete(policyId);
|
|
175
146
|
}
|
|
147
|
+
isEmpty() {
|
|
148
|
+
return this.policies.size === 0;
|
|
149
|
+
}
|
|
176
150
|
/**
|
|
177
151
|
* Removes all policies from the field.
|
|
178
152
|
* After this, `set()` will no longer apply any transformations to the value until new policies are added.
|
|
179
153
|
*/
|
|
180
|
-
|
|
154
|
+
clear() {
|
|
181
155
|
this.policies.forEach((policy) => policy.destroy?.());
|
|
182
156
|
this.policies.clear();
|
|
183
157
|
}
|
|
@@ -185,406 +159,849 @@ var Field = class {
|
|
|
185
159
|
* Forces the current value to be re-processed by all policies.
|
|
186
160
|
* Useful if a policy's logic has changed and you need to re-evaluate the current state.
|
|
187
161
|
*/
|
|
188
|
-
|
|
189
|
-
|
|
162
|
+
apply(val) {
|
|
163
|
+
let finalVal = val;
|
|
164
|
+
this.policies.forEach((policy) => finalVal = policy.apply(finalVal));
|
|
165
|
+
return finalVal;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// src/field-definitions/default-field.ts
|
|
170
|
+
var import_utils = require("@axi-engine/utils");
|
|
171
|
+
var import_dequal = require("dequal");
|
|
172
|
+
var DefaultField = class _DefaultField {
|
|
173
|
+
/** A type keyword of the field */
|
|
174
|
+
static typeName = "default";
|
|
175
|
+
typeName = _DefaultField.typeName;
|
|
176
|
+
/** A unique identifier for the field. */
|
|
177
|
+
_name;
|
|
178
|
+
_value;
|
|
179
|
+
_onChange = new import_utils.Emitter();
|
|
180
|
+
onChange;
|
|
181
|
+
policies = new Policies();
|
|
182
|
+
get name() {
|
|
183
|
+
return this._name;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Gets the current raw value of the field.
|
|
187
|
+
* For reactive updates, it's recommended to use the `.signal` property instead.
|
|
188
|
+
*/
|
|
189
|
+
get value() {
|
|
190
|
+
return this._value;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Sets a new value for the field.
|
|
194
|
+
* The provided value will be processed by all registered policies before the underlying signal is updated.
|
|
195
|
+
* @param val The new value to set.
|
|
196
|
+
*/
|
|
197
|
+
set value(val) {
|
|
198
|
+
const oldVal = this._value;
|
|
199
|
+
const finalVal = this.policies.apply(val);
|
|
200
|
+
if (!(0, import_dequal.dequal)(this._value, finalVal)) {
|
|
201
|
+
this._value = finalVal;
|
|
202
|
+
this._onChange.emit(this._value, oldVal);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Creates an instance of a Field.
|
|
207
|
+
* @param name A unique identifier for the field.
|
|
208
|
+
* @param initialVal The initial value of the field.
|
|
209
|
+
* @param options Optional configuration for the field.
|
|
210
|
+
* @param options.policies An array of policies to apply to the field's value on every `set` operation.
|
|
211
|
+
* @param options.isEqual An function for compare old and new value, by default uses the strictEquals from `utils`
|
|
212
|
+
*
|
|
213
|
+
*/
|
|
214
|
+
constructor(name, initialVal, options) {
|
|
215
|
+
this.onChange = this._onChange;
|
|
216
|
+
this._name = name;
|
|
217
|
+
options?.policies?.forEach((policy) => this.policies.add(policy));
|
|
218
|
+
this.value = initialVal;
|
|
219
|
+
}
|
|
220
|
+
setValueSilently(val) {
|
|
221
|
+
this._value = this.policies.apply(val);
|
|
222
|
+
}
|
|
223
|
+
batchUpdate(updateFn) {
|
|
224
|
+
this.value = updateFn(this.value);
|
|
190
225
|
}
|
|
191
226
|
/**
|
|
192
227
|
* Cleans up resources used by the field and its policies.
|
|
193
228
|
* This should be called when the field is no longer needed to prevent memory leaks from reactive policies.
|
|
194
229
|
*/
|
|
195
230
|
destroy() {
|
|
196
|
-
this.
|
|
231
|
+
this.policies.clear();
|
|
232
|
+
this._onChange.clear();
|
|
197
233
|
}
|
|
198
234
|
};
|
|
199
235
|
|
|
200
|
-
// src/
|
|
201
|
-
var
|
|
202
|
-
|
|
236
|
+
// src/field-definitions/default-boolean-field.ts
|
|
237
|
+
var DefaultBooleanField = class _DefaultBooleanField extends DefaultField {
|
|
238
|
+
static typeName = "boolean";
|
|
239
|
+
typeName = _DefaultBooleanField.typeName;
|
|
240
|
+
constructor(name, initialVal, options) {
|
|
241
|
+
super(name, initialVal, options);
|
|
242
|
+
}
|
|
243
|
+
toggle() {
|
|
244
|
+
this.value = !this.value;
|
|
245
|
+
return this.value;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// src/field-definitions/default-string-field.ts
|
|
250
|
+
var DefaultStringField = class _DefaultStringField extends DefaultField {
|
|
251
|
+
static typeName = "string";
|
|
252
|
+
typeName = _DefaultStringField.typeName;
|
|
253
|
+
constructor(name, initialVal, options) {
|
|
254
|
+
super(name, initialVal, options);
|
|
255
|
+
}
|
|
256
|
+
append(str) {
|
|
257
|
+
this.value = this.value + str;
|
|
258
|
+
return this;
|
|
259
|
+
}
|
|
260
|
+
prepend(str) {
|
|
261
|
+
this.value = str + this.value;
|
|
262
|
+
return this;
|
|
263
|
+
}
|
|
264
|
+
trim() {
|
|
265
|
+
this.value = this.value.trim();
|
|
266
|
+
return this;
|
|
267
|
+
}
|
|
268
|
+
isEmpty() {
|
|
269
|
+
return this.value.length === 0;
|
|
270
|
+
}
|
|
271
|
+
clear() {
|
|
272
|
+
this.value = "";
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// src/field-definitions/default-numeric-field.ts
|
|
277
|
+
var import_utils2 = require("@axi-engine/utils");
|
|
278
|
+
var DefaultNumericField = class _DefaultNumericField extends DefaultField {
|
|
279
|
+
static typeName = "numeric";
|
|
280
|
+
typeName = _DefaultNumericField.typeName;
|
|
203
281
|
get min() {
|
|
204
|
-
const policy = this.
|
|
282
|
+
const policy = this.policies.get(ClampPolicy.id) ?? this.policies.get(ClampMinPolicy.id);
|
|
205
283
|
return policy?.min;
|
|
206
284
|
}
|
|
207
285
|
get max() {
|
|
208
|
-
const policy = this.
|
|
286
|
+
const policy = this.policies.get(ClampPolicy.id) ?? this.policies.get(ClampMaxPolicy.id);
|
|
209
287
|
return policy?.max;
|
|
210
288
|
}
|
|
211
|
-
get isMin() {
|
|
212
|
-
const min = this.min;
|
|
213
|
-
return (0, import_utils.isNullOrUndefined)(min) ? false : this.val <= min;
|
|
214
|
-
}
|
|
215
|
-
get isMax() {
|
|
216
|
-
const max = this.max;
|
|
217
|
-
return (0, import_utils.isNullOrUndefined)(max) ? false : this.val >= max;
|
|
218
|
-
}
|
|
219
289
|
constructor(name, initialVal, options) {
|
|
220
290
|
const policies = options?.policies ?? [];
|
|
221
|
-
if (!(0,
|
|
291
|
+
if (!(0, import_utils2.isNullOrUndefined)(options?.min) && !(0, import_utils2.isNullOrUndefined)(options?.max)) {
|
|
222
292
|
policies.unshift(clampPolicy(options.min, options.max));
|
|
223
|
-
} else if (!(0,
|
|
293
|
+
} else if (!(0, import_utils2.isNullOrUndefined)(options?.min)) {
|
|
224
294
|
policies.unshift(clampMinPolicy(options.min));
|
|
225
|
-
} else if (!(0,
|
|
295
|
+
} else if (!(0, import_utils2.isNullOrUndefined)(options?.max)) {
|
|
226
296
|
policies.unshift(clampMaxPolicy(options.max));
|
|
227
297
|
}
|
|
228
298
|
super(name, initialVal, { policies });
|
|
229
299
|
}
|
|
300
|
+
isMin() {
|
|
301
|
+
const min = this.min;
|
|
302
|
+
return (0, import_utils2.isNullOrUndefined)(min) ? false : this.value <= min;
|
|
303
|
+
}
|
|
304
|
+
isMax() {
|
|
305
|
+
const max = this.max;
|
|
306
|
+
return (0, import_utils2.isNullOrUndefined)(max) ? false : this.value >= max;
|
|
307
|
+
}
|
|
230
308
|
inc(amount = 1) {
|
|
231
|
-
this.
|
|
309
|
+
this.value = this.value + amount;
|
|
232
310
|
}
|
|
233
311
|
dec(amount = 1) {
|
|
234
|
-
this.
|
|
312
|
+
this.value = this.value - amount;
|
|
235
313
|
}
|
|
236
314
|
};
|
|
237
315
|
|
|
238
|
-
// src/
|
|
239
|
-
var
|
|
240
|
-
var
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
316
|
+
// src/utils/constructor-registry.ts
|
|
317
|
+
var import_utils3 = require("@axi-engine/utils");
|
|
318
|
+
var ConstructorRegistry = class {
|
|
319
|
+
items = /* @__PURE__ */ new Map();
|
|
320
|
+
/**
|
|
321
|
+
* Registers a constructor with a unique string identifier.
|
|
322
|
+
*
|
|
323
|
+
* @param typeId - The unique identifier for the constructor (e.g., a static `typeName` property from a class).
|
|
324
|
+
* @param ctor - The class constructor to register.
|
|
325
|
+
* @returns The registry instance for chainable calls.
|
|
326
|
+
* @throws If a constructor with the same `typeId` is already registered.
|
|
327
|
+
*/
|
|
328
|
+
register(typeId, ctor) {
|
|
329
|
+
(0, import_utils3.throwIf)(this.items.has(typeId), `A constructor with typeId '${typeId}' is already registered.`);
|
|
330
|
+
this.items.set(typeId, ctor);
|
|
331
|
+
return this;
|
|
246
332
|
}
|
|
247
333
|
/**
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
334
|
+
* Retrieves a constructor by its identifier.
|
|
335
|
+
*
|
|
336
|
+
* @param typeId - The identifier of the constructor to retrieve.
|
|
337
|
+
* @returns The found class constructor.
|
|
338
|
+
* @throws If no constructor is found for the given `typeId`.
|
|
339
|
+
*/
|
|
340
|
+
get(typeId) {
|
|
341
|
+
const Ctor = this.items.get(typeId);
|
|
342
|
+
(0, import_utils3.throwIfEmpty)(Ctor, `No constructor found for typeId '${typeId}'`);
|
|
343
|
+
return Ctor;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Checks if a constructor for a given identifier is registered.
|
|
347
|
+
* @param typeId - The identifier to check.
|
|
348
|
+
* @returns `true` if a constructor is registered, otherwise `false`.
|
|
349
|
+
*/
|
|
350
|
+
has(typeId) {
|
|
351
|
+
return this.items.has(typeId);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Clears all registered constructors from the registry.
|
|
355
|
+
*/
|
|
356
|
+
clear() {
|
|
357
|
+
this.items.clear();
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// src/field-registry.ts
|
|
362
|
+
var FieldRegistry = class extends ConstructorRegistry {
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// src/fields.ts
|
|
366
|
+
var import_utils4 = require("@axi-engine/utils");
|
|
367
|
+
var Fields = class _Fields {
|
|
368
|
+
static typeName = "fields";
|
|
369
|
+
typeName = _Fields.typeName;
|
|
370
|
+
_fields = /* @__PURE__ */ new Map();
|
|
371
|
+
_fieldRegistry;
|
|
372
|
+
/**
|
|
373
|
+
* An event emitter that fires when a new field is added to the collection.
|
|
374
|
+
* @event
|
|
375
|
+
* @param {object} event - The event payload.
|
|
376
|
+
* @param {string} event.name - The name of the added field.
|
|
377
|
+
* @param {Field<any>} event.field - The `Field` instance that was added.
|
|
378
|
+
*/
|
|
379
|
+
onAdd = new import_utils4.Emitter();
|
|
380
|
+
/**
|
|
381
|
+
* An event emitter that fires after one or more fields have been removed.
|
|
382
|
+
* @event
|
|
383
|
+
* @param {object} event - The event payload.
|
|
384
|
+
* @param {string[]} event.names - An array of names of the fields that were successfully removed.
|
|
385
|
+
*/
|
|
386
|
+
onRemove = new import_utils4.Emitter();
|
|
387
|
+
/**
|
|
388
|
+
* Gets the read-only map of all `Field` instances in this container.
|
|
389
|
+
* @returns {Map<string, Field<any>>} The collection of fields.
|
|
251
390
|
*/
|
|
252
391
|
get fields() {
|
|
253
392
|
return this._fields;
|
|
254
393
|
}
|
|
255
394
|
/**
|
|
256
|
-
*
|
|
257
|
-
* @param
|
|
258
|
-
* @returns `true` if the field exists, otherwise `false`.
|
|
395
|
+
* Creates an instance of Fields.
|
|
396
|
+
* @param {FieldRegistry} fieldRegistry - The registry used to create new `Field` instances.
|
|
259
397
|
*/
|
|
260
|
-
|
|
261
|
-
|
|
398
|
+
constructor(fieldRegistry) {
|
|
399
|
+
this._fieldRegistry = fieldRegistry;
|
|
262
400
|
}
|
|
263
401
|
/**
|
|
264
|
-
*
|
|
265
|
-
* @param name The
|
|
266
|
-
* @
|
|
267
|
-
* @returns The newly created `Field` instance.
|
|
402
|
+
* Checks if a field with the given name exists in the collection.
|
|
403
|
+
* @param {string} name The name of the field to check.
|
|
404
|
+
* @returns {boolean} `true` if the field exists, otherwise `false`.
|
|
268
405
|
*/
|
|
269
|
-
|
|
270
|
-
return this.
|
|
406
|
+
has(name) {
|
|
407
|
+
return this._fields.has(name);
|
|
271
408
|
}
|
|
272
409
|
/**
|
|
273
|
-
* Adds a pre-existing `Field` instance to the collection.
|
|
274
|
-
*
|
|
275
|
-
* @param field The `Field` instance to add.
|
|
276
|
-
* @returns The added `Field` instance
|
|
410
|
+
* Adds a pre-existing `Field` instance to the collection and fires the `onAdd` event.
|
|
411
|
+
* @template T - The specific `Field` type being added.
|
|
412
|
+
* @param {Field<any>} field - The `Field` instance to add.
|
|
413
|
+
* @returns {T} The added `Field` instance, cast to type `T`.
|
|
414
|
+
* @throws If a field with the same name already exists.
|
|
277
415
|
*/
|
|
278
416
|
add(field) {
|
|
279
|
-
(0,
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
this.events.emit("created", {
|
|
284
|
-
fieldName: field.name,
|
|
417
|
+
(0, import_utils4.throwIf)(this.has(field.name), `Field with name '${field.name}' already exists`);
|
|
418
|
+
this._fields.set(field.name, field);
|
|
419
|
+
this.onAdd.emit({
|
|
420
|
+
name: field.name,
|
|
285
421
|
field
|
|
286
422
|
});
|
|
287
423
|
return field;
|
|
288
424
|
}
|
|
289
425
|
/**
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
* @
|
|
293
|
-
* @
|
|
426
|
+
* Creates a new `Field` instance of a specified type, adds it to the collection, and returns it.
|
|
427
|
+
* This is the primary factory method for creating fields within this container.
|
|
428
|
+
* @template T - The expected `Field` type to be returned.
|
|
429
|
+
* @param {string} typeName - The registered type name of the field to create (e.g., 'numeric', 'boolean').
|
|
430
|
+
* @param {string} name - The unique name for the new field.
|
|
431
|
+
* @param {*} initialValue - The initial value for the new field.
|
|
432
|
+
* @param {*} [options] - Optional configuration passed to the field's constructor.
|
|
433
|
+
* @returns {T} The newly created `Field` instance.
|
|
294
434
|
*/
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
435
|
+
create(typeName, name, initialValue, options) {
|
|
436
|
+
const Ctor = this._fieldRegistry.get(typeName);
|
|
437
|
+
const field = new Ctor(name, initialValue, options);
|
|
438
|
+
this.add(field);
|
|
439
|
+
return field;
|
|
298
440
|
}
|
|
299
441
|
/**
|
|
300
|
-
*
|
|
301
|
-
* @
|
|
302
|
-
* @param
|
|
303
|
-
* @
|
|
442
|
+
* Updates an existing field's value or creates a new one if it doesn't exist.
|
|
443
|
+
* @template T - The expected `Field` type.
|
|
444
|
+
* @param {string} typeName - The type name to use if a new field needs to be created.
|
|
445
|
+
* @param {string} name - The name of the field to update or create.
|
|
446
|
+
* @param {*} value - The new value to set.
|
|
447
|
+
* @param {*} [options] - Optional configuration, used only if a new field is created.
|
|
448
|
+
* @returns {T} The existing or newly created `Field` instance.
|
|
304
449
|
*/
|
|
305
|
-
upset(name, value) {
|
|
450
|
+
upset(typeName, name, value, options) {
|
|
306
451
|
if (this.has(name)) {
|
|
307
452
|
const field = this.get(name);
|
|
308
|
-
field.
|
|
453
|
+
field.value = value;
|
|
309
454
|
return field;
|
|
310
455
|
}
|
|
311
|
-
return this.create(name, value);
|
|
456
|
+
return this.create(typeName, name, value, options);
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Retrieves a field by its name.
|
|
460
|
+
* @template T - The expected `Field` type to be returned.
|
|
461
|
+
* @param {string} name - The name of the field to retrieve.
|
|
462
|
+
* @returns {T} The `Field` instance.
|
|
463
|
+
* @throws If the field does not exist.
|
|
464
|
+
*/
|
|
465
|
+
get(name) {
|
|
466
|
+
(0, import_utils4.throwIf)(!this._fields.has(name), `Field with name '${name}' not exists`);
|
|
467
|
+
return this._fields.get(name);
|
|
312
468
|
}
|
|
313
469
|
/**
|
|
314
470
|
* Removes one or more fields from the collection.
|
|
315
471
|
* This method ensures that the `destroy` method of each removed field is called to clean up its resources.
|
|
316
|
-
* @param names A single name or an array of names to remove.
|
|
472
|
+
* @param {string| string[]} names A single name or an array of names to remove.
|
|
317
473
|
*/
|
|
318
474
|
remove(names) {
|
|
319
475
|
const namesToRemove = Array.isArray(names) ? names : [names];
|
|
320
|
-
const fieldsMap = new Map(this._fields.value);
|
|
321
476
|
const reallyRemoved = namesToRemove.filter((name) => {
|
|
322
|
-
const field =
|
|
477
|
+
const field = this._fields.get(name);
|
|
323
478
|
if (!field) {
|
|
324
479
|
return false;
|
|
325
480
|
}
|
|
326
481
|
field.destroy();
|
|
327
|
-
|
|
328
|
-
return true;
|
|
482
|
+
return this._fields.delete(name);
|
|
329
483
|
});
|
|
330
484
|
if (!reallyRemoved.length) {
|
|
331
485
|
return;
|
|
332
486
|
}
|
|
333
|
-
this.
|
|
334
|
-
this.events.emit("removed", { fieldNames: reallyRemoved });
|
|
487
|
+
this.onRemove.emit({ names: reallyRemoved });
|
|
335
488
|
}
|
|
336
489
|
/**
|
|
337
490
|
* Removes all fields from the collection, ensuring each is properly destroyed.
|
|
338
491
|
*/
|
|
339
492
|
clear() {
|
|
340
|
-
this.remove(Array.from(this._fields.
|
|
493
|
+
this.remove(Array.from(this._fields.keys()));
|
|
341
494
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
snapshot() {
|
|
347
|
-
const dump = {
|
|
348
|
-
__type: "Fields" /* fields */
|
|
349
|
-
};
|
|
350
|
-
this._fields.value.forEach((field, key) => dump[key] = field.val);
|
|
351
|
-
return dump;
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* Restores the state of the fields from a snapshot.
|
|
355
|
-
* It uses the `upset` logic to create or update fields based on the snapshot data.
|
|
356
|
-
* @param snapshot The snapshot object to load.
|
|
357
|
-
*/
|
|
358
|
-
hydrate(snapshot) {
|
|
359
|
-
for (let key in snapshot) {
|
|
360
|
-
if (key === "__type") {
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
this.upset(key, snapshot[key]);
|
|
364
|
-
}
|
|
495
|
+
destroy() {
|
|
496
|
+
this.clear();
|
|
497
|
+
this.onAdd.clear();
|
|
498
|
+
this.onRemove.clear();
|
|
365
499
|
}
|
|
366
500
|
};
|
|
367
501
|
|
|
368
|
-
// src/fields.ts
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
upsetNumber(name, value, options) {
|
|
375
|
-
if (this.has(name)) {
|
|
376
|
-
const field = this.getNumber(name);
|
|
377
|
-
field.set(value);
|
|
378
|
-
return field;
|
|
502
|
+
// src/mixins/with-boolean-fields.mixin.ts
|
|
503
|
+
function WithBooleanFields(Base) {
|
|
504
|
+
return class FieldsWithBoolean extends Base {
|
|
505
|
+
createBoolean(name, initialValue, options) {
|
|
506
|
+
return this.create(DefaultBooleanField.typeName, name, initialValue, options);
|
|
379
507
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
getNumber(name) {
|
|
383
|
-
const field = this.get(name);
|
|
384
|
-
(0, import_utils3.throwIf)(!(field instanceof NumberField), `wrong field type, field ${name} not a instance of NUmberField`);
|
|
385
|
-
return field;
|
|
386
|
-
}
|
|
387
|
-
create(name, initialValue) {
|
|
388
|
-
return this.add(new Field(name, initialValue));
|
|
389
|
-
}
|
|
390
|
-
upset(name, value) {
|
|
391
|
-
if (this.has(name)) {
|
|
392
|
-
const field = this.get(name);
|
|
393
|
-
field.set(value);
|
|
394
|
-
return field;
|
|
508
|
+
upsetBoolean(name, value, options) {
|
|
509
|
+
return this.upset(DefaultBooleanField.typeName, name, value, options);
|
|
395
510
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
511
|
+
getBoolean(name) {
|
|
512
|
+
return this.get(name);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/mixins/with-string-fields.mixin.ts
|
|
518
|
+
function WithStringFields(Base) {
|
|
519
|
+
return class FieldsWithString extends Base {
|
|
520
|
+
createString(name, initialValue, options) {
|
|
521
|
+
return this.create(DefaultStringField.typeName, name, initialValue, options);
|
|
522
|
+
}
|
|
523
|
+
upsetString(name, value, options) {
|
|
524
|
+
return this.upset(DefaultStringField.typeName, name, value, options);
|
|
525
|
+
}
|
|
526
|
+
getString(name) {
|
|
527
|
+
return this.get(name);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/mixins/with-numeric-fields.mixin.ts
|
|
533
|
+
function WithNumericFields(Base) {
|
|
534
|
+
return class FieldsWithNumeric extends Base {
|
|
535
|
+
createNumeric(name, initialValue, options) {
|
|
536
|
+
return this.create(DefaultNumericField.typeName, name, initialValue, options);
|
|
537
|
+
}
|
|
538
|
+
upsetNumeric(name, value, options) {
|
|
539
|
+
return this.upset(DefaultNumericField.typeName, name, value, options);
|
|
540
|
+
}
|
|
541
|
+
getNumeric(name) {
|
|
542
|
+
return this.get(name);
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/mixins/with-default-fields.mixin.ts
|
|
548
|
+
function WithDefaultFields(Base) {
|
|
549
|
+
return class FieldsWithDefault extends Base {
|
|
550
|
+
createDefault(name, initialValue, options) {
|
|
551
|
+
return this.create(DefaultField.typeName, name, initialValue, options);
|
|
552
|
+
}
|
|
553
|
+
upsetDefault(name, value, options) {
|
|
554
|
+
return this.upset(DefaultField.typeName, name, value, options);
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
}
|
|
403
558
|
|
|
404
|
-
// src/
|
|
405
|
-
var
|
|
559
|
+
// src/default-fields.ts
|
|
560
|
+
var DefaultFields = class extends WithBooleanFields(WithStringFields(WithNumericFields(WithDefaultFields(Fields)))) {
|
|
406
561
|
};
|
|
407
562
|
|
|
408
563
|
// src/field-tree.ts
|
|
409
|
-
var
|
|
410
|
-
var import_utils4 = require("@axi-engine/utils");
|
|
411
|
-
var import_events2 = require("@axi-engine/events");
|
|
564
|
+
var import_utils5 = require("@axi-engine/utils");
|
|
412
565
|
var FieldTree = class _FieldTree {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
566
|
+
static typeName = "fieldTree";
|
|
567
|
+
typeName = _FieldTree.typeName;
|
|
568
|
+
/** @private The internal map storing child nodes (branches or leaves). */
|
|
569
|
+
_nodes = /* @__PURE__ */ new Map();
|
|
570
|
+
/** @private The factory used to create new child nodes. */
|
|
571
|
+
_factory;
|
|
572
|
+
/**
|
|
573
|
+
* An event emitter that fires immediately after a new node is added to this tree branch.
|
|
574
|
+
* @event
|
|
575
|
+
* @param {object} event - The event payload.
|
|
576
|
+
* @param {string} event.name - The name (key) of the added node.
|
|
577
|
+
* @param event.node - The node instance that was added.
|
|
578
|
+
* @example
|
|
579
|
+
* myTree.onAdd.subscribe(({ name, node }) => {
|
|
580
|
+
* console.log(`Node '${name}' was added.`, node);
|
|
581
|
+
* });
|
|
582
|
+
*/
|
|
583
|
+
onAdd = new import_utils5.Emitter();
|
|
584
|
+
/**
|
|
585
|
+
* An event emitter that fires once after one or more nodes have been successfully removed.
|
|
586
|
+
* @event
|
|
587
|
+
* @param {object} event - The event payload.
|
|
588
|
+
* @param {string[]} event.names - An array of names of the nodes that were removed.
|
|
589
|
+
* @example
|
|
590
|
+
* myTree.onRemove.subscribe(({ names }) => {
|
|
591
|
+
* console.log(`Nodes removed: ${names.join(', ')}`);
|
|
592
|
+
* });
|
|
593
|
+
*/
|
|
594
|
+
onRemove = new import_utils5.Emitter();
|
|
595
|
+
/**
|
|
596
|
+
* Gets the collection of direct child nodes of this tree branch.
|
|
597
|
+
*/
|
|
598
|
+
get nodes() {
|
|
599
|
+
return this._nodes;
|
|
416
600
|
}
|
|
417
601
|
/**
|
|
418
|
-
*
|
|
419
|
-
*
|
|
602
|
+
* Creates an instance of FieldTree.
|
|
603
|
+
* @param {TreeNodeFactory} factory - A factory responsible for creating new nodes within the tree.
|
|
420
604
|
*/
|
|
421
|
-
|
|
422
|
-
|
|
605
|
+
constructor(factory) {
|
|
606
|
+
this._factory = factory;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Checks if a direct child node with the given name exists.
|
|
610
|
+
* @param {string} name - The name of the direct child node.
|
|
611
|
+
* @returns {boolean} `true` if the node exists, otherwise `false`.
|
|
612
|
+
*/
|
|
613
|
+
has(name) {
|
|
614
|
+
return this._nodes.has(name);
|
|
423
615
|
}
|
|
424
616
|
/**
|
|
425
|
-
* Checks if a
|
|
426
|
-
* @
|
|
617
|
+
* Checks if a node exists at a given path, traversing the tree.
|
|
618
|
+
* @param {PathType} path - The path to check (e.g., 'player/stats' or ['player', 'stats']).
|
|
619
|
+
* @returns {boolean} `true` if the entire path resolves to a node, otherwise `false`.
|
|
427
620
|
*/
|
|
428
621
|
hasPath(path) {
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
for (let i = 0; i < pathParts.length; i++) {
|
|
432
|
-
const part = pathParts[i];
|
|
433
|
-
const nextNode = currentNode._items.value.get(part);
|
|
434
|
-
if (!nextNode) {
|
|
435
|
-
return false;
|
|
436
|
-
}
|
|
437
|
-
if (nextNode instanceof BaseFields) {
|
|
438
|
-
if (i === pathParts.length - 1) {
|
|
439
|
-
return true;
|
|
440
|
-
}
|
|
441
|
-
(0, import_utils4.throwIf)(
|
|
442
|
-
pathParts.length - i > 2,
|
|
443
|
-
`Path validation failed, full path: ${(0, import_utils4.ensurePathString)(path)}, has extra nodes after Fields placed at: ${(0, import_utils4.ensurePathString)(pathParts.slice(0, i + 1))}`
|
|
444
|
-
);
|
|
445
|
-
return nextNode.has(pathParts[i + 1]);
|
|
446
|
-
}
|
|
447
|
-
currentNode = nextNode;
|
|
448
|
-
}
|
|
449
|
-
return true;
|
|
622
|
+
const traversedPath = this.traversePath(path);
|
|
623
|
+
return traversedPath.branch.has(traversedPath.leafName);
|
|
450
624
|
}
|
|
451
625
|
/**
|
|
452
|
-
*
|
|
453
|
-
* @param name The name
|
|
454
|
-
* @
|
|
455
|
-
* @
|
|
626
|
+
* Adds a pre-existing node as a direct child of this tree branch.
|
|
627
|
+
* @param {string} name - The name to assign to the new child node.
|
|
628
|
+
* @param {TreeNode} node - The node instance to add.
|
|
629
|
+
* @returns {TreeNode} The added node.
|
|
630
|
+
* @throws If a node with the same name already exists.
|
|
456
631
|
*/
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
(
|
|
632
|
+
addNode(name, node) {
|
|
633
|
+
(0, import_utils5.throwIf)(this.has(name), `Can't add node with name: '${name}', node already exists`);
|
|
634
|
+
this._nodes.set(name, node);
|
|
635
|
+
this.onAdd.emit({ name, node });
|
|
460
636
|
return node;
|
|
461
637
|
}
|
|
462
638
|
/**
|
|
463
|
-
* Retrieves a child node
|
|
464
|
-
* @param name The name of the child node.
|
|
465
|
-
* @returns The
|
|
466
|
-
* @throws If
|
|
639
|
+
* Retrieves a direct child node by its name.
|
|
640
|
+
* @param {string} name - The name of the child node.
|
|
641
|
+
* @returns {TreeNode} The retrieved node.
|
|
642
|
+
* @throws If a node with the given name cannot be found.
|
|
467
643
|
*/
|
|
468
|
-
|
|
469
|
-
const node = this.
|
|
470
|
-
(0,
|
|
644
|
+
getNode(name) {
|
|
645
|
+
const node = this._nodes.get(name);
|
|
646
|
+
(0, import_utils5.throwIfEmpty)(node, `Can't find node with name '${name}'`);
|
|
471
647
|
return node;
|
|
472
648
|
}
|
|
473
649
|
/**
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
650
|
+
* Removes one or more nodes from this tree branch.
|
|
651
|
+
*
|
|
652
|
+
* This method first validates that all specified nodes exist. If validation passes,
|
|
653
|
+
* it recursively calls `destroy()` on each node to ensure proper cleanup of the entire subtree.
|
|
654
|
+
* Finally, it emits a single `onRemove` event with the names of all successfully removed nodes.
|
|
655
|
+
*
|
|
656
|
+
* @param {string | string[]} names - A single name or an array of names of the nodes to remove.
|
|
657
|
+
* @throws If any of the specified names do not correspond to an existing node.
|
|
658
|
+
*/
|
|
659
|
+
removeNode(names) {
|
|
660
|
+
const toRemoveNames = Array.isArray(names) ? names : [names];
|
|
661
|
+
toRemoveNames.forEach((name) => {
|
|
662
|
+
(0, import_utils5.throwIf)(!this.has(name), `Can't remove node with name: '${name}', node doesn't exists`);
|
|
663
|
+
});
|
|
664
|
+
toRemoveNames.forEach((name) => {
|
|
665
|
+
this._nodes.get(name).destroy();
|
|
666
|
+
this._nodes.delete(name);
|
|
667
|
+
});
|
|
668
|
+
if (toRemoveNames.length) {
|
|
669
|
+
this.onRemove.emit({ names: toRemoveNames });
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Creates a new `FieldTree` (branch) node at the specified path.
|
|
674
|
+
* @param {PathType} path - The path where the new `FieldTree` should be created.
|
|
675
|
+
* @param {boolean} [createPath=false] - If `true`, any missing parent branches in the path will be created automatically.
|
|
676
|
+
* @returns {FieldTree} The newly created `FieldTree` instance.
|
|
677
|
+
* @throws If the path is invalid or a node already exists at the target location.
|
|
678
|
+
*/
|
|
679
|
+
createFieldTree(path, createPath) {
|
|
680
|
+
const traversedPath = this.traversePath(path, createPath);
|
|
681
|
+
return traversedPath.branch.addNode(traversedPath.leafName, this._factory.tree());
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Creates a new `Fields` (leaf) container at the specified path.
|
|
685
|
+
* @param {PathType} path - The path where the new `Fields` container should be created.
|
|
686
|
+
* @param {boolean} [createPath=false] - If `true`, any missing parent branches in the path will be created automatically.
|
|
687
|
+
* @returns {Fields} The newly created `Fields` instance.
|
|
688
|
+
* @throws If the path is invalid or a node already exists at the target location.
|
|
689
|
+
*/
|
|
690
|
+
createFields(path, createPath) {
|
|
691
|
+
const traversedPath = this.traversePath(path, createPath);
|
|
692
|
+
return traversedPath.branch.addNode(traversedPath.leafName, this._factory.fields());
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Retrieves a `FieldTree` (branch) node from a specified path.
|
|
696
|
+
* @param {PathType} path - The path to the `FieldTree` node.
|
|
697
|
+
* @returns {FieldTree} The `FieldTree` instance at the specified path.
|
|
698
|
+
* @throws If the path is invalid or the node at the path is not a `FieldTree`.
|
|
478
699
|
*/
|
|
479
|
-
|
|
480
|
-
const
|
|
481
|
-
|
|
700
|
+
getFieldTree(path) {
|
|
701
|
+
const traversedPath = this.traversePath(path);
|
|
702
|
+
const node = traversedPath.branch.getNode(traversedPath.leafName);
|
|
703
|
+
(0, import_utils5.throwIf)(
|
|
704
|
+
!(node instanceof _FieldTree),
|
|
705
|
+
`Node with name: ${traversedPath.leafName} by path: '${(0, import_utils5.ensurePathString)(path)}' should be instance of FieldTree`
|
|
706
|
+
);
|
|
482
707
|
return node;
|
|
483
708
|
}
|
|
484
709
|
/**
|
|
485
|
-
* Retrieves a
|
|
486
|
-
* @param
|
|
487
|
-
* @returns The
|
|
488
|
-
* @throws If
|
|
710
|
+
* Retrieves a `Fields` (leaf) container from a specified path.
|
|
711
|
+
* @param {PathType} path - The path to the `Fields` container.
|
|
712
|
+
* @returns {Fields} The `Fields` instance at the specified path.
|
|
713
|
+
* @throws If the path is invalid or the node at the path is not a `Fields` container.
|
|
489
714
|
*/
|
|
490
|
-
|
|
491
|
-
const
|
|
492
|
-
|
|
715
|
+
getFields(path) {
|
|
716
|
+
const traversedPath = this.traversePath(path);
|
|
717
|
+
const node = traversedPath.branch.getNode(traversedPath.leafName);
|
|
718
|
+
(0, import_utils5.throwIf)(
|
|
719
|
+
!(node instanceof Fields),
|
|
720
|
+
`Node with name: ${traversedPath.leafName} by path: '${(0, import_utils5.ensurePathString)(path)}' should be instance of Fields`
|
|
721
|
+
);
|
|
493
722
|
return node;
|
|
494
723
|
}
|
|
495
724
|
/**
|
|
496
|
-
*
|
|
497
|
-
* @param
|
|
498
|
-
* @returns The newly created `FieldTree` instance.
|
|
725
|
+
* Retrieves a `FieldTree` at the specified path. If it or any part of the path doesn't exist, it will be created.
|
|
726
|
+
* @param {PathType} path - The path to the `FieldTree` node.
|
|
727
|
+
* @returns {FieldTree} The existing or newly created `FieldTree` instance.
|
|
728
|
+
*/
|
|
729
|
+
getOrCreateFieldTree(path) {
|
|
730
|
+
const traversedPath = this.traversePath(path, true);
|
|
731
|
+
return traversedPath.branch.has(traversedPath.leafName) ? traversedPath.branch.getFieldTree(traversedPath.leafName) : traversedPath.branch.createFieldTree(traversedPath.leafName);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Retrieves a `Fields` container at the specified path. If it or any part of the path doesn't exist, it will be created.
|
|
735
|
+
* @param {PathType} path - The path to the `Fields` container.
|
|
736
|
+
* @returns {Fields} The existing or newly created `Fields` instance.
|
|
737
|
+
*/
|
|
738
|
+
getOrCreateFields(path) {
|
|
739
|
+
const traversedPath = this.traversePath(path, true);
|
|
740
|
+
return traversedPath.branch.has(traversedPath.leafName) ? traversedPath.branch.getFields(traversedPath.leafName) : traversedPath.branch.createFields(traversedPath.leafName);
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Removes all child nodes from this tree branch.
|
|
744
|
+
* This method ensures that `destroy()` is called on each child node, allowing for
|
|
745
|
+
* a full, recursive cleanup of the entire subtree.
|
|
746
|
+
*/
|
|
747
|
+
clear() {
|
|
748
|
+
this.removeNode(Array.from(this._nodes.keys()));
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Performs a complete cleanup of this node and its entire subtree.
|
|
752
|
+
*
|
|
753
|
+
* It recursively destroys all child nodes by calling `clear()` and then
|
|
754
|
+
* unsubscribes all listeners from its own event emitters.
|
|
755
|
+
* This method should be called when a node is no longer needed.
|
|
756
|
+
*/
|
|
757
|
+
destroy() {
|
|
758
|
+
this.clear();
|
|
759
|
+
this.onAdd.clear();
|
|
760
|
+
this.onRemove.clear();
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* @private
|
|
764
|
+
* Navigates the tree to the parent of a target node.
|
|
765
|
+
* This is the core traversal logic for all path-based operations.
|
|
766
|
+
* @param {PathType} path - The full path to the target node.
|
|
767
|
+
* @param {boolean} [createPath=false] - If `true`, creates missing `FieldTree` branches along the path.
|
|
768
|
+
* @returns {{branch: FieldTree, leafName: string}} An object containing the final branch (parent node) and the name of the leaf (target node).
|
|
769
|
+
* @throws If the path is empty, invalid, or contains a `Fields` container as an intermediate segment.
|
|
770
|
+
*/
|
|
771
|
+
traversePath(path, createPath) {
|
|
772
|
+
const pathArr = (0, import_utils5.ensurePathArray)(path);
|
|
773
|
+
(0, import_utils5.throwIfEmpty)(pathArr, "The path is empty");
|
|
774
|
+
const leafName = pathArr.pop();
|
|
775
|
+
let currentNode = this;
|
|
776
|
+
for (const pathPart of pathArr) {
|
|
777
|
+
let node;
|
|
778
|
+
if (currentNode.has(pathPart)) {
|
|
779
|
+
node = currentNode.getNode(pathPart);
|
|
780
|
+
} else {
|
|
781
|
+
if (createPath) {
|
|
782
|
+
node = currentNode.createFieldTree(pathPart);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
(0, import_utils5.throwIfEmpty)(node, `Can't find node with name ${pathPart} by path parsing: ${(0, import_utils5.ensurePathString)(path)}`);
|
|
786
|
+
(0, import_utils5.throwIf)(node instanceof Fields, `Node with name ${pathPart} should be instance of FieldTree`);
|
|
787
|
+
currentNode = node;
|
|
788
|
+
}
|
|
789
|
+
return { branch: currentNode, leafName };
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
// src/field-tree-node-factory.ts
|
|
794
|
+
var DefaultFieldsFactory = class {
|
|
795
|
+
constructor(fieldRegistry) {
|
|
796
|
+
this.fieldRegistry = fieldRegistry;
|
|
797
|
+
}
|
|
798
|
+
fields() {
|
|
799
|
+
return new DefaultFields(this.fieldRegistry);
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
var DefaultTreeNodeFactory = class extends DefaultFieldsFactory {
|
|
803
|
+
constructor(fieldRegistry) {
|
|
804
|
+
super(fieldRegistry);
|
|
805
|
+
}
|
|
806
|
+
tree() {
|
|
807
|
+
return new FieldTree(this);
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
// src/serializer/policies/clamp-policy-serializer-handler.ts
|
|
812
|
+
var ClampPolicySerializerHandler = class {
|
|
813
|
+
snapshot(policy) {
|
|
814
|
+
return { min: policy.min, max: policy.max };
|
|
815
|
+
}
|
|
816
|
+
hydrate(data) {
|
|
817
|
+
return new ClampPolicy(data.min, data.max);
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
// src/serializer/policies/clamp-max-policy-serializer-handler.ts
|
|
822
|
+
var ClampMaxPolicySerializerHandler = class {
|
|
823
|
+
snapshot(policy) {
|
|
824
|
+
return { max: policy.max };
|
|
825
|
+
}
|
|
826
|
+
hydrate(data) {
|
|
827
|
+
return new ClampMaxPolicy(data.max);
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
// src/serializer/policies/clamp-min-policy-serializer-handler.ts
|
|
832
|
+
var ClampMinPolicySerializerHandler = class {
|
|
833
|
+
snapshot(policy) {
|
|
834
|
+
return { min: policy.min };
|
|
835
|
+
}
|
|
836
|
+
hydrate(data) {
|
|
837
|
+
return new ClampMinPolicy(data.min);
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
// src/serializer/policy-serializer.ts
|
|
842
|
+
var import_utils6 = require("@axi-engine/utils");
|
|
843
|
+
var PolicySerializer = class {
|
|
844
|
+
handlers = /* @__PURE__ */ new Map();
|
|
845
|
+
register(policyId, handler) {
|
|
846
|
+
(0, import_utils6.throwIf)(this.handlers.has(policyId), `A handler for policy ID '${policyId}' is already registered.`);
|
|
847
|
+
this.handlers.set(policyId, handler);
|
|
848
|
+
return this;
|
|
849
|
+
}
|
|
850
|
+
clearHandlers() {
|
|
851
|
+
this.handlers.clear();
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Creates a serializable snapshot of a policy instance.
|
|
855
|
+
* The snapshot includes the policy's state and a `__type` identifier.
|
|
856
|
+
* @param policy The policy instance to snapshot.
|
|
857
|
+
* @returns A plain object ready for JSON serialization.
|
|
858
|
+
* @throws If no handler is registered for the policy's ID.
|
|
499
859
|
*/
|
|
500
|
-
|
|
501
|
-
|
|
860
|
+
snapshot(policy) {
|
|
861
|
+
const handler = this.handlers.get(policy.id);
|
|
862
|
+
(0, import_utils6.throwIfEmpty)(handler, `No serializer handler registered for policy ID: '${policy.id}'`);
|
|
863
|
+
const data = handler.snapshot(policy);
|
|
864
|
+
return {
|
|
865
|
+
__type: policy.id,
|
|
866
|
+
...data
|
|
867
|
+
};
|
|
502
868
|
}
|
|
503
869
|
/**
|
|
504
|
-
*
|
|
505
|
-
* @param
|
|
506
|
-
* @returns
|
|
870
|
+
* Restores a policy instance from its snapshot representation.
|
|
871
|
+
* @param snapshot The plain object snapshot, which must contain a `__type` property.
|
|
872
|
+
* @returns A new, fully functional policy instance.
|
|
873
|
+
* @throws If the snapshot is invalid or no handler is registered for its `__type`.
|
|
507
874
|
*/
|
|
508
|
-
|
|
509
|
-
|
|
875
|
+
hydrate(snapshot) {
|
|
876
|
+
const typeId = snapshot?.__type;
|
|
877
|
+
(0, import_utils6.throwIfEmpty)(typeId, 'Invalid policy snapshot: missing "__type" identifier.');
|
|
878
|
+
const handler = this.handlers.get(typeId);
|
|
879
|
+
(0, import_utils6.throwIfEmpty)(handler, `No serializer handler registered for policy ID: '${typeId}'`);
|
|
880
|
+
const { __type, ...data } = snapshot;
|
|
881
|
+
return handler.hydrate(data);
|
|
510
882
|
}
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
// src/serializer/field-serializer.ts
|
|
886
|
+
var import_utils7 = require("@axi-engine/utils");
|
|
887
|
+
var FieldSerializer = class {
|
|
511
888
|
/**
|
|
512
|
-
* Creates
|
|
513
|
-
* @param
|
|
514
|
-
* @
|
|
889
|
+
* Creates an instance of FieldSerializer.
|
|
890
|
+
* @param {FieldRegistry} fieldRegistry - A registry that maps string type names to Field constructors.
|
|
891
|
+
* @param {PolicySerializer} policySerializer - A serializer dedicated to handling Policy instances.
|
|
515
892
|
*/
|
|
516
|
-
|
|
517
|
-
|
|
893
|
+
constructor(fieldRegistry, policySerializer) {
|
|
894
|
+
this.fieldRegistry = fieldRegistry;
|
|
895
|
+
this.policySerializer = policySerializer;
|
|
518
896
|
}
|
|
519
897
|
/**
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
* @
|
|
523
|
-
* @
|
|
898
|
+
* Creates a serializable snapshot of a Field instance.
|
|
899
|
+
* The snapshot includes the field's type, name, current value, and the state of all its policies.
|
|
900
|
+
* @param {Field<any>} field - The Field instance to serialize.
|
|
901
|
+
* @returns {FieldSnapshot} A plain object ready for JSON serialization.
|
|
524
902
|
*/
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
903
|
+
snapshot(field) {
|
|
904
|
+
let snapshot = {
|
|
905
|
+
__type: field.typeName,
|
|
906
|
+
name: field.name,
|
|
907
|
+
value: field.value
|
|
908
|
+
};
|
|
909
|
+
if (!field.policies.isEmpty()) {
|
|
910
|
+
const serializedPolicies = [];
|
|
911
|
+
field.policies.items.forEach((policy) => serializedPolicies.push(this.policySerializer.snapshot(policy)));
|
|
912
|
+
snapshot.policies = serializedPolicies;
|
|
531
913
|
}
|
|
532
|
-
return
|
|
914
|
+
return snapshot;
|
|
533
915
|
}
|
|
534
916
|
/**
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
* @param
|
|
539
|
-
* @returns
|
|
917
|
+
* Restores a Field instance from its snapshot representation.
|
|
918
|
+
* It uses the `__type` property to find the correct constructor and hydrates
|
|
919
|
+
* the field with its value and all its policies.
|
|
920
|
+
* @param {FieldSnapshot} snapshot - The plain object snapshot to deserialize.
|
|
921
|
+
* @returns {Field<any>} A new, fully functional Field instance.
|
|
922
|
+
* @throws If the snapshot is invalid, missing a `__type`, or if the type is not registered.
|
|
540
923
|
*/
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
924
|
+
hydrate(snapshot) {
|
|
925
|
+
const fieldType = snapshot.__type;
|
|
926
|
+
(0, import_utils7.throwIfEmpty)(fieldType, 'Invalid field snapshot: missing "__type" identifier.');
|
|
927
|
+
const Ctor = this.fieldRegistry.get(fieldType);
|
|
928
|
+
let policies;
|
|
929
|
+
if (!(0, import_utils7.isNullOrUndefined)(snapshot.policies)) {
|
|
930
|
+
policies = [];
|
|
931
|
+
snapshot.policies.forEach((p) => policies.push(this.policySerializer.hydrate(p)));
|
|
932
|
+
}
|
|
933
|
+
return new Ctor(snapshot.name, snapshot.value, { policies });
|
|
546
934
|
}
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
// src/serializer/fields-serializer.ts
|
|
938
|
+
var FieldsSerializer = class {
|
|
547
939
|
/**
|
|
548
|
-
* Creates
|
|
549
|
-
* @param
|
|
550
|
-
* @param
|
|
551
|
-
* @returns The newly created `NumberField` instance.
|
|
940
|
+
* Creates an instance of FieldsSerializer.
|
|
941
|
+
* @param {FieldsFactory} fieldsFactory - A registry that maps string type names to Field constructors.
|
|
942
|
+
* @param {FieldSerializer} fieldSerializer - A serializer of field instances.
|
|
552
943
|
*/
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
return this.getFieldsByPath(fullPath).createNumber(fieldName, initialValue);
|
|
944
|
+
constructor(fieldsFactory, fieldSerializer) {
|
|
945
|
+
this.fieldsFactory = fieldsFactory;
|
|
946
|
+
this.fieldSerializer = fieldSerializer;
|
|
557
947
|
}
|
|
558
948
|
/**
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
949
|
+
* Creates a serializable snapshot of a `Fields` container.
|
|
950
|
+
*
|
|
951
|
+
* The snapshot includes a `__type` identifier (currently hardcoded) and an array of snapshots
|
|
952
|
+
* for each `Field` within the container.
|
|
953
|
+
* @param {Fields} fields - The `Fields` instance to serialize.
|
|
954
|
+
* @returns {FieldsSnapshot} A plain object ready for JSON serialization.
|
|
562
955
|
*/
|
|
563
|
-
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
956
|
+
snapshot(fields) {
|
|
957
|
+
const res = {
|
|
958
|
+
__type: fields.typeName
|
|
959
|
+
};
|
|
960
|
+
fields.fields.forEach((field) => res[field.name] = this.fieldSerializer.snapshot(field));
|
|
961
|
+
return res;
|
|
567
962
|
}
|
|
568
963
|
/**
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
964
|
+
* Restores a `Fields` container instance from its snapshot representation.
|
|
965
|
+
*
|
|
966
|
+
* It iterates through the field snapshots and hydrates them individually, adding them to the new container.
|
|
967
|
+
* @param {FieldsSnapshot} snapshot - The plain object snapshot to deserialize.
|
|
968
|
+
* @returns {Fields} A new `DefaultFields` instance populated with the restored fields.
|
|
572
969
|
*/
|
|
573
|
-
|
|
574
|
-
const
|
|
575
|
-
const
|
|
576
|
-
|
|
970
|
+
hydrate(snapshot) {
|
|
971
|
+
const { __type, ...fieldsData } = snapshot;
|
|
972
|
+
const fields = this.fieldsFactory.fields();
|
|
973
|
+
for (const fieldName in fieldsData) {
|
|
974
|
+
const fieldSnapshot = fieldsData[fieldName];
|
|
975
|
+
const restoredField = this.fieldSerializer.hydrate(fieldSnapshot);
|
|
976
|
+
fields.add(restoredField);
|
|
977
|
+
}
|
|
978
|
+
return fields;
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
// src/serializer/field-tree-serializer.ts
|
|
983
|
+
var import_utils8 = require("@axi-engine/utils");
|
|
984
|
+
var FieldTreeSerializer = class {
|
|
985
|
+
constructor(fieldTreeNodeFactory, fieldsSerializer) {
|
|
986
|
+
this.fieldTreeNodeFactory = fieldTreeNodeFactory;
|
|
987
|
+
this.fieldsSerializer = fieldsSerializer;
|
|
577
988
|
}
|
|
578
989
|
/**
|
|
579
990
|
* Creates a serializable snapshot of the entire tree and its contained fields.
|
|
580
991
|
* @returns A plain JavaScript object representing the complete state managed by this tree.
|
|
581
992
|
*/
|
|
582
|
-
snapshot() {
|
|
583
|
-
const
|
|
584
|
-
__type:
|
|
993
|
+
snapshot(tree) {
|
|
994
|
+
const res = {
|
|
995
|
+
__type: tree.typeName
|
|
585
996
|
};
|
|
586
|
-
|
|
587
|
-
|
|
997
|
+
tree.nodes.forEach((node, key) => {
|
|
998
|
+
if (node.typeName === tree.typeName) {
|
|
999
|
+
res[key] = this.snapshot(node);
|
|
1000
|
+
} else if (node.typeName === Fields.typeName) {
|
|
1001
|
+
res[key] = this.fieldsSerializer.snapshot(node);
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
return res;
|
|
588
1005
|
}
|
|
589
1006
|
/**
|
|
590
1007
|
* Restores the state of the tree from a snapshot.
|
|
@@ -592,61 +1009,45 @@ var FieldTree = class _FieldTree {
|
|
|
592
1009
|
* @param snapshot The snapshot object to load.
|
|
593
1010
|
*/
|
|
594
1011
|
hydrate(snapshot) {
|
|
595
|
-
|
|
596
|
-
|
|
1012
|
+
const { __type, ...nodes } = snapshot;
|
|
1013
|
+
const tree = this.fieldTreeNodeFactory.tree();
|
|
1014
|
+
for (const key in nodes) {
|
|
1015
|
+
const nodeData = nodes[key];
|
|
1016
|
+
if ((0, import_utils8.isString)(nodeData)) {
|
|
597
1017
|
continue;
|
|
598
1018
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if (type === "Fields" /* fields */) {
|
|
604
|
-
node = this.createFields(key);
|
|
605
|
-
} else if (type === "FieldTree" /* fieldTree */) {
|
|
606
|
-
node = this.createFieldTree(key);
|
|
607
|
-
} else {
|
|
608
|
-
console.warn(`Node '${key}' in snapshot has no __type metadata. Skipping.`);
|
|
609
|
-
}
|
|
1019
|
+
if (nodeData.__type === FieldTree.typeName) {
|
|
1020
|
+
tree.addNode(key, this.hydrate(nodeData));
|
|
1021
|
+
} else if (nodeData.__type === Fields.typeName) {
|
|
1022
|
+
tree.addNode(key, this.fieldsSerializer.hydrate(nodeData));
|
|
610
1023
|
}
|
|
611
|
-
node?.hydrate(field);
|
|
612
1024
|
}
|
|
613
|
-
|
|
614
|
-
/**
|
|
615
|
-
* @private
|
|
616
|
-
* Generic internal method for creating and adding a new node to the tree.
|
|
617
|
-
* @param name The name of the node to create.
|
|
618
|
-
* @param ctor The constructor for the node type (e.g., `FieldTree` or `Fields`).
|
|
619
|
-
* @returns The newly created node instance.
|
|
620
|
-
*/
|
|
621
|
-
createNode(name, ctor) {
|
|
622
|
-
const currentItems = this._items.value;
|
|
623
|
-
(0, import_utils4.throwIf)(currentItems.has(name), `Can't create node with name: '${name}', node already exists`);
|
|
624
|
-
const res = new ctor();
|
|
625
|
-
const newItems = new Map(currentItems);
|
|
626
|
-
newItems.set(name, res);
|
|
627
|
-
this._items.value = newItems;
|
|
628
|
-
this.events.emit("created", {
|
|
629
|
-
type: "created",
|
|
630
|
-
name,
|
|
631
|
-
path: [],
|
|
632
|
-
// todo: need to decide how to pass full path
|
|
633
|
-
node: res
|
|
634
|
-
});
|
|
635
|
-
return res;
|
|
1025
|
+
return tree;
|
|
636
1026
|
}
|
|
637
1027
|
};
|
|
638
1028
|
// Annotate the CommonJS export names for ESM import in node:
|
|
639
1029
|
0 && (module.exports = {
|
|
640
|
-
BaseFields,
|
|
641
1030
|
ClampMaxPolicy,
|
|
1031
|
+
ClampMaxPolicySerializerHandler,
|
|
642
1032
|
ClampMinPolicy,
|
|
1033
|
+
ClampMinPolicySerializerHandler,
|
|
643
1034
|
ClampPolicy,
|
|
644
|
-
|
|
1035
|
+
ClampPolicySerializerHandler,
|
|
1036
|
+
DefaultBooleanField,
|
|
1037
|
+
DefaultField,
|
|
1038
|
+
DefaultFields,
|
|
1039
|
+
DefaultFieldsFactory,
|
|
1040
|
+
DefaultNumericField,
|
|
1041
|
+
DefaultStringField,
|
|
1042
|
+
DefaultTreeNodeFactory,
|
|
1043
|
+
FieldRegistry,
|
|
1044
|
+
FieldSerializer,
|
|
645
1045
|
FieldTree,
|
|
1046
|
+
FieldTreeSerializer,
|
|
646
1047
|
Fields,
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
1048
|
+
FieldsSerializer,
|
|
1049
|
+
Policies,
|
|
1050
|
+
PolicySerializer,
|
|
650
1051
|
clampMaxPolicy,
|
|
651
1052
|
clampMinPolicy,
|
|
652
1053
|
clampPolicy
|