@milaboratories/pl-tree 1.4.2 → 1.4.3
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/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +282 -259
- package/dist/index.mjs.map +1 -1
- package/dist/state.d.ts +18 -23
- package/dist/state.d.ts.map +1 -1
- package/dist/sync.d.ts +2 -2
- package/dist/sync.d.ts.map +1 -1
- package/dist/synchronized_tree.d.ts +5 -3
- package/dist/synchronized_tree.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/snapshot.test.ts +2 -2
- package/src/state.test.ts +10 -10
- package/src/state.ts +73 -44
- package/src/sync.test.ts +2 -2
- package/src/sync.ts +18 -19
- package/src/synchronized_tree.ts +16 -7
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { PlTreeEntry } from './accessors';
|
|
2
|
-
import { PlClient, ResourceId } from '@milaboratories/pl-client';
|
|
3
|
-
import { FinalPredicate } from './state';
|
|
2
|
+
import { FinalResourceDataPredicate, PlClient, ResourceId } from '@milaboratories/pl-client';
|
|
4
3
|
import { PruningFunction } from './sync';
|
|
5
4
|
import { MiLogger } from '@milaboratories/ts-helpers';
|
|
6
5
|
type StatLoggingMode = 'cumulative' | 'per-request';
|
|
7
6
|
export type SynchronizedTreeOps = {
|
|
8
|
-
|
|
7
|
+
/** Override final predicate from the PlClient */
|
|
8
|
+
finalPredicateOverride?: FinalResourceDataPredicate;
|
|
9
|
+
/** Pruning function to limit set of fields through which tree will
|
|
10
|
+
* traverse during state synchronization */
|
|
9
11
|
pruning?: PruningFunction;
|
|
10
12
|
/** Interval after last sync to sleep before the next one */
|
|
11
13
|
pollingInterval: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"synchronized_tree.d.ts","sourceRoot":"","sources":["../src/synchronized_tree.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,
|
|
1
|
+
{"version":3,"file":"synchronized_tree.d.ts","sourceRoot":"","sources":["../src/synchronized_tree.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EACL,0BAA0B,EAE1B,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAIL,eAAe,EAEhB,MAAM,QAAQ,CAAC;AAEhB,OAAO,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAEtD,KAAK,eAAe,GAAG,YAAY,GAAG,aAAa,CAAC;AAEpD,MAAM,MAAM,mBAAmB,GAAG;IAChC,iDAAiD;IACjD,sBAAsB,CAAC,EAAE,0BAA0B,CAAC;IAEpD;+CAC2C;IAC3C,OAAO,CAAC,EAAE,eAAe,CAAC;IAE1B,4DAA4D;IAC5D,eAAe,EAAE,MAAM,CAAC;IACxB,2EAA2E;IAC3E,gBAAgB,EAAE,MAAM,CAAC;IAEzB,wEAAwE;IACxE,OAAO,CAAC,EAAE,eAAe,CAAC;CAC3B,CAAC;AAOF,qBAAa,qBAAqB;IAU9B,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAErB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAZ1B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAkB;IAC3C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAkB;IAC3C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;IAC/C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAyB;IAEzD,OAAO;IAoBP,sCAAsC;IAC/B,QAAQ,CAAC,GAAG,GAAE,UAAsB,GAAG,WAAW;IAKlD,KAAK,CAAC,GAAG,GAAE,UAAsB,GAAG,WAAW;IAKtD;wDACoD;IACvC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAK1C,OAAO,CAAC,oBAAoB,CAA0B;IAEtD,OAAO,CAAC,mBAAmB;IAK3B,2BAA2B;IAC3B,OAAO,CAAC,aAAa;IAMrB,2BAA2B;IAC3B,OAAO,CAAC,YAAY;IAIpB,yDAAyD;IACzD,OAAO,CAAC,WAAW,CAAS;IAC5B,iCAAiC;IACjC,OAAO,CAAC,WAAW,CAAwC;IAE3D,iEAAiE;YACnD,OAAO;IASrB,wDAAwD;IACxD,OAAO,CAAC,UAAU,CAAS;YAEb,QAAQ;IAqEtB;;;SAGK;IACQ,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAWvC,kBAAkB;IACL,wBAAwB,IAAI,OAAO,CAAC,IAAI,CAAC;WAKlC,IAAI,CACtB,EAAE,EAAE,QAAQ,EACZ,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,mBAAmB,EACxB,MAAM,CAAC,EAAE,QAAQ;CAapB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milaboratories/pl-tree",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"description": "Reactive pl tree state",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"utility-types": "^3.11.0",
|
|
22
22
|
"zod": "^3.23.8",
|
|
23
23
|
"@milaboratories/computable": "^2.1.13",
|
|
24
|
-
"@milaboratories/pl-client": "^2.5.
|
|
24
|
+
"@milaboratories/pl-client": "^2.5.2",
|
|
25
25
|
"@milaboratories/ts-helpers": "^1.1.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
package/src/snapshot.test.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from './test_utils';
|
|
12
12
|
import { PlTreeState } from './state';
|
|
13
13
|
import { Computable } from '@milaboratories/computable';
|
|
14
|
-
import { ResourceId } from '@milaboratories/pl-client';
|
|
14
|
+
import { DefaultFinalResourceDataPredicate, ResourceId } from '@milaboratories/pl-client';
|
|
15
15
|
|
|
16
16
|
// schema definition
|
|
17
17
|
const MyTestResourceState = rsSchema({
|
|
@@ -26,7 +26,7 @@ const MyTestResourceState = rsSchema({
|
|
|
26
26
|
type MyTestResourceState = InferSnapshot<typeof MyTestResourceState>;
|
|
27
27
|
|
|
28
28
|
test('simple snapshot test', async () => {
|
|
29
|
-
const tree = new PlTreeState(TestDynamicRootId1);
|
|
29
|
+
const tree = new PlTreeState(TestDynamicRootId1, DefaultFinalResourceDataPredicate);
|
|
30
30
|
|
|
31
31
|
const c1 = Computable.make((ctx) => {
|
|
32
32
|
const accessor = ctx.accessor(tree.entry()).node().traverse('a');
|
package/src/state.test.ts
CHANGED
|
@@ -11,14 +11,14 @@ import {
|
|
|
11
11
|
TestErrorResourceState2
|
|
12
12
|
} from './test_utils';
|
|
13
13
|
import { Computable } from '@milaboratories/computable';
|
|
14
|
-
import { NullResourceId, ResourceId } from '@milaboratories/pl-client';
|
|
14
|
+
import { DefaultFinalResourceDataPredicate, NullResourceId, ResourceId } from '@milaboratories/pl-client';
|
|
15
15
|
|
|
16
16
|
function rid(id: bigint): ResourceId {
|
|
17
17
|
return id as ResourceId;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
test('simple tree test 1', async () => {
|
|
21
|
-
const tree = new PlTreeState(TestDynamicRootId1);
|
|
21
|
+
const tree = new PlTreeState(TestDynamicRootId1, DefaultFinalResourceDataPredicate);
|
|
22
22
|
const entry = tree.entry();
|
|
23
23
|
expect(isPlTreeEntry(entry)).toStrictEqual(true);
|
|
24
24
|
const c1 = Computable.make((c) => {
|
|
@@ -68,7 +68,7 @@ test('simple tree test 1', async () => {
|
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
test('simple tree kv test', async () => {
|
|
71
|
-
const tree = new PlTreeState(TestDynamicRootId1);
|
|
71
|
+
const tree = new PlTreeState(TestDynamicRootId1, DefaultFinalResourceDataPredicate);
|
|
72
72
|
const c1 = Computable.make((c) =>
|
|
73
73
|
c.accessor(tree.entry()).node().traverse('a', 'b')?.getKeyValueAsString('thekey')
|
|
74
74
|
);
|
|
@@ -121,7 +121,7 @@ test('simple tree kv test', async () => {
|
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
test('partial tree update', async () => {
|
|
124
|
-
const tree = new PlTreeState(TestDynamicRootId1);
|
|
124
|
+
const tree = new PlTreeState(TestDynamicRootId1, DefaultFinalResourceDataPredicate);
|
|
125
125
|
const c1 = Computable.make((c) =>
|
|
126
126
|
c
|
|
127
127
|
.accessor(tree.entry())
|
|
@@ -157,7 +157,7 @@ test('partial tree update', async () => {
|
|
|
157
157
|
});
|
|
158
158
|
|
|
159
159
|
test('resource error', async () => {
|
|
160
|
-
const tree = new PlTreeState(TestDynamicRootId1);
|
|
160
|
+
const tree = new PlTreeState(TestDynamicRootId1, DefaultFinalResourceDataPredicate);
|
|
161
161
|
const c1 = Computable.make((c) =>
|
|
162
162
|
c.accessor(tree.entry()).node().traverse('a', 'b')?.getKeyValueAsString('thekey')
|
|
163
163
|
);
|
|
@@ -180,7 +180,7 @@ test('resource error', async () => {
|
|
|
180
180
|
});
|
|
181
181
|
|
|
182
182
|
test('field error', async () => {
|
|
183
|
-
const tree = new PlTreeState(TestDynamicRootId1);
|
|
183
|
+
const tree = new PlTreeState(TestDynamicRootId1, DefaultFinalResourceDataPredicate);
|
|
184
184
|
const c1 = Computable.make((c) =>
|
|
185
185
|
c.accessor(tree.entry()).node().traverse('b', 'a')?.getKeyValueAsString('thekey')
|
|
186
186
|
);
|
|
@@ -206,7 +206,7 @@ test('field error', async () => {
|
|
|
206
206
|
});
|
|
207
207
|
|
|
208
208
|
test('exception - deletion of input field', () => {
|
|
209
|
-
const tree = new PlTreeState(TestDynamicRootId1);
|
|
209
|
+
const tree = new PlTreeState(TestDynamicRootId1, DefaultFinalResourceDataPredicate);
|
|
210
210
|
|
|
211
211
|
tree.updateFromResourceData([
|
|
212
212
|
{ ...TestDynamicRootState1, fields: [dField('b'), dField('a', rid(1n))] },
|
|
@@ -224,7 +224,7 @@ test('exception - deletion of input field', () => {
|
|
|
224
224
|
});
|
|
225
225
|
|
|
226
226
|
test('exception - addition of input field', () => {
|
|
227
|
-
const tree = new PlTreeState(TestDynamicRootId1);
|
|
227
|
+
const tree = new PlTreeState(TestDynamicRootId1, DefaultFinalResourceDataPredicate);
|
|
228
228
|
|
|
229
229
|
tree.updateFromResourceData([
|
|
230
230
|
{ ...TestDynamicRootState1, fields: [dField('b'), dField('a', rid(1n))] },
|
|
@@ -254,7 +254,7 @@ test('exception - addition of input field', () => {
|
|
|
254
254
|
});
|
|
255
255
|
|
|
256
256
|
test('exception - ready without locks 1', () => {
|
|
257
|
-
const tree = new PlTreeState(TestDynamicRootId1);
|
|
257
|
+
const tree = new PlTreeState(TestDynamicRootId1, DefaultFinalResourceDataPredicate);
|
|
258
258
|
|
|
259
259
|
expect(() =>
|
|
260
260
|
tree.updateFromResourceData([
|
|
@@ -278,7 +278,7 @@ test('exception - ready without locks 1', () => {
|
|
|
278
278
|
});
|
|
279
279
|
|
|
280
280
|
test('exception - ready without locks 2', () => {
|
|
281
|
-
const tree = new PlTreeState(TestDynamicRootId1);
|
|
281
|
+
const tree = new PlTreeState(TestDynamicRootId1, DefaultFinalResourceDataPredicate);
|
|
282
282
|
|
|
283
283
|
tree.updateFromResourceData([
|
|
284
284
|
{ ...TestDynamicRootState1, fields: [dField('b'), dField('a', rid(1n))] },
|
package/src/state.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BasicResourceData,
|
|
3
|
+
FieldData,
|
|
4
|
+
FieldStatus,
|
|
3
5
|
FieldType,
|
|
4
6
|
isNotNullResourceId,
|
|
5
7
|
isNullResourceId,
|
|
@@ -17,7 +19,8 @@ import { ChangeSource, Watcher } from '@milaboratories/computable';
|
|
|
17
19
|
import { PlTreeEntry } from './accessors';
|
|
18
20
|
import { ValueAndError } from './value_and_error';
|
|
19
21
|
import { MiLogger, notEmpty } from '@milaboratories/ts-helpers';
|
|
20
|
-
import { FieldTraversalStep, GetFieldStep
|
|
22
|
+
import { FieldTraversalStep, GetFieldStep } from './traversal_ops';
|
|
23
|
+
import { FinalResourceDataPredicate } from '@milaboratories/pl-client';
|
|
21
24
|
|
|
22
25
|
export type ExtendedResourceData = ResourceData & {
|
|
23
26
|
kv: KeyValue[];
|
|
@@ -29,20 +32,16 @@ export class TreeStateUpdateError extends Error {
|
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
export interface PlTreeFieldI {
|
|
34
|
-
readonly type: FieldType;
|
|
35
|
-
readonly value: OptionalResourceId;
|
|
36
|
-
readonly error: OptionalResourceId;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
class PlTreeField {
|
|
35
|
+
class PlTreeField implements FieldData {
|
|
40
36
|
readonly change = new ChangeSource();
|
|
41
37
|
|
|
42
38
|
constructor(
|
|
39
|
+
public readonly name: string,
|
|
43
40
|
public type: FieldType,
|
|
44
41
|
public value: OptionalResourceId,
|
|
45
42
|
public error: OptionalResourceId,
|
|
43
|
+
public status: FieldStatus,
|
|
44
|
+
public valueIsFinal: boolean,
|
|
46
45
|
/** Last version of resource this field was observed, used to garbage collect fields in tree patching procedure */
|
|
47
46
|
public resourceVersion: number
|
|
48
47
|
) {}
|
|
@@ -52,18 +51,12 @@ const InitialResourceVersion = 0;
|
|
|
52
51
|
|
|
53
52
|
const decoder = new TextDecoder();
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
readonly final: boolean;
|
|
58
|
-
readonly fields: Map<string, PlTreeFieldI>;
|
|
54
|
+
export type ResourceDataWithFinalState = ResourceData & {
|
|
55
|
+
finalState: boolean;
|
|
59
56
|
};
|
|
60
57
|
|
|
61
|
-
/** Predicate of resource state used to determine if it's state is considered to be final,
|
|
62
|
-
* and not expected to change in the future. */
|
|
63
|
-
export type FinalPredicate = (r: Omit<PlTreeResourceI, 'final'>) => boolean;
|
|
64
|
-
|
|
65
58
|
/** Never store instances of this class, always get fresh instance from {@link PlTreeState} */
|
|
66
|
-
export class PlTreeResource implements
|
|
59
|
+
export class PlTreeResource implements ResourceDataWithFinalState {
|
|
67
60
|
/** Tracks number of other resources referencing this resource. Used to perform garbage collection in tree patching procedure */
|
|
68
61
|
refCount: number = 0;
|
|
69
62
|
|
|
@@ -72,7 +65,7 @@ export class PlTreeResource implements PlTreeResourceI {
|
|
|
72
65
|
/** Set to resource version when resource state, or it's fields have changed */
|
|
73
66
|
dataVersion: number = InitialResourceVersion;
|
|
74
67
|
|
|
75
|
-
readonly
|
|
68
|
+
readonly fieldsMap: Map<string, PlTreeField> = new Map();
|
|
76
69
|
|
|
77
70
|
readonly kv = new Map<string, Uint8Array>();
|
|
78
71
|
|
|
@@ -108,8 +101,8 @@ export class PlTreeResource implements PlTreeResourceI {
|
|
|
108
101
|
resourceReady: boolean;
|
|
109
102
|
finalFlag: boolean;
|
|
110
103
|
|
|
111
|
-
/** Set externally by the tree, using {@link
|
|
112
|
-
|
|
104
|
+
/** Set externally by the tree, using {@link FinalResourceDataPredicate} */
|
|
105
|
+
_finalState: boolean = false;
|
|
113
106
|
|
|
114
107
|
private readonly logger?: MiLogger;
|
|
115
108
|
|
|
@@ -138,7 +131,15 @@ export class PlTreeResource implements PlTreeResourceI {
|
|
|
138
131
|
}
|
|
139
132
|
|
|
140
133
|
get final(): boolean {
|
|
141
|
-
return this.
|
|
134
|
+
return this.finalFlag;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get finalState(): boolean {
|
|
138
|
+
return this._finalState;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get fields(): FieldData[] {
|
|
142
|
+
return [...this.fieldsMap.values()];
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
public getField(
|
|
@@ -160,7 +161,7 @@ export class PlTreeResource implements PlTreeResourceI {
|
|
|
160
161
|
): ValueAndError<ResourceId> | undefined {
|
|
161
162
|
const step: FieldTraversalStep = typeof _step === 'string' ? { field: _step } : _step;
|
|
162
163
|
|
|
163
|
-
const field = this.
|
|
164
|
+
const field = this.fieldsMap.get(step.field);
|
|
164
165
|
if (field === undefined) {
|
|
165
166
|
if (step.errorIfFieldNotFound || step.errorIfFieldNotSet)
|
|
166
167
|
throw new Error(
|
|
@@ -184,7 +185,7 @@ export class PlTreeResource implements PlTreeResourceI {
|
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
this.dynamicFieldListChanged?.attachWatcher(watcher);
|
|
187
|
-
if (!this.
|
|
188
|
+
if (!this._finalState && !step.stableIfNotFound) onUnstable('field_not_found:' + step.field);
|
|
188
189
|
|
|
189
190
|
return undefined;
|
|
190
191
|
} else {
|
|
@@ -230,7 +231,7 @@ export class PlTreeResource implements PlTreeResourceI {
|
|
|
230
231
|
|
|
231
232
|
public getIsFinal(watcher: Watcher): boolean {
|
|
232
233
|
this.finalChanged?.attachWatcher(watcher);
|
|
233
|
-
return this.
|
|
234
|
+
return this._finalState;
|
|
234
235
|
}
|
|
235
236
|
|
|
236
237
|
public getIsReadyOrError(watcher: Watcher): boolean {
|
|
@@ -252,7 +253,7 @@ export class PlTreeResource implements PlTreeResourceI {
|
|
|
252
253
|
|
|
253
254
|
public listInputFields(watcher: Watcher): string[] {
|
|
254
255
|
const ret: string[] = [];
|
|
255
|
-
this.
|
|
256
|
+
this.fieldsMap.forEach((field, name) => {
|
|
256
257
|
if (field.type === 'Input' || field.type === 'Service') ret.push(name);
|
|
257
258
|
});
|
|
258
259
|
if (!this.inputsLocked) this.inputAndServiceFieldListChanged?.attachWatcher(watcher);
|
|
@@ -262,7 +263,7 @@ export class PlTreeResource implements PlTreeResourceI {
|
|
|
262
263
|
|
|
263
264
|
public listOutputFields(watcher: Watcher): string[] {
|
|
264
265
|
const ret: string[] = [];
|
|
265
|
-
this.
|
|
266
|
+
this.fieldsMap.forEach((field, name) => {
|
|
266
267
|
if (field.type === 'Output') ret.push(name);
|
|
267
268
|
});
|
|
268
269
|
if (!this.outputsLocked) this.outputFieldListChanged?.attachWatcher(watcher);
|
|
@@ -272,7 +273,7 @@ export class PlTreeResource implements PlTreeResourceI {
|
|
|
272
273
|
|
|
273
274
|
public listDynamicFields(watcher: Watcher): string[] {
|
|
274
275
|
const ret: string[] = [];
|
|
275
|
-
this.
|
|
276
|
+
this.fieldsMap.forEach((field, name) => {
|
|
276
277
|
if (field.type !== 'Input' && field.type !== 'Output') ret.push(name);
|
|
277
278
|
});
|
|
278
279
|
this.dynamicFieldListChanged?.attachWatcher(watcher);
|
|
@@ -323,10 +324,11 @@ export class PlTreeResource implements PlTreeResourceI {
|
|
|
323
324
|
};
|
|
324
325
|
}
|
|
325
326
|
|
|
327
|
+
/** Called when {@link FinalResourceDataPredicate} returns true for the state. */
|
|
326
328
|
markFinal() {
|
|
327
|
-
if (this.
|
|
329
|
+
if (this._finalState) return;
|
|
328
330
|
|
|
329
|
-
this.
|
|
331
|
+
this._finalState = true;
|
|
330
332
|
notEmpty(this.finalChanged).markChanged();
|
|
331
333
|
this.finalChanged = undefined;
|
|
332
334
|
this.resourceStateChange = undefined;
|
|
@@ -338,7 +340,7 @@ export class PlTreeResource implements PlTreeResourceI {
|
|
|
338
340
|
|
|
339
341
|
/** Used for invalidation */
|
|
340
342
|
markAllChanged() {
|
|
341
|
-
this.
|
|
343
|
+
this.fieldsMap.forEach((field) => field.change.markChanged());
|
|
342
344
|
this.finalChanged?.markChanged();
|
|
343
345
|
this.resourceStateChange?.markChanged();
|
|
344
346
|
this.lockedChange?.markChanged();
|
|
@@ -350,9 +352,6 @@ export class PlTreeResource implements PlTreeResourceI {
|
|
|
350
352
|
}
|
|
351
353
|
}
|
|
352
354
|
|
|
353
|
-
// TODO implement invalidate tree
|
|
354
|
-
// TODO make invalid state permanent
|
|
355
|
-
// TODO invalidate on update errors
|
|
356
355
|
export class PlTreeState {
|
|
357
356
|
/** resource heap */
|
|
358
357
|
private resources: Map<ResourceId, PlTreeResource> = new Map();
|
|
@@ -365,10 +364,10 @@ export class PlTreeState {
|
|
|
365
364
|
constructor(
|
|
366
365
|
/** This will be the only resource not deleted during GC round */
|
|
367
366
|
public readonly root: ResourceId,
|
|
368
|
-
public readonly isFinalPredicate:
|
|
367
|
+
public readonly isFinalPredicate: FinalResourceDataPredicate
|
|
369
368
|
) {}
|
|
370
369
|
|
|
371
|
-
public forEachResource(cb: (res:
|
|
370
|
+
public forEachResource(cb: (res: ResourceDataWithFinalState) => void): void {
|
|
372
371
|
this.resources.forEach((v) => cb(v));
|
|
373
372
|
}
|
|
374
373
|
|
|
@@ -414,7 +413,7 @@ export class PlTreeState {
|
|
|
414
413
|
if (resource !== undefined) {
|
|
415
414
|
// updating existing resource
|
|
416
415
|
|
|
417
|
-
if (resource.
|
|
416
|
+
if (resource.finalState)
|
|
418
417
|
unexpectedTransitionError('resource state can\t be updated after it is marked as final');
|
|
419
418
|
|
|
420
419
|
let changed = false;
|
|
@@ -443,12 +442,20 @@ export class PlTreeState {
|
|
|
443
442
|
|
|
444
443
|
// updating fields
|
|
445
444
|
for (const fd of rd.fields) {
|
|
446
|
-
let field = resource.
|
|
445
|
+
let field = resource.fieldsMap.get(fd.name);
|
|
447
446
|
|
|
448
447
|
if (!field) {
|
|
449
448
|
// new field
|
|
450
449
|
|
|
451
|
-
field = new PlTreeField(
|
|
450
|
+
field = new PlTreeField(
|
|
451
|
+
fd.name,
|
|
452
|
+
fd.type,
|
|
453
|
+
fd.value,
|
|
454
|
+
fd.error,
|
|
455
|
+
fd.status,
|
|
456
|
+
fd.valueIsFinal,
|
|
457
|
+
resource.version
|
|
458
|
+
);
|
|
452
459
|
if (isNotNullResourceId(fd.value)) incrementRefs.push(fd.value);
|
|
453
460
|
if (isNotNullResourceId(fd.error)) incrementRefs.push(fd.error);
|
|
454
461
|
|
|
@@ -468,7 +475,7 @@ export class PlTreeState {
|
|
|
468
475
|
notEmpty(resource.dynamicFieldListChanged).markChanged();
|
|
469
476
|
}
|
|
470
477
|
|
|
471
|
-
resource.
|
|
478
|
+
resource.fieldsMap.set(fd.name, field);
|
|
472
479
|
|
|
473
480
|
changed = true;
|
|
474
481
|
} else {
|
|
@@ -516,12 +523,26 @@ export class PlTreeState {
|
|
|
516
523
|
changed = true;
|
|
517
524
|
}
|
|
518
525
|
|
|
526
|
+
// field status
|
|
527
|
+
if (field.status !== fd.status) {
|
|
528
|
+
field.status = fd.status;
|
|
529
|
+
field.change.markChanged();
|
|
530
|
+
changed = true;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// field valueIsFinal flag
|
|
534
|
+
if (field.valueIsFinal !== fd.valueIsFinal) {
|
|
535
|
+
field.valueIsFinal = fd.valueIsFinal;
|
|
536
|
+
field.change.markChanged();
|
|
537
|
+
changed = true;
|
|
538
|
+
}
|
|
539
|
+
|
|
519
540
|
field.resourceVersion = resource.version;
|
|
520
541
|
}
|
|
521
542
|
}
|
|
522
543
|
|
|
523
544
|
// detecting removed fields
|
|
524
|
-
resource.
|
|
545
|
+
resource.fieldsMap.forEach((field, fieldName, fields) => {
|
|
525
546
|
if (field.resourceVersion !== resource!.version) {
|
|
526
547
|
if (field.type === 'Input' || field.type === 'Service' || field.type === 'Output')
|
|
527
548
|
unexpectedTransitionError(`removal of ${field.type} field ${fieldName}`);
|
|
@@ -604,10 +625,18 @@ export class PlTreeState {
|
|
|
604
625
|
resource.verifyReadyState();
|
|
605
626
|
if (isNotNullResourceId(resource.error)) incrementRefs.push(resource.error);
|
|
606
627
|
for (const fd of rd.fields) {
|
|
607
|
-
const field = new PlTreeField(
|
|
628
|
+
const field = new PlTreeField(
|
|
629
|
+
fd.name,
|
|
630
|
+
fd.type,
|
|
631
|
+
fd.value,
|
|
632
|
+
fd.error,
|
|
633
|
+
fd.status,
|
|
634
|
+
fd.valueIsFinal,
|
|
635
|
+
InitialResourceVersion
|
|
636
|
+
);
|
|
608
637
|
if (isNotNullResourceId(fd.value)) incrementRefs.push(fd.value);
|
|
609
638
|
if (isNotNullResourceId(fd.error)) incrementRefs.push(fd.error);
|
|
610
|
-
resource.
|
|
639
|
+
resource.fieldsMap.set(fd.name, field);
|
|
611
640
|
}
|
|
612
641
|
|
|
613
642
|
// adding kv
|
|
@@ -647,7 +676,7 @@ export class PlTreeState {
|
|
|
647
676
|
// garbage collection
|
|
648
677
|
if (res.refCount === 0 && res.id !== this.root) {
|
|
649
678
|
// removing fields
|
|
650
|
-
res.
|
|
679
|
+
res.fieldsMap.forEach((field) => {
|
|
651
680
|
if (isNotNullResourceId(field.value)) nextRefs.push(field.value);
|
|
652
681
|
if (isNotNullResourceId(field.error)) nextRefs.push(field.error);
|
|
653
682
|
field.change.markChanged();
|
package/src/sync.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { test, expect } from '@jest/globals';
|
|
2
|
-
import { field, TestHelpers } from '@milaboratories/pl-client';
|
|
2
|
+
import { DefaultFinalResourceDataPredicate, field, TestHelpers } from '@milaboratories/pl-client';
|
|
3
3
|
import { PlTreeState } from './state';
|
|
4
4
|
import { constructTreeLoadingRequest, loadTreeState } from './sync';
|
|
5
5
|
import { Computable } from '@milaboratories/computable';
|
|
@@ -21,7 +21,7 @@ test('load resources', async () => {
|
|
|
21
21
|
{ sync: true }
|
|
22
22
|
);
|
|
23
23
|
|
|
24
|
-
const treeState = new PlTreeState(r1);
|
|
24
|
+
const treeState = new PlTreeState(r1, DefaultFinalResourceDataPredicate);
|
|
25
25
|
|
|
26
26
|
const theComputable = Computable.make((c) =>
|
|
27
27
|
c.accessor(treeState.entry()).node().traverse('a', 'b')?.getDataAsString()
|
package/src/sync.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
FieldData,
|
|
3
3
|
isNullResourceId,
|
|
4
|
-
NullResourceId,
|
|
5
4
|
OptionalResourceId,
|
|
6
5
|
PlTransaction,
|
|
7
6
|
ResourceId
|
|
@@ -41,7 +40,7 @@ export function constructTreeLoadingRequest(
|
|
|
41
40
|
const seedResources: ResourceId[] = [];
|
|
42
41
|
const finalResources = new Set<ResourceId>();
|
|
43
42
|
tree.forEachResource((res) => {
|
|
44
|
-
if (res.
|
|
43
|
+
if (res.finalState) finalResources.add(res.id);
|
|
45
44
|
else seedResources.push(res.id);
|
|
46
45
|
});
|
|
47
46
|
|
|
@@ -53,13 +52,13 @@ export function constructTreeLoadingRequest(
|
|
|
53
52
|
|
|
54
53
|
export type TreeLoadingStat = {
|
|
55
54
|
requests: number;
|
|
56
|
-
|
|
55
|
+
roundTrips: number;
|
|
57
56
|
retrievedResources: number;
|
|
58
57
|
retrievedFields: number;
|
|
59
58
|
retrievedKeyValues: number;
|
|
60
59
|
retrievedResourceDataBytes: number;
|
|
61
60
|
retrievedKeyValueBytes: number;
|
|
62
|
-
|
|
61
|
+
prunedFields: number;
|
|
63
62
|
finalResourcesSkipped: number;
|
|
64
63
|
millisSpent: number;
|
|
65
64
|
};
|
|
@@ -67,13 +66,13 @@ export type TreeLoadingStat = {
|
|
|
67
66
|
export function initialTreeLoadingStat(): TreeLoadingStat {
|
|
68
67
|
return {
|
|
69
68
|
requests: 0,
|
|
70
|
-
|
|
69
|
+
roundTrips: 0,
|
|
71
70
|
retrievedResources: 0,
|
|
72
71
|
retrievedFields: 0,
|
|
73
72
|
retrievedKeyValues: 0,
|
|
74
73
|
retrievedResourceDataBytes: 0,
|
|
75
74
|
retrievedKeyValueBytes: 0,
|
|
76
|
-
|
|
75
|
+
prunedFields: 0,
|
|
77
76
|
finalResourcesSkipped: 0,
|
|
78
77
|
millisSpent: 0
|
|
79
78
|
};
|
|
@@ -82,13 +81,13 @@ export function initialTreeLoadingStat(): TreeLoadingStat {
|
|
|
82
81
|
export function formatTreeLoadingStat(stat: TreeLoadingStat): string {
|
|
83
82
|
let result = `Requests: ${stat.requests}\n`;
|
|
84
83
|
result += `Total time: ${msToHumanReadable(stat.millisSpent)}\n`;
|
|
85
|
-
result += `
|
|
84
|
+
result += `Round-trips: ${stat.roundTrips}\n`;
|
|
86
85
|
result += `Resources: ${stat.retrievedResources}\n`;
|
|
87
86
|
result += `Fields: ${stat.retrievedFields}\n`;
|
|
88
87
|
result += `KV: ${stat.retrievedKeyValues}\n`;
|
|
89
88
|
result += `Data Bytes: ${stat.retrievedResourceDataBytes}\n`;
|
|
90
89
|
result += `KV Bytes: ${stat.retrievedKeyValueBytes}\n`;
|
|
91
|
-
result += `Pruned fields: ${stat.
|
|
90
|
+
result += `Pruned fields: ${stat.prunedFields}\n`;
|
|
92
91
|
result += `Final resources skipped: ${stat.finalResourcesSkipped}`;
|
|
93
92
|
return result;
|
|
94
93
|
}
|
|
@@ -104,7 +103,7 @@ export async function loadTreeState(
|
|
|
104
103
|
// saving start timestamp to add time spent in this function to the stats at the end of the method
|
|
105
104
|
const startTimestamp = Date.now();
|
|
106
105
|
|
|
107
|
-
//
|
|
106
|
+
// counting the request
|
|
108
107
|
if (stats) stats.requests++;
|
|
109
108
|
|
|
110
109
|
const { seedResources, finalResources, pruningFunction } = loadingRequest;
|
|
@@ -117,8 +116,8 @@ export async function loadTreeState(
|
|
|
117
116
|
const pending = new Denque<Promise<ExtendedResourceData | undefined>>();
|
|
118
117
|
|
|
119
118
|
// vars to calculate number of roundtrips for stats
|
|
120
|
-
let
|
|
121
|
-
let
|
|
119
|
+
let roundTripToggle: boolean = true;
|
|
120
|
+
let numberOfRoundTrips = 0;
|
|
122
121
|
|
|
123
122
|
// tracking resources we already requested
|
|
124
123
|
const requested = new Set<ResourceId>();
|
|
@@ -139,19 +138,19 @@ export async function loadTreeState(
|
|
|
139
138
|
const resourceData = tx.getResourceDataIfExists(rid, true);
|
|
140
139
|
const kvData = tx.listKeyValuesIfResourceExists(rid);
|
|
141
140
|
|
|
142
|
-
// counting
|
|
143
|
-
const addRT =
|
|
144
|
-
if (
|
|
141
|
+
// counting round-trip (begin)
|
|
142
|
+
const addRT = roundTripToggle;
|
|
143
|
+
if (roundTripToggle) roundTripToggle = false;
|
|
145
144
|
|
|
146
145
|
// pushing combined promise
|
|
147
146
|
pending.push(
|
|
148
147
|
(async () => {
|
|
149
148
|
const [resource, kv] = await Promise.all([resourceData, kvData]);
|
|
150
149
|
|
|
151
|
-
// counting
|
|
150
|
+
// counting round-trip, actually incrementing counter and returning toggle back, so the next request can acquire it
|
|
152
151
|
if (addRT) {
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
numberOfRoundTrips++;
|
|
153
|
+
roundTripToggle = true;
|
|
155
154
|
}
|
|
156
155
|
|
|
157
156
|
if (resource === undefined) return undefined;
|
|
@@ -184,7 +183,7 @@ export async function loadTreeState(
|
|
|
184
183
|
// apply field pruning, if requested
|
|
185
184
|
const fieldsAfterPruning = pruningFunction(nextResource);
|
|
186
185
|
// collecting stats
|
|
187
|
-
if (stats) stats.
|
|
186
|
+
if (stats) stats.prunedFields += nextResource.fields.length - fieldsAfterPruning.length;
|
|
188
187
|
nextResource = { ...nextResource, fields: fieldsAfterPruning };
|
|
189
188
|
}
|
|
190
189
|
|
|
@@ -211,7 +210,7 @@ export async function loadTreeState(
|
|
|
211
210
|
// adding the time we spent in this method to stats
|
|
212
211
|
if (stats) {
|
|
213
212
|
stats.millisSpent += Date.now() - startTimestamp;
|
|
214
|
-
stats.
|
|
213
|
+
stats.roundTrips += numberOfRoundTrips;
|
|
215
214
|
}
|
|
216
215
|
|
|
217
216
|
return result;
|
package/src/synchronized_tree.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { PollingComputableHooks } from '@milaboratories/computable';
|
|
2
2
|
import { PlTreeEntry } from './accessors';
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
FinalResourceDataPredicate,
|
|
5
|
+
isTimeoutOrCancelError,
|
|
6
|
+
PlClient,
|
|
7
|
+
ResourceId
|
|
8
|
+
} from '@milaboratories/pl-client';
|
|
9
|
+
import { PlTreeState, TreeStateUpdateError } from './state';
|
|
5
10
|
import {
|
|
6
11
|
constructTreeLoadingRequest,
|
|
7
12
|
initialTreeLoadingStat,
|
|
@@ -15,7 +20,11 @@ import { MiLogger } from '@milaboratories/ts-helpers';
|
|
|
15
20
|
type StatLoggingMode = 'cumulative' | 'per-request';
|
|
16
21
|
|
|
17
22
|
export type SynchronizedTreeOps = {
|
|
18
|
-
|
|
23
|
+
/** Override final predicate from the PlClient */
|
|
24
|
+
finalPredicateOverride?: FinalResourceDataPredicate;
|
|
25
|
+
|
|
26
|
+
/** Pruning function to limit set of fields through which tree will
|
|
27
|
+
* traverse during state synchronization */
|
|
19
28
|
pruning?: PruningFunction;
|
|
20
29
|
|
|
21
30
|
/** Interval after last sync to sleep before the next one */
|
|
@@ -33,7 +42,7 @@ type ScheduledRefresh = {
|
|
|
33
42
|
};
|
|
34
43
|
|
|
35
44
|
export class SynchronizedTreeState {
|
|
36
|
-
private readonly finalPredicate:
|
|
45
|
+
private readonly finalPredicate: FinalResourceDataPredicate;
|
|
37
46
|
private state: PlTreeState;
|
|
38
47
|
private readonly pollingInterval: number;
|
|
39
48
|
private readonly pruning?: PruningFunction;
|
|
@@ -47,12 +56,12 @@ export class SynchronizedTreeState {
|
|
|
47
56
|
ops: SynchronizedTreeOps,
|
|
48
57
|
private readonly logger?: MiLogger
|
|
49
58
|
) {
|
|
50
|
-
const {
|
|
59
|
+
const { finalPredicateOverride, pruning, pollingInterval, stopPollingDelay, logStat } = ops;
|
|
51
60
|
this.pruning = pruning;
|
|
52
61
|
this.pollingInterval = pollingInterval;
|
|
53
|
-
this.finalPredicate = finalPredicate;
|
|
62
|
+
this.finalPredicate = finalPredicateOverride ?? pl.finalPredicate;
|
|
54
63
|
this.logStat = logStat;
|
|
55
|
-
this.state = new PlTreeState(root, finalPredicate);
|
|
64
|
+
this.state = new PlTreeState(root, this.finalPredicate);
|
|
56
65
|
this.hooks = new PollingComputableHooks(
|
|
57
66
|
() => this.startUpdating(),
|
|
58
67
|
() => this.stopUpdating(),
|