@milaboratories/pl-tree 1.3.17 → 1.3.19
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 +9 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +328 -286
- package/dist/index.mjs.map +1 -1
- package/dist/sync.d.ts +14 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/synchronized_tree.d.ts +7 -1
- package/dist/synchronized_tree.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/sync.ts +92 -7
- package/src/synchronized_tree.test.ts +32 -12
- package/src/synchronized_tree.ts +37 -7
package/src/sync.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
FieldData,
|
|
3
3
|
isNullResourceId,
|
|
4
|
+
NullResourceId,
|
|
4
5
|
OptionalResourceId,
|
|
5
6
|
PlTransaction,
|
|
6
7
|
ResourceId
|
|
7
8
|
} from '@milaboratories/pl-client';
|
|
8
9
|
import Denque from 'denque';
|
|
9
10
|
import { ExtendedResourceData, PlTreeState } from './state';
|
|
11
|
+
import { msToHumanReadable } from '@milaboratories/ts-helpers';
|
|
10
12
|
|
|
11
13
|
/** Applied to list of fields in resource data. */
|
|
12
14
|
export type PruningFunction = (resource: ExtendedResourceData) => FieldData[];
|
|
@@ -49,13 +51,59 @@ export function constructTreeLoadingRequest(
|
|
|
49
51
|
return { seedResources, finalResources, pruningFunction };
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
export type TreeLoadingStat = {
|
|
55
|
+
requests: number;
|
|
56
|
+
roundtrips: number;
|
|
57
|
+
retrievedResources: number;
|
|
58
|
+
retrievedFields: number;
|
|
59
|
+
retrievedKeyValues: number;
|
|
60
|
+
retrievedResourceDataBytes: number;
|
|
61
|
+
prunnedFields: number;
|
|
62
|
+
finalResourcesSkipped: number;
|
|
63
|
+
millisSpent: number;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export function initialTreeLoadingStat(): TreeLoadingStat {
|
|
67
|
+
return {
|
|
68
|
+
requests: 0,
|
|
69
|
+
roundtrips: 0,
|
|
70
|
+
retrievedResources: 0,
|
|
71
|
+
retrievedFields: 0,
|
|
72
|
+
retrievedKeyValues: 0,
|
|
73
|
+
retrievedResourceDataBytes: 0,
|
|
74
|
+
prunnedFields: 0,
|
|
75
|
+
finalResourcesSkipped: 0,
|
|
76
|
+
millisSpent: 0
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function formatTreeLoadingStat(stat: TreeLoadingStat): string {
|
|
81
|
+
let result = `Requests: ${stat.requests}\n`;
|
|
82
|
+
result += `Total time: ${msToHumanReadable(stat.millisSpent)}\n`;
|
|
83
|
+
result += `Roundtrips: ${stat.roundtrips}\n`;
|
|
84
|
+
result += `Resources: ${stat.retrievedResources}\n`;
|
|
85
|
+
result += `Fields: ${stat.retrievedFields}\n`;
|
|
86
|
+
result += `KV: ${stat.retrievedKeyValues}\n`;
|
|
87
|
+
result += `Data Bytes: ${stat.retrievedResourceDataBytes}\n`;
|
|
88
|
+
result += `Pruned fields: ${stat.prunnedFields}\n`;
|
|
89
|
+
result += `Final resources skipped: ${stat.finalResourcesSkipped}`;
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
52
93
|
/** Given the transaction (preferably read-only) and loading request, executes
|
|
53
94
|
* the tree traversal algorithm, and collects fresh states of resources
|
|
54
95
|
* to update the tree state. */
|
|
55
96
|
export async function loadTreeState(
|
|
56
97
|
tx: PlTransaction,
|
|
57
|
-
loadingRequest: TreeLoadingRequest
|
|
98
|
+
loadingRequest: TreeLoadingRequest,
|
|
99
|
+
stats?: TreeLoadingStat
|
|
58
100
|
): Promise<ExtendedResourceData[]> {
|
|
101
|
+
// saving start timestamp to add time spent in this function to the stats at the end of the method
|
|
102
|
+
const startTimestamp = Date.now();
|
|
103
|
+
|
|
104
|
+
// countind the request
|
|
105
|
+
if (stats) stats.requests++;
|
|
106
|
+
|
|
59
107
|
const { seedResources, finalResources, pruningFunction } = loadingRequest;
|
|
60
108
|
|
|
61
109
|
// Main idea of using a queue here is that responses will arrive in the same order as they were
|
|
@@ -65,10 +113,20 @@ export async function loadTreeState(
|
|
|
65
113
|
|
|
66
114
|
const pending = new Denque<Promise<ExtendedResourceData | undefined>>();
|
|
67
115
|
|
|
116
|
+
// vars to calculate number of roundtrips for stats
|
|
117
|
+
let roundtripToggle: boolean = true;
|
|
118
|
+
let numberOfRoundtrips = 0;
|
|
119
|
+
|
|
68
120
|
// tracking resources we already requested
|
|
69
121
|
const requested = new Set<ResourceId>();
|
|
70
122
|
const requestState = (rid: OptionalResourceId) => {
|
|
71
|
-
if (isNullResourceId(rid) || requested.has(rid)
|
|
123
|
+
if (isNullResourceId(rid) || requested.has(rid)) return;
|
|
124
|
+
|
|
125
|
+
// separate check to collect stats
|
|
126
|
+
if (finalResources.has(rid)) {
|
|
127
|
+
if (stats) stats.finalResourcesSkipped++;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
72
130
|
|
|
73
131
|
// adding the id, so we will not request it's state again if somebody else
|
|
74
132
|
// references the same resource
|
|
@@ -78,11 +136,20 @@ export async function loadTreeState(
|
|
|
78
136
|
const resourceData = tx.getResourceDataIfExists(rid, true);
|
|
79
137
|
const kvData = tx.listKeyValuesIfResourceExists(rid);
|
|
80
138
|
|
|
139
|
+
// counting roundrip (begin)
|
|
140
|
+
const addRT = roundtripToggle;
|
|
141
|
+
if (roundtripToggle) roundtripToggle = false;
|
|
142
|
+
|
|
81
143
|
// pushing combined promise
|
|
82
144
|
pending.push(
|
|
83
145
|
(async () => {
|
|
84
|
-
const resource = await resourceData;
|
|
85
|
-
|
|
146
|
+
const [resource, kv] = await Promise.all([resourceData, kvData]);
|
|
147
|
+
|
|
148
|
+
// counting roundrip, actually incrementing counter and returning toggle back, so the next request can acquire it
|
|
149
|
+
if (addRT) {
|
|
150
|
+
numberOfRoundtrips++;
|
|
151
|
+
roundtripToggle = true;
|
|
152
|
+
}
|
|
86
153
|
|
|
87
154
|
if (resource === undefined) return undefined;
|
|
88
155
|
|
|
@@ -110,9 +177,13 @@ export async function loadTreeState(
|
|
|
110
177
|
// ignoring resources that were not found (this may happen for seed resource ids)
|
|
111
178
|
continue;
|
|
112
179
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
180
|
+
if (pruningFunction !== undefined) {
|
|
181
|
+
// apply field pruning, if requested
|
|
182
|
+
const fieldsAfterPruning = pruningFunction(nextResource);
|
|
183
|
+
// collecting stats
|
|
184
|
+
if (stats) stats.prunnedFields += nextResource.fields.length - fieldsAfterPruning.length;
|
|
185
|
+
nextResource = { ...nextResource, fields: fieldsAfterPruning };
|
|
186
|
+
}
|
|
116
187
|
|
|
117
188
|
// continue traversal over the referenced resource
|
|
118
189
|
requestState(nextResource.error);
|
|
@@ -121,9 +192,23 @@ export async function loadTreeState(
|
|
|
121
192
|
requestState(field.error);
|
|
122
193
|
}
|
|
123
194
|
|
|
195
|
+
// collecting stats
|
|
196
|
+
if (stats) {
|
|
197
|
+
stats.retrievedResources++;
|
|
198
|
+
stats.retrievedFields += nextResource.fields.length;
|
|
199
|
+
stats.retrievedKeyValues += nextResource.kv.length;
|
|
200
|
+
stats.retrievedResourceDataBytes += nextResource.data?.length ?? 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
124
203
|
// aggregating the state
|
|
125
204
|
result.push(nextResource);
|
|
126
205
|
}
|
|
127
206
|
|
|
207
|
+
// adding the time we spent in this method to stats
|
|
208
|
+
if (stats) {
|
|
209
|
+
stats.millisSpent += Date.now() - startTimestamp;
|
|
210
|
+
stats.roundtrips += numberOfRoundtrips;
|
|
211
|
+
}
|
|
212
|
+
|
|
128
213
|
return result;
|
|
129
214
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { test, expect } from '@jest/globals';
|
|
1
2
|
import { field, TestHelpers } from '@milaboratories/pl-client';
|
|
2
3
|
import { TestStructuralResourceType1 } from './test_utils';
|
|
3
4
|
import { Computable } from '@milaboratories/computable';
|
|
4
5
|
import { SynchronizedTreeState } from './synchronized_tree';
|
|
6
|
+
import { ConsoleLoggerAdapter } from '@milaboratories/ts-helpers';
|
|
5
7
|
|
|
6
8
|
test('simple synchronized tree test', async () => {
|
|
7
9
|
await TestHelpers.withTempRoot(async (pl) => {
|
|
@@ -18,10 +20,16 @@ test('simple synchronized tree test', async () => {
|
|
|
18
20
|
{ sync: true }
|
|
19
21
|
);
|
|
20
22
|
|
|
21
|
-
const treeState = await SynchronizedTreeState.init(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const treeState = await SynchronizedTreeState.init(
|
|
24
|
+
pl,
|
|
25
|
+
r1,
|
|
26
|
+
{
|
|
27
|
+
stopPollingDelay: 10,
|
|
28
|
+
pollingInterval: 10,
|
|
29
|
+
logStat: 'cumulative'
|
|
30
|
+
},
|
|
31
|
+
new ConsoleLoggerAdapter(require('console'))
|
|
32
|
+
);
|
|
25
33
|
|
|
26
34
|
const theComputable = Computable.make((c) =>
|
|
27
35
|
c.accessor(treeState.entry()).node().traverse('a', 'b')?.getDataAsString()
|
|
@@ -113,10 +121,16 @@ test('synchronized tree test with KV', async () => {
|
|
|
113
121
|
{ sync: true }
|
|
114
122
|
);
|
|
115
123
|
|
|
116
|
-
const treeState = await SynchronizedTreeState.init(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
124
|
+
const treeState = await SynchronizedTreeState.init(
|
|
125
|
+
pl,
|
|
126
|
+
r1,
|
|
127
|
+
{
|
|
128
|
+
stopPollingDelay: 10,
|
|
129
|
+
pollingInterval: 10,
|
|
130
|
+
logStat: 'cumulative'
|
|
131
|
+
},
|
|
132
|
+
new ConsoleLoggerAdapter(require('console'))
|
|
133
|
+
);
|
|
120
134
|
|
|
121
135
|
const theComputable = Computable.make((c) =>
|
|
122
136
|
c.accessor(treeState.entry()).node().traverse('a')?.getKeyValueAsString('b', true)
|
|
@@ -182,10 +196,16 @@ test('termination test', async () => {
|
|
|
182
196
|
{ sync: true }
|
|
183
197
|
);
|
|
184
198
|
|
|
185
|
-
const treeState = await SynchronizedTreeState.init(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
199
|
+
const treeState = await SynchronizedTreeState.init(
|
|
200
|
+
pl,
|
|
201
|
+
r1,
|
|
202
|
+
{
|
|
203
|
+
stopPollingDelay: 10,
|
|
204
|
+
pollingInterval: 10,
|
|
205
|
+
logStat: 'cumulative'
|
|
206
|
+
},
|
|
207
|
+
new ConsoleLoggerAdapter(require('console'))
|
|
208
|
+
);
|
|
189
209
|
|
|
190
210
|
const entry = treeState.entry();
|
|
191
211
|
const theComputable = Computable.make((c) =>
|
package/src/synchronized_tree.ts
CHANGED
|
@@ -2,10 +2,18 @@ import { PollingComputableHooks } from '@milaboratories/computable';
|
|
|
2
2
|
import { PlTreeEntry } from './accessors';
|
|
3
3
|
import { isTimeoutOrCancelError, PlClient, ResourceId } from '@milaboratories/pl-client';
|
|
4
4
|
import { FinalPredicate, PlTreeState, TreeStateUpdateError } from './state';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
constructTreeLoadingRequest,
|
|
7
|
+
initialTreeLoadingStat,
|
|
8
|
+
loadTreeState,
|
|
9
|
+
PruningFunction,
|
|
10
|
+
TreeLoadingStat
|
|
11
|
+
} from './sync';
|
|
6
12
|
import * as tp from 'node:timers/promises';
|
|
7
13
|
import { MiLogger } from '@milaboratories/ts-helpers';
|
|
8
14
|
|
|
15
|
+
type StatLoggingMode = 'cumulative' | 'per-request';
|
|
16
|
+
|
|
9
17
|
export type SynchronizedTreeOps = {
|
|
10
18
|
finalPredicate?: FinalPredicate;
|
|
11
19
|
pruning?: PruningFunction;
|
|
@@ -14,6 +22,9 @@ export type SynchronizedTreeOps = {
|
|
|
14
22
|
pollingInterval: number;
|
|
15
23
|
/** For how long to continue polling after the last derived value access */
|
|
16
24
|
stopPollingDelay: number;
|
|
25
|
+
|
|
26
|
+
/** If one of the values, tree will log stats of each polling request */
|
|
27
|
+
logStat?: StatLoggingMode;
|
|
17
28
|
};
|
|
18
29
|
|
|
19
30
|
type ScheduledRefresh = {
|
|
@@ -26,6 +37,7 @@ export class SynchronizedTreeState {
|
|
|
26
37
|
private state: PlTreeState;
|
|
27
38
|
private readonly pollingInterval: number;
|
|
28
39
|
private readonly pruning?: PruningFunction;
|
|
40
|
+
private readonly logStat?: StatLoggingMode;
|
|
29
41
|
private readonly hooks: PollingComputableHooks;
|
|
30
42
|
private readonly abortController = new AbortController();
|
|
31
43
|
|
|
@@ -35,10 +47,11 @@ export class SynchronizedTreeState {
|
|
|
35
47
|
ops: SynchronizedTreeOps,
|
|
36
48
|
private readonly logger?: MiLogger
|
|
37
49
|
) {
|
|
38
|
-
const { finalPredicate, pruning, pollingInterval, stopPollingDelay } = ops;
|
|
50
|
+
const { finalPredicate, pruning, pollingInterval, stopPollingDelay, logStat } = ops;
|
|
39
51
|
this.pruning = pruning;
|
|
40
52
|
this.pollingInterval = pollingInterval;
|
|
41
53
|
this.finalPredicate = finalPredicate;
|
|
54
|
+
this.logStat = logStat;
|
|
42
55
|
this.state = new PlTreeState(root, finalPredicate);
|
|
43
56
|
this.hooks = new PollingComputableHooks(
|
|
44
57
|
() => this.startUpdating(),
|
|
@@ -91,11 +104,11 @@ export class SynchronizedTreeState {
|
|
|
91
104
|
private currentLoop: Promise<void> | undefined = undefined;
|
|
92
105
|
|
|
93
106
|
/** Executed from the main loop, and initialization procedure. */
|
|
94
|
-
private async refresh(): Promise<void> {
|
|
107
|
+
private async refresh(stats?: TreeLoadingStat): Promise<void> {
|
|
95
108
|
if (this.terminated) throw new Error('tree synchronization is terminated');
|
|
96
109
|
const request = constructTreeLoadingRequest(this.state, this.pruning);
|
|
97
110
|
const data = await this.pl.withReadTx('ReadingTree', async (tx) => {
|
|
98
|
-
return await loadTreeState(tx, request);
|
|
111
|
+
return await loadTreeState(tx, request, stats);
|
|
99
112
|
});
|
|
100
113
|
this.state.updateFromResourceData(data, true);
|
|
101
114
|
}
|
|
@@ -104,6 +117,9 @@ export class SynchronizedTreeState {
|
|
|
104
117
|
private terminated = false;
|
|
105
118
|
|
|
106
119
|
private async mainLoop() {
|
|
120
|
+
// will hold request stats
|
|
121
|
+
let stat = this.logStat ? initialTreeLoadingStat() : undefined;
|
|
122
|
+
|
|
107
123
|
while (true) {
|
|
108
124
|
if (!this.keepRunning) break;
|
|
109
125
|
|
|
@@ -117,12 +133,21 @@ export class SynchronizedTreeState {
|
|
|
117
133
|
}
|
|
118
134
|
|
|
119
135
|
try {
|
|
136
|
+
// resetting stats if we were asked to collect non-cumulative stats
|
|
137
|
+
if (this.logStat === 'per-request') stat = initialTreeLoadingStat();
|
|
138
|
+
|
|
120
139
|
// actual tree synchronization
|
|
121
|
-
await this.refresh();
|
|
140
|
+
await this.refresh(stat);
|
|
141
|
+
|
|
142
|
+
// logging stats if we were asked to
|
|
143
|
+
if (stat && this.logger) this.logger.info(`Tree stat (success): ${JSON.stringify(stat)}`);
|
|
122
144
|
|
|
123
145
|
// notifying that we got new state
|
|
124
146
|
if (toNotify !== undefined) for (const n of toNotify) n.resolve();
|
|
125
147
|
} catch (e: any) {
|
|
148
|
+
// logging stats if we were asked to (even if error occured)
|
|
149
|
+
if (stat && this.logger) this.logger.info(`Tree stat (error): ${JSON.stringify(stat)}`);
|
|
150
|
+
|
|
126
151
|
// notifying that we failed to refresh the state
|
|
127
152
|
if (toNotify !== undefined) for (const n of toNotify) n.reject(e);
|
|
128
153
|
|
|
@@ -181,8 +206,13 @@ export class SynchronizedTreeState {
|
|
|
181
206
|
await this.currentLoop;
|
|
182
207
|
}
|
|
183
208
|
|
|
184
|
-
public static async init(
|
|
185
|
-
|
|
209
|
+
public static async init(
|
|
210
|
+
pl: PlClient,
|
|
211
|
+
root: ResourceId,
|
|
212
|
+
ops: SynchronizedTreeOps,
|
|
213
|
+
logger?: MiLogger
|
|
214
|
+
) {
|
|
215
|
+
const tree = new SynchronizedTreeState(pl, root, ops, logger);
|
|
186
216
|
await tree.refresh();
|
|
187
217
|
return tree;
|
|
188
218
|
}
|