@milaboratories/pl-tree 1.3.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.
- package/dist/index.cjs +1023 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +979 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
- package/src/accessors.ts +422 -0
- package/src/index.ts +8 -0
- package/src/snapshot.test.ts +101 -0
- package/src/snapshot.ts +219 -0
- package/src/state.test.ts +316 -0
- package/src/state.ts +681 -0
- package/src/sync.test.ts +101 -0
- package/src/sync.ts +129 -0
- package/src/synchronized_tree.test.ts +210 -0
- package/src/synchronized_tree.ts +189 -0
- package/src/test_utils.ts +156 -0
- package/src/traversal_ops.ts +56 -0
- package/src/value_and_error.ts +19 -0
- package/src/value_or_error.ts +9 -0
package/src/accessors.ts
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { PlTreeResource, PlTreeState } from './state';
|
|
2
|
+
import {
|
|
3
|
+
AccessorProvider,
|
|
4
|
+
ComputableCtx,
|
|
5
|
+
ComputableHooks,
|
|
6
|
+
UsageGuard
|
|
7
|
+
} from '@milaboratories/computable';
|
|
8
|
+
import {
|
|
9
|
+
ResourceId,
|
|
10
|
+
resourceIdToString,
|
|
11
|
+
ResourceType,
|
|
12
|
+
resourceTypesEqual,
|
|
13
|
+
resourceTypeToString,
|
|
14
|
+
NullResourceId,
|
|
15
|
+
OptionalResourceId
|
|
16
|
+
} from '@milaboratories/pl-client';
|
|
17
|
+
import { mapValueAndError, ValueAndError } from './value_and_error';
|
|
18
|
+
import {
|
|
19
|
+
CommonFieldTraverseOps,
|
|
20
|
+
FieldTraversalStep,
|
|
21
|
+
GetFieldStep,
|
|
22
|
+
ResourceTraversalOps
|
|
23
|
+
} from './traversal_ops';
|
|
24
|
+
import { ValueOrError } from './value_or_error';
|
|
25
|
+
import { ZodType, z } from 'zod';
|
|
26
|
+
import { Optional, Writable } from 'utility-types';
|
|
27
|
+
import { notEmpty } from '@milaboratories/ts-helpers';
|
|
28
|
+
|
|
29
|
+
/** Error encountered during traversal in field or resource. */
|
|
30
|
+
export class PlError extends Error {
|
|
31
|
+
constructor(message: string) {
|
|
32
|
+
super(message);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type TreeAccessorData = {
|
|
37
|
+
readonly treeProvider: () => PlTreeState;
|
|
38
|
+
readonly hooks?: ComputableHooks;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type TreeAccessorInstanceData = {
|
|
42
|
+
readonly guard: UsageGuard;
|
|
43
|
+
readonly ctx: ComputableCtx;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function isPlTreeEntry(obj: unknown): obj is PlTreeEntry {
|
|
47
|
+
return (
|
|
48
|
+
typeof obj === 'object' &&
|
|
49
|
+
obj !== null &&
|
|
50
|
+
(obj as any)['__pl_tree_type_marker__'] === 'PlTreeEntry'
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isPlTreeEntryAccessor(obj: unknown): obj is PlTreeEntryAccessor {
|
|
55
|
+
return (
|
|
56
|
+
typeof obj === 'object' &&
|
|
57
|
+
obj !== null &&
|
|
58
|
+
(obj as any)['__pl_tree_type_marker__'] === 'PlTreeEntryAccessor'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isPlTreeNodeAccessor(obj: unknown): obj is PlTreeNodeAccessor {
|
|
63
|
+
return (
|
|
64
|
+
typeof obj === 'object' &&
|
|
65
|
+
obj !== null &&
|
|
66
|
+
(obj as any)['__pl_tree_type_marker__'] === 'PlTreeNodeAccessor'
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Main entry point for using PlTree in reactive setting */
|
|
71
|
+
export class PlTreeEntry implements AccessorProvider<PlTreeEntryAccessor> {
|
|
72
|
+
private readonly __pl_tree_type_marker__ = 'PlTreeEntry';
|
|
73
|
+
|
|
74
|
+
constructor(
|
|
75
|
+
private readonly accessorData: TreeAccessorData,
|
|
76
|
+
public readonly rid: ResourceId
|
|
77
|
+
) {}
|
|
78
|
+
|
|
79
|
+
public createAccessor(ctx: ComputableCtx, guard: UsageGuard): PlTreeEntryAccessor {
|
|
80
|
+
return new PlTreeEntryAccessor(this.accessorData, this.accessorData.treeProvider(), this.rid, {
|
|
81
|
+
ctx,
|
|
82
|
+
guard
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public toJSON(): string {
|
|
87
|
+
return this.toString();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public toString(): string {
|
|
91
|
+
return `[ENTRY:${resourceIdToString(this.rid)}]`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getResourceFromTree(
|
|
96
|
+
accessorData: TreeAccessorData,
|
|
97
|
+
tree: PlTreeState,
|
|
98
|
+
instanceData: TreeAccessorInstanceData,
|
|
99
|
+
rid: ResourceId,
|
|
100
|
+
ops: ResourceTraversalOps
|
|
101
|
+
): PlTreeNodeAccessor {
|
|
102
|
+
const acc = new PlTreeNodeAccessor(
|
|
103
|
+
accessorData,
|
|
104
|
+
tree,
|
|
105
|
+
tree.get(instanceData.ctx.watcher, rid),
|
|
106
|
+
instanceData
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (!ops.ignoreError) {
|
|
110
|
+
const err = acc.getError();
|
|
111
|
+
if (err !== undefined)
|
|
112
|
+
throw new PlError(
|
|
113
|
+
`error encountered on resource ${resourceIdToString(acc.id)} (${resourceTypeToString(acc.resourceType)}): ${err.getDataAsString()}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
ops.assertResourceType !== undefined &&
|
|
119
|
+
(Array.isArray(ops.assertResourceType)
|
|
120
|
+
? ops.assertResourceType.findIndex((rt) => resourceTypesEqual(rt, acc.resourceType)) === -1
|
|
121
|
+
: !resourceTypesEqual(ops.assertResourceType, acc.resourceType))
|
|
122
|
+
)
|
|
123
|
+
throw new Error(
|
|
124
|
+
`wrong resource type ${resourceTypeToString(acc.resourceType)} but expected ${ops.assertResourceType}`
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return acc;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export class PlTreeEntryAccessor {
|
|
131
|
+
private readonly __pl_tree_type_marker__ = 'PlTreeEntryAccessor';
|
|
132
|
+
|
|
133
|
+
constructor(
|
|
134
|
+
private readonly accessorData: TreeAccessorData,
|
|
135
|
+
private readonly tree: PlTreeState,
|
|
136
|
+
private readonly rid: ResourceId,
|
|
137
|
+
private readonly instanceData: TreeAccessorInstanceData
|
|
138
|
+
) {}
|
|
139
|
+
|
|
140
|
+
node(ops: ResourceTraversalOps = {}): PlTreeNodeAccessor {
|
|
141
|
+
this.instanceData.guard();
|
|
142
|
+
|
|
143
|
+
// this is the only entry point to acquire a PlTreeNodeAccessor,
|
|
144
|
+
// so this is the only point where we should attach the hooks
|
|
145
|
+
if (this.accessorData.hooks !== undefined)
|
|
146
|
+
this.instanceData.ctx.attacheHooks(this.accessorData.hooks);
|
|
147
|
+
|
|
148
|
+
return getResourceFromTree(this.accessorData, this.tree, this.instanceData, this.rid, ops);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Helper type to simplify implementation of APIs requiring type information.
|
|
154
|
+
* @deprecated
|
|
155
|
+
* */
|
|
156
|
+
export type ResourceInfo = {
|
|
157
|
+
readonly id: ResourceId;
|
|
158
|
+
readonly type: ResourceType;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Can be called only when a ctx is provided, because pl tree entry is a computable entity.
|
|
163
|
+
* @deprecated
|
|
164
|
+
* */
|
|
165
|
+
export function treeEntryToResourceInfo(res: PlTreeEntry | ResourceInfo, ctx: ComputableCtx) {
|
|
166
|
+
if (res instanceof PlTreeEntry) return ctx.accessor(res).node().resourceInfo;
|
|
167
|
+
|
|
168
|
+
return res;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* API contracts:
|
|
173
|
+
* - API never return {@link NullResourceId}, absence of link is always modeled as `undefined`
|
|
174
|
+
*
|
|
175
|
+
* Important: never store instances of this class, always get fresh instance from {@link PlTreeState} accessor.
|
|
176
|
+
* */
|
|
177
|
+
export class PlTreeNodeAccessor {
|
|
178
|
+
private readonly __pl_tree_type_marker__ = 'PlTreeNodeAccessor';
|
|
179
|
+
|
|
180
|
+
constructor(
|
|
181
|
+
private readonly accessorData: TreeAccessorData,
|
|
182
|
+
private readonly tree: PlTreeState,
|
|
183
|
+
private readonly resource: PlTreeResource,
|
|
184
|
+
private readonly instanceData: TreeAccessorInstanceData
|
|
185
|
+
) {}
|
|
186
|
+
|
|
187
|
+
public get id(): ResourceId {
|
|
188
|
+
this.instanceData.guard();
|
|
189
|
+
return this.resource.id;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public get originalId(): OptionalResourceId {
|
|
193
|
+
this.instanceData.guard();
|
|
194
|
+
return this.resource.originalResourceId;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public get resourceType(): ResourceType {
|
|
198
|
+
this.instanceData.guard();
|
|
199
|
+
return this.resource.type;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
public get resourceInfo(): ResourceInfo {
|
|
203
|
+
return { id: this.id, type: this.resourceType };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private getResourceFromTree(rid: ResourceId, ops: ResourceTraversalOps): PlTreeNodeAccessor {
|
|
207
|
+
return getResourceFromTree(this.accessorData, this.tree, this.instanceData, rid, ops);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
public traverse(
|
|
211
|
+
...steps: [
|
|
212
|
+
Omit<FieldTraversalStep, 'errorIfFieldNotSet'> & {
|
|
213
|
+
errorIfFieldNotSet: true;
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
): PlTreeNodeAccessor;
|
|
217
|
+
public traverse(...steps: (FieldTraversalStep | string)[]): PlTreeNodeAccessor | undefined;
|
|
218
|
+
public traverse(...steps: (FieldTraversalStep | string)[]): PlTreeNodeAccessor | undefined {
|
|
219
|
+
return this.traverseWithCommon({}, ...steps);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
public traverseOrError(
|
|
223
|
+
...steps: [
|
|
224
|
+
Omit<FieldTraversalStep, 'errorIfFieldNotSet'> & {
|
|
225
|
+
errorIfFieldNotSet: true;
|
|
226
|
+
}
|
|
227
|
+
]
|
|
228
|
+
): ValueOrError<PlTreeNodeAccessor, string>;
|
|
229
|
+
public traverseOrError(
|
|
230
|
+
...steps: (FieldTraversalStep | string)[]
|
|
231
|
+
): ValueOrError<PlTreeNodeAccessor, string> | undefined;
|
|
232
|
+
public traverseOrError(
|
|
233
|
+
...steps: (FieldTraversalStep | string)[]
|
|
234
|
+
): ValueOrError<PlTreeNodeAccessor, string> | undefined {
|
|
235
|
+
return this.traverseOrErrorWithCommon({}, ...steps);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
public traverseWithCommon(
|
|
239
|
+
commonOptions: CommonFieldTraverseOps,
|
|
240
|
+
...steps: (FieldTraversalStep | string)[]
|
|
241
|
+
): PlTreeNodeAccessor | undefined {
|
|
242
|
+
const result = this.traverseOrErrorWithCommon(commonOptions, ...steps);
|
|
243
|
+
if (result === undefined) return undefined;
|
|
244
|
+
if (!result.ok) throw new PlError(result.error);
|
|
245
|
+
return result.value;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
public traverseOrErrorWithCommon(
|
|
249
|
+
commonOptions: CommonFieldTraverseOps,
|
|
250
|
+
...steps: (FieldTraversalStep | string)[]
|
|
251
|
+
): ValueOrError<PlTreeNodeAccessor, string> | undefined {
|
|
252
|
+
let current: PlTreeNodeAccessor = this;
|
|
253
|
+
|
|
254
|
+
for (const _step of steps) {
|
|
255
|
+
const step: FieldTraversalStep =
|
|
256
|
+
typeof _step === 'string'
|
|
257
|
+
? {
|
|
258
|
+
...commonOptions,
|
|
259
|
+
field: _step
|
|
260
|
+
}
|
|
261
|
+
: { ...commonOptions, ..._step };
|
|
262
|
+
|
|
263
|
+
const next = current.getField(_step);
|
|
264
|
+
|
|
265
|
+
if (next === undefined) return undefined;
|
|
266
|
+
|
|
267
|
+
if (step.pureFieldErrorToUndefined && next.value === undefined && next.error !== undefined)
|
|
268
|
+
return undefined;
|
|
269
|
+
|
|
270
|
+
if ((!step.ignoreError || next.value === undefined) && next.error !== undefined)
|
|
271
|
+
return {
|
|
272
|
+
ok: false,
|
|
273
|
+
error: `error in field ${step.field} of ${resourceIdToString(current.id)}: ${next.error.getDataAsString()}`
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
if (next.value === undefined) {
|
|
277
|
+
if (step.errorIfFieldNotSet)
|
|
278
|
+
return {
|
|
279
|
+
ok: false,
|
|
280
|
+
error: `field have no assigned value ${step.field} of ${resourceIdToString(current.id)}`
|
|
281
|
+
};
|
|
282
|
+
// existing but unpopulated field is unstable because it must be resolved at some point
|
|
283
|
+
this.onUnstableLambda('unpopulated_field:' + step.field);
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
current = next.value;
|
|
288
|
+
}
|
|
289
|
+
return { ok: true, value: current };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private readonly onUnstableLambda = (marker: string) => {
|
|
293
|
+
this.instanceData.ctx.markUnstable(marker);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
public getField(
|
|
297
|
+
_step:
|
|
298
|
+
| (Omit<GetFieldStep, 'errorIfFieldNotFound'> & { errorIfFieldNotFound: true })
|
|
299
|
+
| (Omit<GetFieldStep, 'errorIfFieldNotSet'> & { errorIfFieldNotSet: true })
|
|
300
|
+
): ValueAndError<PlTreeNodeAccessor>;
|
|
301
|
+
public getField(_step: GetFieldStep | string): ValueAndError<PlTreeNodeAccessor> | undefined;
|
|
302
|
+
public getField(_step: GetFieldStep | string): ValueAndError<PlTreeNodeAccessor> | undefined {
|
|
303
|
+
this.instanceData.guard();
|
|
304
|
+
const step: GetFieldStep = typeof _step === 'string' ? { field: _step } : _step;
|
|
305
|
+
|
|
306
|
+
const ve = this.resource.getField(this.instanceData.ctx.watcher, step, this.onUnstableLambda);
|
|
307
|
+
|
|
308
|
+
if (ve === undefined) return undefined;
|
|
309
|
+
|
|
310
|
+
return mapValueAndError(ve, (rid) => this.getResourceFromTree(rid, { ignoreError: true }));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
public getInputsLocked(): boolean {
|
|
314
|
+
this.instanceData.guard();
|
|
315
|
+
const result = this.resource.getInputsLocked(this.instanceData.ctx.watcher);
|
|
316
|
+
if (!result) this.instanceData.ctx.markUnstable('inputs_unlocked:' + this.resourceType.name);
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
public getOutputsLocked(): boolean {
|
|
321
|
+
this.instanceData.guard();
|
|
322
|
+
const result = this.resource.getOutputsLocked(this.instanceData.ctx.watcher);
|
|
323
|
+
if (!result) this.instanceData.ctx.markUnstable('outputs_unlocked:' + this.resourceType.name);
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
public getIsReadyOrError(): boolean {
|
|
328
|
+
this.instanceData.guard();
|
|
329
|
+
const result = this.resource.getIsReadyOrError(this.instanceData.ctx.watcher);
|
|
330
|
+
if (!result) this.instanceData.ctx.markUnstable('not_ready:' + this.resourceType.name);
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
public getIsFinal() {
|
|
335
|
+
this.instanceData.guard();
|
|
336
|
+
return this.resource.getIsFinal(this.instanceData.ctx.watcher);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
public getError(): PlTreeNodeAccessor | undefined {
|
|
340
|
+
this.instanceData.guard();
|
|
341
|
+
const rid = this.resource.getError(this.instanceData.ctx.watcher);
|
|
342
|
+
if (rid === undefined)
|
|
343
|
+
// absence of error always considered as stable
|
|
344
|
+
return undefined;
|
|
345
|
+
return this.getResourceFromTree(rid, {});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
public getData(): Uint8Array | undefined {
|
|
349
|
+
return this.resource.data;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
public getDataAsString(): string | undefined {
|
|
353
|
+
return this.resource.getDataAsString();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
public getDataAsJson<T = unknown>(): T | undefined {
|
|
357
|
+
return this.resource.getDataAsJson<T>();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
public listInputFields(): string[] {
|
|
361
|
+
this.instanceData.guard();
|
|
362
|
+
this.getInputsLocked(); // will set unstable if not locked
|
|
363
|
+
return this.resource.listInputFields(this.instanceData.ctx.watcher);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
public listOutputFields(): string[] {
|
|
367
|
+
this.instanceData.guard();
|
|
368
|
+
this.getOutputsLocked(); // will set unstable if not locked
|
|
369
|
+
return this.resource.listOutputFields(this.instanceData.ctx.watcher);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
public listDynamicFields(): string[] {
|
|
373
|
+
this.instanceData.guard();
|
|
374
|
+
return this.resource.listDynamicFields(this.instanceData.ctx.watcher);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
public getKeyValue(key: string, unstableIfNotFound: boolean = false): Uint8Array | undefined {
|
|
378
|
+
this.instanceData.guard();
|
|
379
|
+
const result = this.resource.getKeyValue(this.instanceData.ctx.watcher, key);
|
|
380
|
+
if (result === undefined && unstableIfNotFound)
|
|
381
|
+
this.instanceData.ctx.markUnstable('key_not_found_b:' + key);
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** @deprecated */
|
|
386
|
+
public getKeyValueString(key: string): string | undefined {
|
|
387
|
+
return this.getKeyValueAsString(key);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
public getKeyValueAsString(key: string, unstableIfNotFound: boolean = false): string | undefined {
|
|
391
|
+
this.instanceData.guard();
|
|
392
|
+
const result = this.resource.getKeyValueString(this.instanceData.ctx.watcher, key);
|
|
393
|
+
if (result === undefined && unstableIfNotFound)
|
|
394
|
+
this.instanceData.ctx.markUnstable('key_not_found_s:' + key);
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
public getKeyValueAsJson<T = unknown>(
|
|
399
|
+
key: string,
|
|
400
|
+
unstableIfNotFound: boolean = false
|
|
401
|
+
): T | undefined {
|
|
402
|
+
const result = this.resource.getKeyValueString(this.instanceData.ctx.watcher, key);
|
|
403
|
+
if (result === undefined) {
|
|
404
|
+
if (unstableIfNotFound) this.instanceData.ctx.markUnstable('key_not_found_j:' + key);
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
return JSON.parse(result) as T;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Can be used to passe a higher level accessor that will wrap the resource and throw its
|
|
412
|
+
* errors on node resolution.
|
|
413
|
+
* */
|
|
414
|
+
public toEntryAccessor(): PlTreeEntryAccessor {
|
|
415
|
+
return new PlTreeEntryAccessor(this.accessorData, this.tree, this.id, this.instanceData);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Can be passed to nested computable. */
|
|
419
|
+
public persist(): PlTreeEntry {
|
|
420
|
+
return new PlTreeEntry(this.accessorData, this.resource.id);
|
|
421
|
+
}
|
|
422
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { InferSnapshot, makeResourceSnapshot, rsSchema } from './snapshot';
|
|
3
|
+
import {
|
|
4
|
+
TestDynamicRootId1,
|
|
5
|
+
TestDynamicRootState1,
|
|
6
|
+
TestStructuralResourceState1,
|
|
7
|
+
TestStructuralResourceType1,
|
|
8
|
+
TestValueResourceState1,
|
|
9
|
+
dField,
|
|
10
|
+
iField
|
|
11
|
+
} from './test_utils';
|
|
12
|
+
import { PlTreeState } from './state';
|
|
13
|
+
import { Computable } from '@milaboratories/computable';
|
|
14
|
+
import { ResourceId } from '@milaboratories/pl-client';
|
|
15
|
+
|
|
16
|
+
// schema definition
|
|
17
|
+
const MyTestResourceState = rsSchema({
|
|
18
|
+
data: z.object({
|
|
19
|
+
jf: z.number()
|
|
20
|
+
}),
|
|
21
|
+
fields: { b: true, c: false },
|
|
22
|
+
kv: { thekey: z.string() }
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// type derived from schema for out users and us
|
|
26
|
+
type MyTestResourceState = InferSnapshot<typeof MyTestResourceState>;
|
|
27
|
+
|
|
28
|
+
test('simple snapshot test', async () => {
|
|
29
|
+
const tree = new PlTreeState(TestDynamicRootId1);
|
|
30
|
+
|
|
31
|
+
const c1 = Computable.make((ctx) => {
|
|
32
|
+
const accessor = ctx.accessor(tree.entry()).node().traverse('a');
|
|
33
|
+
if (accessor == undefined) return undefined;
|
|
34
|
+
|
|
35
|
+
const result: MyTestResourceState = makeResourceSnapshot(accessor, MyTestResourceState);
|
|
36
|
+
return result;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
tree.updateFromResourceData([
|
|
40
|
+
{ ...TestDynamicRootState1, fields: [dField('b'), dField('a', rid(1n))] },
|
|
41
|
+
{
|
|
42
|
+
...TestStructuralResourceState1,
|
|
43
|
+
id: rid(1n),
|
|
44
|
+
fields: [iField('b', rid(2n))],
|
|
45
|
+
data: new TextEncoder().encode(`{"jf": 0}`)
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
...TestValueResourceState1,
|
|
49
|
+
id: rid(2n)
|
|
50
|
+
}
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
expect(c1.isChanged()).toBeTruthy();
|
|
54
|
+
expect((await c1.getValueOrError()).type).toStrictEqual('error');
|
|
55
|
+
expect(c1.isChanged()).toBeFalsy();
|
|
56
|
+
|
|
57
|
+
tree.updateFromResourceData([
|
|
58
|
+
{
|
|
59
|
+
...TestValueResourceState1,
|
|
60
|
+
id: rid(1n),
|
|
61
|
+
fields: [iField('b', rid(2n))],
|
|
62
|
+
data: new TextEncoder().encode(`{"jf": 0}`),
|
|
63
|
+
kv: [{ key: 'thekey', value: Buffer.from('"thevalue"') }]
|
|
64
|
+
}
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
expect(c1.isChanged()).toBeTruthy();
|
|
68
|
+
expect(await c1.getValue()).toMatchObject({
|
|
69
|
+
id: rid(1n),
|
|
70
|
+
type: TestStructuralResourceType1,
|
|
71
|
+
data: {
|
|
72
|
+
jf: 0
|
|
73
|
+
},
|
|
74
|
+
fields: {
|
|
75
|
+
b: rid(2n),
|
|
76
|
+
c: undefined
|
|
77
|
+
},
|
|
78
|
+
kv: {
|
|
79
|
+
thekey: 'thevalue'
|
|
80
|
+
}
|
|
81
|
+
} as MyTestResourceState);
|
|
82
|
+
expect(c1.isChanged()).toBeFalsy();
|
|
83
|
+
|
|
84
|
+
tree.updateFromResourceData([
|
|
85
|
+
{
|
|
86
|
+
...TestValueResourceState1,
|
|
87
|
+
id: rid(1n),
|
|
88
|
+
fields: [iField('b', rid(2n))],
|
|
89
|
+
data: new TextEncoder().encode(`{"jf": 0}`),
|
|
90
|
+
kv: [{ key: 'thekey', value: Buffer.from('123') }] // thekey type changed to number (invalid accordig to zod schema)
|
|
91
|
+
}
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
expect(c1.isChanged()).toBeTruthy();
|
|
95
|
+
expect((await c1.getValueOrError()).type).toStrictEqual('error');
|
|
96
|
+
expect(c1.isChanged()).toBeFalsy();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
function rid(id: bigint): ResourceId {
|
|
100
|
+
return id as ResourceId;
|
|
101
|
+
}
|