@milaboratories/pl-tree 1.7.4 → 1.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/accessors.cjs +297 -0
  2. package/dist/accessors.cjs.map +1 -0
  3. package/dist/accessors.d.ts +0 -1
  4. package/dist/accessors.js +288 -0
  5. package/dist/accessors.js.map +1 -0
  6. package/dist/dump.cjs +86 -0
  7. package/dist/dump.cjs.map +1 -0
  8. package/dist/dump.d.ts +0 -1
  9. package/dist/dump.js +84 -0
  10. package/dist/dump.js.map +1 -0
  11. package/dist/index.cjs +36 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.d.ts +0 -1
  14. package/dist/index.js +7 -10
  15. package/dist/index.js.map +1 -1
  16. package/dist/snapshot.cjs +88 -0
  17. package/dist/snapshot.cjs.map +1 -0
  18. package/dist/snapshot.d.ts +0 -1
  19. package/dist/snapshot.js +83 -0
  20. package/dist/snapshot.js.map +1 -0
  21. package/dist/state.cjs +631 -0
  22. package/dist/state.cjs.map +1 -0
  23. package/dist/state.d.ts +0 -1
  24. package/dist/state.js +627 -0
  25. package/dist/state.js.map +1 -0
  26. package/dist/sync.cjs +156 -0
  27. package/dist/sync.cjs.map +1 -0
  28. package/dist/sync.d.ts +0 -1
  29. package/dist/sync.js +151 -0
  30. package/dist/sync.js.map +1 -0
  31. package/dist/synchronized_tree.cjs +235 -0
  32. package/dist/synchronized_tree.cjs.map +1 -0
  33. package/dist/synchronized_tree.d.ts +0 -1
  34. package/dist/synchronized_tree.js +214 -0
  35. package/dist/synchronized_tree.js.map +1 -0
  36. package/dist/test_utils.d.ts +0 -1
  37. package/dist/traversal_ops.d.ts +0 -1
  38. package/dist/value_and_error.cjs +20 -0
  39. package/dist/value_and_error.cjs.map +1 -0
  40. package/dist/value_and_error.d.ts +0 -1
  41. package/dist/value_and_error.js +17 -0
  42. package/dist/value_and_error.js.map +1 -0
  43. package/dist/value_or_error.d.ts +0 -1
  44. package/package.json +16 -14
  45. package/src/snapshot.test.ts +4 -3
  46. package/src/state.test.ts +6 -4
  47. package/dist/accessors.d.ts.map +0 -1
  48. package/dist/dump.d.ts.map +0 -1
  49. package/dist/index.d.ts.map +0 -1
  50. package/dist/index.mjs +0 -904
  51. package/dist/index.mjs.map +0 -1
  52. package/dist/snapshot.d.ts.map +0 -1
  53. package/dist/state.d.ts.map +0 -1
  54. package/dist/sync.d.ts.map +0 -1
  55. package/dist/synchronized_tree.d.ts.map +0 -1
  56. package/dist/test_utils.d.ts.map +0 -1
  57. package/dist/traversal_ops.d.ts.map +0 -1
  58. package/dist/value_and_error.d.ts.map +0 -1
  59. package/dist/value_or_error.d.ts.map +0 -1
package/dist/state.cjs ADDED
@@ -0,0 +1,631 @@
1
+ 'use strict';
2
+
3
+ var plClient = require('@milaboratories/pl-client');
4
+ var computable = require('@milaboratories/computable');
5
+ var accessors = require('./accessors.cjs');
6
+ var tsHelpers = require('@milaboratories/ts-helpers');
7
+
8
+ class TreeStateUpdateError extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ }
12
+ }
13
+ class PlTreeField {
14
+ name;
15
+ type;
16
+ value;
17
+ error;
18
+ status;
19
+ valueIsFinal;
20
+ resourceVersion;
21
+ change = new computable.ChangeSource();
22
+ constructor(name, type, value, error, status, valueIsFinal,
23
+ /** Last version of resource this field was observed, used to garbage collect fields in tree patching procedure */
24
+ resourceVersion) {
25
+ this.name = name;
26
+ this.type = type;
27
+ this.value = value;
28
+ this.error = error;
29
+ this.status = status;
30
+ this.valueIsFinal = valueIsFinal;
31
+ this.resourceVersion = resourceVersion;
32
+ }
33
+ get state() {
34
+ return {
35
+ name: this.name,
36
+ type: this.type,
37
+ status: this.status,
38
+ value: this.value,
39
+ error: this.error,
40
+ valueIsFinal: this.valueIsFinal,
41
+ };
42
+ }
43
+ }
44
+ const InitialResourceVersion = 0;
45
+ /** Never store instances of this class, always get fresh instance from {@link PlTreeState} */
46
+ class PlTreeResource {
47
+ /** Tracks number of other resources referencing this resource. Used to perform garbage collection in tree patching procedure */
48
+ refCount = 0;
49
+ /** Increments each time resource is checked for difference with new state */
50
+ version = InitialResourceVersion;
51
+ /** Set to resource version when resource state, or it's fields have changed */
52
+ dataVersion = InitialResourceVersion;
53
+ fieldsMap = new Map();
54
+ kv = new Map();
55
+ resourceRemoved = new computable.ChangeSource();
56
+ // following change source are removed when resource is marked as final
57
+ finalChanged = new computable.ChangeSource();
58
+ resourceStateChange = new computable.ChangeSource();
59
+ lockedChange = new computable.ChangeSource();
60
+ inputAndServiceFieldListChanged = new computable.ChangeSource();
61
+ outputFieldListChanged = new computable.ChangeSource();
62
+ dynamicFieldListChanged = new computable.ChangeSource();
63
+ // kvChangedGlobal? = new ChangeSource();
64
+ kvChangedPerKey = new computable.KeyedChangeSource();
65
+ id;
66
+ originalResourceId;
67
+ kind;
68
+ type;
69
+ data;
70
+ dataAsString;
71
+ dataAsJson;
72
+ error;
73
+ inputsLocked;
74
+ outputsLocked;
75
+ resourceReady;
76
+ finalFlag;
77
+ /** Set externally by the tree, using {@link FinalResourceDataPredicate} */
78
+ _finalState = false;
79
+ logger;
80
+ constructor(initialState, logger) {
81
+ this.id = initialState.id;
82
+ this.originalResourceId = initialState.originalResourceId;
83
+ this.kind = initialState.kind;
84
+ this.type = initialState.type;
85
+ this.data = initialState.data;
86
+ this.error = initialState.error;
87
+ this.inputsLocked = initialState.inputsLocked;
88
+ this.outputsLocked = initialState.outputsLocked;
89
+ this.resourceReady = initialState.resourceReady;
90
+ this.finalFlag = initialState.final;
91
+ this.logger = logger;
92
+ }
93
+ // TODO add logging
94
+ info(msg) {
95
+ if (this.logger !== undefined)
96
+ this.logger.info(msg);
97
+ }
98
+ warn(msg) {
99
+ if (this.logger !== undefined)
100
+ this.logger.warn(msg);
101
+ }
102
+ get final() {
103
+ return this.finalFlag;
104
+ }
105
+ get finalState() {
106
+ return this._finalState;
107
+ }
108
+ get fields() {
109
+ return [...this.fieldsMap.values()];
110
+ }
111
+ getField(watcher, _step, onUnstable = () => { }) {
112
+ const step = typeof _step === 'string' ? { field: _step } : _step;
113
+ const field = this.fieldsMap.get(step.field);
114
+ if (field === undefined) {
115
+ if (step.errorIfFieldNotFound || step.errorIfFieldNotSet)
116
+ throw new Error(`Field "${step.field}" not found in resource ${plClient.resourceIdToString(this.id)}`);
117
+ if (!this.inputsLocked)
118
+ this.inputAndServiceFieldListChanged?.attachWatcher(watcher);
119
+ else if (step.assertFieldType === 'Service' || step.assertFieldType === 'Input') {
120
+ if (step.allowPermanentAbsence)
121
+ // stable absence of field
122
+ return undefined;
123
+ else
124
+ throw new Error(`Service or input field not found ${step.field}.`);
125
+ }
126
+ if (!this.outputsLocked)
127
+ this.outputFieldListChanged?.attachWatcher(watcher);
128
+ else if (step.assertFieldType === 'Output') {
129
+ if (step.allowPermanentAbsence)
130
+ // stable absence of field
131
+ return undefined;
132
+ else
133
+ throw new Error(`Output field not found ${step.field}.`);
134
+ }
135
+ this.dynamicFieldListChanged?.attachWatcher(watcher);
136
+ if (!this._finalState && !step.stableIfNotFound)
137
+ onUnstable('field_not_found:' + step.field);
138
+ return undefined;
139
+ }
140
+ else {
141
+ if (step.assertFieldType !== undefined && field.type !== step.assertFieldType)
142
+ throw new Error(`Unexpected field type: expected ${step.assertFieldType} but got ${field.type} for the field name ${step.field}`);
143
+ const ret = {};
144
+ if (plClient.isNotNullResourceId(field.value))
145
+ ret.value = field.value;
146
+ if (plClient.isNotNullResourceId(field.error))
147
+ ret.error = field.error;
148
+ if (ret.value === undefined && ret.error === undefined)
149
+ // this method returns value and error of the field, thus those values are considered to be accessed;
150
+ // any existing but not resolved field here is considered to be unstable, in the sense it is
151
+ // considered to acquire some resolved value eventually
152
+ onUnstable('field_not_resolved:' + step.field);
153
+ field.change.attachWatcher(watcher);
154
+ return ret;
155
+ }
156
+ }
157
+ getInputsLocked(watcher) {
158
+ if (!this.inputsLocked)
159
+ // reverse transition can't happen, so there is no reason to wait for value to change
160
+ this.resourceStateChange?.attachWatcher(watcher);
161
+ return this.inputsLocked;
162
+ }
163
+ getOutputsLocked(watcher) {
164
+ if (!this.outputsLocked)
165
+ // reverse transition can't happen, so there is no reason to wait for value to change
166
+ this.resourceStateChange?.attachWatcher(watcher);
167
+ return this.outputsLocked;
168
+ }
169
+ get isReadyOrError() {
170
+ return (this.error !== plClient.NullResourceId
171
+ || this.resourceReady
172
+ || this.originalResourceId !== plClient.NullResourceId);
173
+ }
174
+ getIsFinal(watcher) {
175
+ this.finalChanged?.attachWatcher(watcher);
176
+ return this._finalState;
177
+ }
178
+ getIsReadyOrError(watcher) {
179
+ if (!this.isReadyOrError)
180
+ // reverse transition can't happen, so there is no reason to wait for value to change if it is already true
181
+ this.resourceStateChange?.attachWatcher(watcher);
182
+ return this.isReadyOrError;
183
+ }
184
+ getError(watcher) {
185
+ if (plClient.isNullResourceId(this.error)) {
186
+ this.resourceStateChange?.attachWatcher(watcher);
187
+ return undefined;
188
+ }
189
+ else {
190
+ // reverse transition can't happen, so there is no reason to wait for value to change, if error already set
191
+ return this.error;
192
+ }
193
+ }
194
+ listInputFields(watcher) {
195
+ const ret = [];
196
+ this.fieldsMap.forEach((field, name) => {
197
+ if (field.type === 'Input' || field.type === 'Service')
198
+ ret.push(name);
199
+ });
200
+ if (!this.inputsLocked)
201
+ this.inputAndServiceFieldListChanged?.attachWatcher(watcher);
202
+ return ret;
203
+ }
204
+ listOutputFields(watcher) {
205
+ const ret = [];
206
+ this.fieldsMap.forEach((field, name) => {
207
+ if (field.type === 'Output')
208
+ ret.push(name);
209
+ });
210
+ if (!this.outputsLocked)
211
+ this.outputFieldListChanged?.attachWatcher(watcher);
212
+ return ret;
213
+ }
214
+ listDynamicFields(watcher) {
215
+ const ret = [];
216
+ this.fieldsMap.forEach((field, name) => {
217
+ if (field.type !== 'Input' && field.type !== 'Output')
218
+ ret.push(name);
219
+ });
220
+ this.dynamicFieldListChanged?.attachWatcher(watcher);
221
+ return ret;
222
+ }
223
+ getKeyValue(watcher, key) {
224
+ this.kvChangedPerKey?.attachWatcher(key, watcher);
225
+ return this.kv.get(key);
226
+ }
227
+ getKeyValueString(watcher, key) {
228
+ const bytes = this.getKeyValue(watcher, key);
229
+ if (bytes === undefined)
230
+ return undefined;
231
+ return tsHelpers.cachedDecode(bytes);
232
+ }
233
+ getKeyValueAsJson(watcher, key) {
234
+ const bytes = this.getKeyValue(watcher, key);
235
+ if (bytes === undefined)
236
+ return undefined;
237
+ return tsHelpers.cachedDeserialize(bytes);
238
+ }
239
+ getDataAsString() {
240
+ if (this.data === undefined)
241
+ return undefined;
242
+ if (this.dataAsString === undefined)
243
+ this.dataAsString = tsHelpers.cachedDecode(this.data);
244
+ return this.dataAsString;
245
+ }
246
+ getDataAsJson() {
247
+ if (this.data === undefined)
248
+ return undefined;
249
+ if (this.dataAsJson === undefined)
250
+ this.dataAsJson = tsHelpers.cachedDeserialize(this.data);
251
+ return this.dataAsJson;
252
+ }
253
+ verifyReadyState() {
254
+ if (this.resourceReady && !this.inputsLocked)
255
+ throw new Error(`ready without input or output lock: ${plClient.stringifyWithResourceId(this.basicState)}`);
256
+ }
257
+ get basicState() {
258
+ return {
259
+ id: this.id,
260
+ kind: this.kind,
261
+ type: this.type,
262
+ data: this.data,
263
+ resourceReady: this.resourceReady,
264
+ inputsLocked: this.inputsLocked,
265
+ outputsLocked: this.outputsLocked,
266
+ error: this.error,
267
+ originalResourceId: this.originalResourceId,
268
+ final: this.finalFlag,
269
+ };
270
+ }
271
+ get extendedState() {
272
+ return {
273
+ ...this.basicState,
274
+ fields: this.fields,
275
+ kv: Array.from(this.kv.entries()).map(([key, value]) => ({ key, value })),
276
+ };
277
+ }
278
+ /** Called when {@link FinalResourceDataPredicate} returns true for the state. */
279
+ markFinal() {
280
+ if (this._finalState)
281
+ return;
282
+ this._finalState = true;
283
+ tsHelpers.notEmpty(this.finalChanged).markChanged('marked final');
284
+ this.finalChanged = undefined;
285
+ this.resourceStateChange = undefined;
286
+ this.dynamicFieldListChanged = undefined;
287
+ this.inputAndServiceFieldListChanged = undefined;
288
+ this.outputFieldListChanged = undefined;
289
+ this.lockedChange = undefined;
290
+ // this.kvChangedGlobal = undefined;
291
+ this.kvChangedPerKey = undefined;
292
+ }
293
+ /** Used for invalidation */
294
+ markAllChanged() {
295
+ this.fieldsMap.forEach((field) => field.change.markChanged('marked all changed'));
296
+ this.finalChanged?.markChanged('marked all changed');
297
+ this.resourceStateChange?.markChanged('marked all changed');
298
+ this.lockedChange?.markChanged('marked all changed');
299
+ this.inputAndServiceFieldListChanged?.markChanged('marked all changed');
300
+ this.outputFieldListChanged?.markChanged('marked all changed');
301
+ this.dynamicFieldListChanged?.markChanged('marked all changed');
302
+ // this.kvChangedGlobal?.markChanged('marked all changed');
303
+ this.kvChangedPerKey?.markAllChanged('marked all changed');
304
+ this.resourceRemoved.markChanged('marked all changed');
305
+ }
306
+ }
307
+ class PlTreeState {
308
+ root;
309
+ isFinalPredicate;
310
+ /** resource heap */
311
+ resources = new Map();
312
+ resourcesAdded = new computable.ChangeSource();
313
+ /** Resets to false if any invalid state transitions are registered,
314
+ * after that tree will produce errors for any read or write operations */
315
+ _isValid = true;
316
+ invalidationMessage;
317
+ constructor(
318
+ /** This will be the only resource not deleted during GC round */
319
+ root, isFinalPredicate) {
320
+ this.root = root;
321
+ this.isFinalPredicate = isFinalPredicate;
322
+ }
323
+ forEachResource(cb) {
324
+ this.resources.forEach((v) => cb(v));
325
+ }
326
+ checkValid() {
327
+ if (!this._isValid)
328
+ throw new Error(this.invalidationMessage ?? 'tree is in invalid state');
329
+ }
330
+ get(watcher, rid) {
331
+ this.checkValid();
332
+ const res = this.resources.get(rid);
333
+ if (res === undefined) {
334
+ // to make recovery from resource not found possible, considering some
335
+ // race conditions, where computable is created before tree is updated
336
+ this.resourcesAdded.attachWatcher(watcher);
337
+ throw new Error(`resource ${plClient.resourceIdToString(rid)} not found in the tree`);
338
+ }
339
+ res.resourceRemoved.attachWatcher(watcher);
340
+ return res;
341
+ }
342
+ updateFromResourceData(resourceData, allowOrphanInputs = false) {
343
+ this.checkValid();
344
+ // All resources for which recount should be incremented, first are aggregated in this list
345
+ const incrementRefs = [];
346
+ const decrementRefs = [];
347
+ // patching / creating resources
348
+ for (const rd of resourceData) {
349
+ let resource = this.resources.get(rd.id);
350
+ const statBeforeMutation = resource?.basicState;
351
+ const unexpectedTransitionError = (reason) => {
352
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
353
+ const { fields, ...rdWithoutFields } = rd;
354
+ this.invalidateTree();
355
+ throw new TreeStateUpdateError(`Unexpected resource state transition (${reason}): ${plClient.stringifyWithResourceId(rdWithoutFields)} -> ${plClient.stringifyWithResourceId(statBeforeMutation)}`);
356
+ };
357
+ if (resource !== undefined) {
358
+ // updating existing resource
359
+ if (resource.finalState)
360
+ unexpectedTransitionError('resource state can\t be updated after it is marked as final');
361
+ let changed = false;
362
+ // updating resource version, even if it was not changed
363
+ resource.version += 1;
364
+ // duplicate / original
365
+ if (resource.originalResourceId !== rd.originalResourceId) {
366
+ if (resource.originalResourceId !== plClient.NullResourceId)
367
+ unexpectedTransitionError('originalResourceId can\'t change after it is set');
368
+ resource.originalResourceId = rd.originalResourceId;
369
+ // duplicate status of the resource counts as ready for the external observer
370
+ tsHelpers.notEmpty(resource.resourceStateChange).markChanged(`originalResourceId changed for ${plClient.resourceIdToString(resource.id)}`);
371
+ changed = true;
372
+ }
373
+ // error
374
+ if (resource.error !== rd.error) {
375
+ if (plClient.isNotNullResourceId(resource.error))
376
+ unexpectedTransitionError('resource can\'t change attached error after it is set');
377
+ resource.error = rd.error;
378
+ incrementRefs.push(resource.error);
379
+ tsHelpers.notEmpty(resource.resourceStateChange).markChanged(`error changed for ${plClient.resourceIdToString(resource.id)}`);
380
+ changed = true;
381
+ }
382
+ // updating fields
383
+ for (const fd of rd.fields) {
384
+ let field = resource.fieldsMap.get(fd.name);
385
+ if (!field) {
386
+ // new field
387
+ field = new PlTreeField(fd.name, fd.type, fd.value, fd.error, fd.status, fd.valueIsFinal, resource.version);
388
+ if (plClient.isNotNullResourceId(fd.value))
389
+ incrementRefs.push(fd.value);
390
+ if (plClient.isNotNullResourceId(fd.error))
391
+ incrementRefs.push(fd.error);
392
+ if (fd.type === 'Input' || fd.type === 'Service') {
393
+ if (resource.inputsLocked)
394
+ unexpectedTransitionError(`adding ${fd.type} (${fd.name}) field while inputs locked`);
395
+ tsHelpers.notEmpty(resource.inputAndServiceFieldListChanged).markChanged(`new ${fd.type} field ${fd.name} added to ${plClient.resourceIdToString(resource.id)}`);
396
+ }
397
+ else if (fd.type === 'Output') {
398
+ if (resource.outputsLocked)
399
+ unexpectedTransitionError(`adding ${fd.type} (${fd.name}) field while outputs locked`);
400
+ tsHelpers.notEmpty(resource.outputFieldListChanged).markChanged(`new ${fd.type} field ${fd.name} added to ${plClient.resourceIdToString(resource.id)}`);
401
+ }
402
+ else {
403
+ tsHelpers.notEmpty(resource.dynamicFieldListChanged).markChanged(`new ${fd.type} field ${fd.name} added to ${plClient.resourceIdToString(resource.id)}`);
404
+ }
405
+ resource.fieldsMap.set(fd.name, field);
406
+ changed = true;
407
+ }
408
+ else {
409
+ // change of old field
410
+ // in principle this transition is possible, see assertions below
411
+ if (field.type !== fd.type) {
412
+ if (field.type !== 'Dynamic')
413
+ unexpectedTransitionError(`field changed type ${field.type} -> ${fd.type}`);
414
+ tsHelpers.notEmpty(resource.dynamicFieldListChanged).markChanged(`field ${fd.name} changed type from Dynamic to ${fd.type} in ${plClient.resourceIdToString(resource.id)}`);
415
+ if (field.type === 'Input' || field.type === 'Service') {
416
+ if (resource.inputsLocked)
417
+ unexpectedTransitionError(`adding input field "${fd.name}", while corresponding list is locked`);
418
+ tsHelpers.notEmpty(resource.inputAndServiceFieldListChanged).markChanged(`field ${fd.name} changed to type ${fd.type} in ${plClient.resourceIdToString(resource.id)}`);
419
+ }
420
+ if (field.type === 'Output') {
421
+ if (resource.outputsLocked)
422
+ unexpectedTransitionError(`adding output field "${fd.name}", while corresponding list is locked`);
423
+ tsHelpers.notEmpty(resource.outputFieldListChanged).markChanged(`field ${fd.name} changed to type ${fd.type} in ${plClient.resourceIdToString(resource.id)}`);
424
+ }
425
+ field.type = fd.type;
426
+ field.change.markChanged(`field ${fd.name} type changed to ${fd.type} in ${plClient.resourceIdToString(resource.id)}`);
427
+ changed = true;
428
+ }
429
+ // field value
430
+ if (field.value !== fd.value) {
431
+ if (plClient.isNotNullResourceId(field.value))
432
+ decrementRefs.push(field.value);
433
+ field.value = fd.value;
434
+ if (plClient.isNotNullResourceId(fd.value))
435
+ incrementRefs.push(fd.value);
436
+ field.change.markChanged(`field ${fd.name} value changed in ${plClient.resourceIdToString(resource.id)}`);
437
+ changed = true;
438
+ }
439
+ // field error
440
+ if (field.error !== fd.error) {
441
+ if (plClient.isNotNullResourceId(field.error))
442
+ decrementRefs.push(field.error);
443
+ field.error = fd.error;
444
+ if (plClient.isNotNullResourceId(fd.error))
445
+ incrementRefs.push(fd.error);
446
+ field.change.markChanged(`field ${fd.name} error changed in ${plClient.resourceIdToString(resource.id)}`);
447
+ changed = true;
448
+ }
449
+ // field status
450
+ if (field.status !== fd.status) {
451
+ field.status = fd.status;
452
+ field.change.markChanged(`field ${fd.name} status changed to ${fd.status} in ${plClient.resourceIdToString(resource.id)}`);
453
+ changed = true;
454
+ }
455
+ // field valueIsFinal flag
456
+ if (field.valueIsFinal !== fd.valueIsFinal) {
457
+ field.valueIsFinal = fd.valueIsFinal;
458
+ field.change.markChanged(`field ${fd.name} valueIsFinal changed to ${fd.valueIsFinal} in ${plClient.resourceIdToString(resource.id)}`);
459
+ changed = true;
460
+ }
461
+ field.resourceVersion = resource.version;
462
+ }
463
+ }
464
+ // detecting removed fields
465
+ resource.fieldsMap.forEach((field, fieldName, fields) => {
466
+ if (field.resourceVersion !== resource.version) {
467
+ if (field.type === 'Input' || field.type === 'Service' || field.type === 'Output')
468
+ unexpectedTransitionError(`removal of ${field.type} field ${fieldName}`);
469
+ field.change.markChanged(`dynamic field ${fieldName} removed from ${plClient.resourceIdToString(resource.id)}`);
470
+ fields.delete(fieldName);
471
+ if (plClient.isNotNullResourceId(field.value))
472
+ decrementRefs.push(field.value);
473
+ if (plClient.isNotNullResourceId(field.error))
474
+ decrementRefs.push(field.error);
475
+ tsHelpers.notEmpty(resource.dynamicFieldListChanged).markChanged(`dynamic field ${fieldName} removed from ${plClient.resourceIdToString(resource.id)}`);
476
+ }
477
+ });
478
+ // inputsLocked
479
+ if (resource.inputsLocked !== rd.inputsLocked) {
480
+ if (resource.inputsLocked)
481
+ unexpectedTransitionError('inputs unlocking is not permitted');
482
+ resource.inputsLocked = rd.inputsLocked;
483
+ tsHelpers.notEmpty(resource.lockedChange).markChanged(`inputs locked for ${plClient.resourceIdToString(resource.id)}`);
484
+ changed = true;
485
+ }
486
+ // outputsLocked
487
+ if (resource.outputsLocked !== rd.outputsLocked) {
488
+ if (resource.outputsLocked)
489
+ unexpectedTransitionError('outputs unlocking is not permitted');
490
+ resource.outputsLocked = rd.outputsLocked;
491
+ tsHelpers.notEmpty(resource.lockedChange).markChanged(`outputs locked for ${plClient.resourceIdToString(resource.id)}`);
492
+ changed = true;
493
+ }
494
+ // ready flag
495
+ if (resource.resourceReady !== rd.resourceReady) {
496
+ const readyStateBefore = resource.resourceReady;
497
+ resource.resourceReady = rd.resourceReady;
498
+ resource.verifyReadyState();
499
+ if (!resource.isReadyOrError)
500
+ unexpectedTransitionError(`resource can't lose it's ready or error state (ready state before ${readyStateBefore})`);
501
+ tsHelpers.notEmpty(resource.resourceStateChange).markChanged(`ready flag changed to ${rd.resourceReady} for ${plClient.resourceIdToString(resource.id)}`);
502
+ changed = true;
503
+ }
504
+ // syncing kv
505
+ for (const kv of rd.kv) {
506
+ const current = resource.kv.get(kv.key);
507
+ if (current === undefined) {
508
+ resource.kv.set(kv.key, kv.value);
509
+ tsHelpers.notEmpty(resource.kvChangedPerKey).markChanged(kv.key, `kv added for ${plClient.resourceIdToString(resource.id)}: ${kv.key}`);
510
+ }
511
+ else if (Buffer.compare(current, kv.value) !== 0) {
512
+ resource.kv.set(kv.key, kv.value);
513
+ tsHelpers.notEmpty(resource.kvChangedPerKey).markChanged(kv.key, `kv changed for ${plClient.resourceIdToString(resource.id)}: ${kv.key}`);
514
+ }
515
+ }
516
+ if (resource.kv.size > rd.kv.length) {
517
+ // only it this case it makes sense to check for deletions
518
+ const newStateKeys = new Set(rd.kv.map((kv) => kv.key));
519
+ // deleting keys not present in resource anymore
520
+ resource.kv.forEach((_value, key, map) => {
521
+ if (!newStateKeys.has(key)) {
522
+ map.delete(key);
523
+ tsHelpers.notEmpty(resource.kvChangedPerKey).markChanged(key, `kv deleted for ${plClient.resourceIdToString(resource.id)}: ${key}`);
524
+ }
525
+ });
526
+ }
527
+ if (changed) {
528
+ // if resource was changed, updating resource data version
529
+ resource.dataVersion = resource.version;
530
+ if (this.isFinalPredicate(resource))
531
+ resource.markFinal();
532
+ }
533
+ }
534
+ else {
535
+ // creating new resource
536
+ resource = new PlTreeResource(rd);
537
+ resource.verifyReadyState();
538
+ if (plClient.isNotNullResourceId(resource.error))
539
+ incrementRefs.push(resource.error);
540
+ for (const fd of rd.fields) {
541
+ const field = new PlTreeField(fd.name, fd.type, fd.value, fd.error, fd.status, fd.valueIsFinal, InitialResourceVersion);
542
+ if (plClient.isNotNullResourceId(fd.value))
543
+ incrementRefs.push(fd.value);
544
+ if (plClient.isNotNullResourceId(fd.error))
545
+ incrementRefs.push(fd.error);
546
+ resource.fieldsMap.set(fd.name, field);
547
+ }
548
+ // adding kv
549
+ for (const kv of rd.kv)
550
+ resource.kv.set(kv.key, kv.value);
551
+ // checking that resource is final, and if so, marking it
552
+ if (this.isFinalPredicate(resource))
553
+ resource.markFinal();
554
+ // adding the resource to the heap
555
+ this.resources.set(resource.id, resource);
556
+ this.resourcesAdded.markChanged(`new resource ${plClient.resourceIdToString(resource.id)} added`);
557
+ }
558
+ }
559
+ // applying refCount increments
560
+ for (const rid of incrementRefs) {
561
+ const res = this.resources.get(rid);
562
+ if (!res) {
563
+ this.invalidateTree();
564
+ throw new TreeStateUpdateError(`orphan resource ${rid}`);
565
+ }
566
+ res.refCount++;
567
+ }
568
+ // recursively applying refCount decrements / doing garbage collection
569
+ let currentRefs = decrementRefs;
570
+ while (currentRefs.length > 0) {
571
+ const nextRefs = [];
572
+ for (const rid of currentRefs) {
573
+ const res = this.resources.get(rid);
574
+ if (!res) {
575
+ this.invalidateTree();
576
+ throw new TreeStateUpdateError(`orphan resource ${rid}`);
577
+ }
578
+ res.refCount--;
579
+ // garbage collection
580
+ if (res.refCount === 0 && res.id !== this.root) {
581
+ // removing fields
582
+ res.fieldsMap.forEach((field) => {
583
+ if (plClient.isNotNullResourceId(field.value))
584
+ nextRefs.push(field.value);
585
+ if (plClient.isNotNullResourceId(field.error))
586
+ nextRefs.push(field.error);
587
+ field.change.markChanged(`field ${field.name} removed during garbage collection of ${plClient.resourceIdToString(res.id)}`);
588
+ });
589
+ if (plClient.isNotNullResourceId(res.error))
590
+ nextRefs.push(res.error);
591
+ res.resourceRemoved.markChanged(`resource removed during garbage collection: ${plClient.resourceIdToString(res.id)}`);
592
+ this.resources.delete(rid);
593
+ }
594
+ }
595
+ currentRefs = nextRefs;
596
+ }
597
+ // checking for orphans (maybe removed in the future)
598
+ if (!allowOrphanInputs) {
599
+ for (const rd of resourceData) {
600
+ if (!this.resources.has(rd.id)) {
601
+ this.invalidateTree();
602
+ throw new TreeStateUpdateError(`orphan input resource ${rd.id}`);
603
+ }
604
+ }
605
+ }
606
+ }
607
+ /** @deprecated use "entry" instead */
608
+ accessor(rid = this.root) {
609
+ this.checkValid();
610
+ return this.entry(rid);
611
+ }
612
+ entry(rid = this.root) {
613
+ this.checkValid();
614
+ return new accessors.PlTreeEntry({ treeProvider: () => this }, rid);
615
+ }
616
+ invalidateTree(msg) {
617
+ this._isValid = false;
618
+ this.invalidationMessage = msg;
619
+ this.resources.forEach((res) => {
620
+ res.markAllChanged();
621
+ });
622
+ }
623
+ dumpState() {
624
+ return Array.from(this.resources.values()).map((res) => res.extendedState);
625
+ }
626
+ }
627
+
628
+ exports.PlTreeResource = PlTreeResource;
629
+ exports.PlTreeState = PlTreeState;
630
+ exports.TreeStateUpdateError = TreeStateUpdateError;
631
+ //# sourceMappingURL=state.cjs.map