@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.
@@ -0,0 +1,219 @@
1
+ import { ResourceId, ResourceType } from '@milaboratories/pl-client';
2
+ import { Optional, Writable } from 'utility-types';
3
+ import { ZodType, z } from 'zod';
4
+ import { PlTreeEntry, PlTreeEntryAccessor, PlTreeNodeAccessor } from './accessors';
5
+ import { ComputableCtx } from '@milaboratories/computable';
6
+ import { notEmpty } from '@milaboratories/ts-helpers';
7
+
8
+ /**
9
+ * A DTO that can be generated from a tree node to make a snapshot of specific parts of it's state.
10
+ * Such snapshots can then be used in core that requires this information without the need of
11
+ * retrieving state from the tree.
12
+ */
13
+ export type ResourceSnapshot<
14
+ Data = undefined,
15
+ Fields extends Record<string, ResourceId | undefined> | undefined = undefined,
16
+ KV extends Record<string, unknown> | undefined = undefined
17
+ > = {
18
+ readonly id: ResourceId;
19
+ readonly type: ResourceType;
20
+ readonly data: Data;
21
+ readonly fields: Fields;
22
+ readonly kv: KV;
23
+ };
24
+
25
+ /** The most generic type of ResourceSnapshot. */
26
+ type ResourceSnapshotGeneric = ResourceSnapshot<
27
+ unknown,
28
+ Record<string, ResourceId | undefined> | undefined,
29
+ Record<string, unknown> | undefined
30
+ >;
31
+
32
+ /** Request that we'll pass to getResourceSnapshot function. We infer the type of ResourceSnapshot from this. */
33
+ export type ResourceSnapshotSchema<
34
+ Data extends ZodType | 'raw' | undefined = undefined,
35
+ Fields extends Record<string, boolean> | undefined = undefined,
36
+ KV extends Record<string, ZodType | 'raw'> | undefined = undefined
37
+ > = {
38
+ readonly data: Data;
39
+ readonly fields: Fields;
40
+ readonly kv: KV;
41
+ };
42
+
43
+ /** Creates ResourceSnapshotSchema. It converts an optional schema type to schema type. */
44
+ export function rsSchema<
45
+ const Data extends ZodType | 'raw' | undefined = undefined,
46
+ const Fields extends Record<string, boolean> | undefined = undefined,
47
+ const KV extends Record<string, ZodType | 'raw'> | undefined = undefined
48
+ >(
49
+ schema: Optional<ResourceSnapshotSchema<Data, Fields, KV>>
50
+ ): ResourceSnapshotSchema<Data, Fields, KV> {
51
+ return schema as any;
52
+ }
53
+
54
+ /** The most generic type of ResourceSnapshotSchema. */
55
+ type ResourceSnapshotSchemaGeneric = ResourceSnapshotSchema<
56
+ ZodType | 'raw' | undefined,
57
+ Record<string, boolean> | undefined,
58
+ Record<string, ZodType | 'raw'> | undefined
59
+ >;
60
+
61
+ /**
62
+ * If Data is 'raw' in schema, we'll get bytes,
63
+ * if it's Zod, we'll parse it via zod.
64
+ * Or else we just got undefined in the field.
65
+ */
66
+ type InferDataType<Data extends ZodType | 'raw' | undefined> = Data extends 'raw'
67
+ ? Uint8Array
68
+ : Data extends ZodType
69
+ ? z.infer<Data>
70
+ : undefined;
71
+
72
+ /**
73
+ * If Fields is a record of field names to booleans,
74
+ * then if the value of the field is true, we'll require this field and throw a Error if it wasn't found.
75
+ * If it's false and doesn't exist, we'll return undefined.
76
+ * If Fields type is undefined, we won't set fields at all.
77
+ */
78
+ type InferFieldsType<Fields extends Record<string, boolean> | undefined> = Fields extends undefined
79
+ ? undefined
80
+ : {
81
+ [FieldName in keyof Fields]: Fields[FieldName] extends true
82
+ ? ResourceId
83
+ : ResourceId | undefined;
84
+ };
85
+
86
+ /**
87
+ * If KV is undefined, won't set it.
88
+ * If one of values is Zod, we'll get KV and converts it to Zod schema.
89
+ * If the value is 'raw', just returns bytes.
90
+ */
91
+ type InferKVType<KV extends Record<string, ZodType | 'raw'> | undefined> = KV extends undefined
92
+ ? undefined
93
+ : {
94
+ [FieldName in keyof KV]: KV[FieldName] extends ZodType ? z.infer<KV[FieldName]> : Uint8Array;
95
+ };
96
+
97
+ /** Infer ResourceSnapshot from ResourceShapshotSchema, S can be any ResourceSnapshotSchema. */
98
+ export type InferSnapshot<S extends ResourceSnapshotSchemaGeneric> = ResourceSnapshot<
99
+ InferDataType<S['data']>,
100
+ InferFieldsType<S['fields']>,
101
+ InferKVType<S['kv']>
102
+ >;
103
+
104
+ /** Gets a ResourceSnapshot from PlTreeEntry. */
105
+ export function makeResourceSnapshot<Schema extends ResourceSnapshotSchemaGeneric>(
106
+ res: PlTreeEntry,
107
+ schema: Schema,
108
+ ctx: ComputableCtx
109
+ ): InferSnapshot<Schema>;
110
+ export function makeResourceSnapshot<Schema extends ResourceSnapshotSchemaGeneric>(
111
+ res: PlTreeEntryAccessor | PlTreeNodeAccessor,
112
+ schema: Schema
113
+ ): InferSnapshot<Schema>;
114
+ export function makeResourceSnapshot<Schema extends ResourceSnapshotSchemaGeneric>(
115
+ res: PlTreeEntry | PlTreeEntryAccessor | PlTreeNodeAccessor,
116
+ schema: Schema,
117
+ ctx?: ComputableCtx
118
+ ): InferSnapshot<Schema> {
119
+ const node =
120
+ res instanceof PlTreeEntry
121
+ ? notEmpty(ctx).accessor(res).node()
122
+ : res instanceof PlTreeEntryAccessor
123
+ ? res.node()
124
+ : res;
125
+ const info = node.resourceInfo;
126
+ const result: Optional<Writable<ResourceSnapshotGeneric>, 'data' | 'fields' | 'kv'> = { ...info };
127
+
128
+ if (schema.data !== undefined) {
129
+ if (schema.data === 'raw') result.data = node.getData();
130
+ else result.data = schema.data.parse(node.getDataAsJson());
131
+ }
132
+
133
+ if (schema.fields !== undefined) {
134
+ const fields: Record<string, ResourceId | undefined> = {};
135
+ for (const [fieldName, required] of Object.entries(schema.fields))
136
+ fields[fieldName] = node.traverse({
137
+ field: fieldName,
138
+ errorIfFieldNotSet: required,
139
+ stableIfNotFound: !required
140
+ })?.id;
141
+ result.fields = fields;
142
+ }
143
+
144
+ if (schema.kv !== undefined) {
145
+ const kv: Record<string, unknown> = {};
146
+ for (const [fieldName, type] of Object.entries(schema.kv)) {
147
+ const value = node.getKeyValue(fieldName);
148
+
149
+ if (value === undefined) {
150
+ throw new Error(`Key not found ${fieldName}`);
151
+ } else if (type === 'raw') {
152
+ kv[fieldName] = value;
153
+ } else {
154
+ kv[fieldName] = type.parse(JSON.parse(Buffer.from(value).toString('utf-8')));
155
+ }
156
+ }
157
+ result.kv = kv;
158
+ }
159
+
160
+ return result as any;
161
+ }
162
+
163
+ /** @deprecated */
164
+ export type ResourceWithData = {
165
+ readonly id: ResourceId;
166
+ readonly type: ResourceType;
167
+ readonly fields: Map<string, ResourceId | undefined>;
168
+ readonly data?: Uint8Array;
169
+ };
170
+
171
+ /** @deprecated */
172
+ export function treeEntryToResourceWithData(
173
+ res: PlTreeEntry | ResourceWithData,
174
+ fields: string[],
175
+ ctx: ComputableCtx
176
+ ): ResourceWithData {
177
+ if (res instanceof PlTreeEntry) {
178
+ const node = ctx.accessor(res as PlTreeEntry).node();
179
+ const info = node.resourceInfo;
180
+
181
+ const fValues: [string, ResourceId | undefined][] = fields.map((name) => [
182
+ name,
183
+ node.getField(name)?.value?.id
184
+ ]);
185
+
186
+ return {
187
+ ...info,
188
+ fields: new Map(fValues),
189
+ data: node.getData() ?? new Uint8Array()
190
+ };
191
+ }
192
+
193
+ return res;
194
+ }
195
+
196
+ /** @deprecated */
197
+ export type ResourceWithMetadata = {
198
+ readonly id: ResourceId;
199
+ readonly type: ResourceType;
200
+ readonly metadata: Record<string, any>;
201
+ };
202
+
203
+ /** @deprecated */
204
+ export function treeEntryToResourceWithMetadata(
205
+ res: PlTreeEntry | ResourceWithMetadata,
206
+ mdKeys: string[],
207
+ ctx: ComputableCtx
208
+ ): ResourceWithMetadata {
209
+ if (!(res instanceof PlTreeEntry)) return res;
210
+
211
+ const node = ctx.accessor(res as PlTreeEntry).node();
212
+ const info = node.resourceInfo;
213
+ const mdEntries: [string, any][] = mdKeys.map((k) => [k, node.getKeyValue(k)]);
214
+
215
+ return {
216
+ ...info,
217
+ metadata: Object.fromEntries(mdEntries)
218
+ };
219
+ }
@@ -0,0 +1,316 @@
1
+ import { isPlTreeEntry, isPlTreeEntryAccessor, isPlTreeNodeAccessor } from './accessors';
2
+ import { PlTreeState } from './state';
3
+ import {
4
+ dField,
5
+ iField,
6
+ ResourceReady,
7
+ TestDynamicRootId1,
8
+ TestDynamicRootState1,
9
+ TestStructuralResourceState1,
10
+ TestValueResourceState1,
11
+ TestErrorResourceState2
12
+ } from './test_utils';
13
+ import { Computable } from '@milaboratories/computable';
14
+ import { NullResourceId, ResourceId } from '@milaboratories/pl-client';
15
+
16
+ function rid(id: bigint): ResourceId {
17
+ return id as ResourceId;
18
+ }
19
+
20
+ test('simple tree test 1', async () => {
21
+ const tree = new PlTreeState(TestDynamicRootId1);
22
+ const entry = tree.entry();
23
+ expect(isPlTreeEntry(entry)).toStrictEqual(true);
24
+ const c1 = Computable.make((c) => {
25
+ const eAcc = c.accessor(entry);
26
+ expect(isPlTreeEntryAccessor(eAcc)).toStrictEqual(true);
27
+ const nAcc = eAcc.node();
28
+ expect(isPlTreeNodeAccessor(nAcc)).toStrictEqual(true);
29
+ return nAcc.traverse('a', 'b')?.getDataAsString();
30
+ });
31
+
32
+ expect(c1.isChanged()).toBeTruthy();
33
+ await expect(async () => await c1.getValue()).rejects.toThrow(/not found/);
34
+ expect(c1.isChanged()).toBeFalsy();
35
+
36
+ tree.updateFromResourceData([{ ...TestDynamicRootState1, fields: [] }]);
37
+ expect(c1.isChanged()).toBeTruthy();
38
+ expect(await c1.getValue()).toBeUndefined();
39
+ expect(c1.isChanged()).toBeFalsy();
40
+
41
+ tree.updateFromResourceData([{ ...TestDynamicRootState1, fields: [dField('b')] }]);
42
+ expect(c1.isChanged()).toBeTruthy();
43
+ expect(await c1.getValue()).toBeUndefined();
44
+ expect(c1.isChanged()).toBeFalsy();
45
+
46
+ tree.updateFromResourceData([{ ...TestDynamicRootState1, fields: [dField('b'), dField('a')] }]);
47
+ expect(c1.isChanged()).toBeTruthy();
48
+ expect(await c1.getValue()).toBeUndefined();
49
+ expect(c1.isChanged()).toBeFalsy();
50
+
51
+ tree.updateFromResourceData([
52
+ { ...TestDynamicRootState1, fields: [dField('b'), dField('a', rid(rid(1n)))] },
53
+ { ...TestStructuralResourceState1, id: rid(rid(1n)), fields: [iField('b', rid(rid(2n)))] },
54
+ {
55
+ ...TestValueResourceState1,
56
+ id: rid(rid(2n)),
57
+ data: new TextEncoder().encode('Test1')
58
+ }
59
+ ]);
60
+ expect(c1.isChanged()).toBeTruthy();
61
+ expect(await c1.getValue()).toStrictEqual('Test1');
62
+ expect(c1.isChanged()).toBeFalsy();
63
+
64
+ tree.updateFromResourceData([{ ...TestDynamicRootState1, fields: [dField('a')] }]);
65
+ expect(c1.isChanged()).toBeTruthy();
66
+ expect(await c1.getValue()).toBeUndefined();
67
+ expect(c1.isChanged()).toBeFalsy();
68
+ });
69
+
70
+ test('simple tree kv test', async () => {
71
+ const tree = new PlTreeState(TestDynamicRootId1);
72
+ const c1 = Computable.make((c) =>
73
+ c.accessor(tree.entry()).node().traverse('a', 'b')?.getKeyValueAsString('thekey')
74
+ );
75
+
76
+ expect(JSON.stringify(tree.entry())).toMatch(/^"\[ENTRY:/);
77
+
78
+ expect(c1.isChanged()).toBeTruthy();
79
+ await expect(async () => await c1.getValue()).rejects.toThrow(/not found/);
80
+ expect(c1.isChanged()).toBeFalsy();
81
+
82
+ tree.updateFromResourceData([
83
+ { ...TestDynamicRootState1, fields: [dField('b'), dField('a', rid(rid(1n)))] },
84
+ { ...TestStructuralResourceState1, id: rid(rid(1n)), fields: [iField('b', rid(rid(2n)))] },
85
+ {
86
+ ...TestValueResourceState1,
87
+ id: rid(rid(2n)),
88
+ data: new TextEncoder().encode('Test1')
89
+ }
90
+ ]);
91
+
92
+ expect(c1.isChanged()).toBeTruthy();
93
+ expect(await c1.getValue()).toBeUndefined();
94
+ expect(c1.isChanged()).toBeFalsy();
95
+
96
+ tree.updateFromResourceData([
97
+ {
98
+ ...TestValueResourceState1,
99
+ id: rid(rid(2n)),
100
+ data: new TextEncoder().encode('Test1'),
101
+ kv: [{ key: 'thekey', value: Buffer.from('thevalue') }]
102
+ }
103
+ ]);
104
+
105
+ expect(c1.isChanged()).toBeTruthy();
106
+ expect(await c1.getValue()).toEqual('thevalue');
107
+ expect(c1.isChanged()).toBeFalsy();
108
+
109
+ tree.updateFromResourceData([
110
+ {
111
+ ...TestValueResourceState1,
112
+ id: rid(rid(2n)),
113
+ data: new TextEncoder().encode('Test1'),
114
+ kv: []
115
+ }
116
+ ]);
117
+
118
+ expect(c1.isChanged()).toBeTruthy();
119
+ expect(await c1.getValue()).toBeUndefined();
120
+ expect(c1.isChanged()).toBeFalsy();
121
+ });
122
+
123
+ test('partial tree update', async () => {
124
+ const tree = new PlTreeState(TestDynamicRootId1);
125
+ const c1 = Computable.make((c) =>
126
+ c
127
+ .accessor(tree.entry())
128
+ .node()
129
+ .traverse(
130
+ { field: 'a', assertFieldType: 'Dynamic' },
131
+ { field: 'b', assertFieldType: 'Dynamic' }
132
+ )
133
+ ?.getDataAsString()
134
+ );
135
+
136
+ expect(c1.isChanged()).toBeTruthy();
137
+ await expect(async () => await c1.getValue()).rejects.toThrow(/not found/);
138
+ expect(c1.isChanged()).toBeFalsy();
139
+
140
+ tree.updateFromResourceData([
141
+ { ...TestDynamicRootState1, fields: [dField('b'), dField('a', rid(1n))] },
142
+ { ...TestStructuralResourceState1, id: rid(1n), fields: [dField('b', rid(2n))] },
143
+ {
144
+ ...TestValueResourceState1,
145
+ id: rid(2n),
146
+ data: new TextEncoder().encode('Test1')
147
+ }
148
+ ]);
149
+ expect(c1.isChanged()).toBeTruthy();
150
+ expect(await c1.getValue()).toStrictEqual('Test1');
151
+ expect(c1.isChanged()).toBeFalsy();
152
+
153
+ tree.updateFromResourceData([{ ...TestStructuralResourceState1, id: rid(1n), fields: [] }]);
154
+ expect(c1.isChanged()).toBeTruthy();
155
+ expect(await c1.getValue()).toBeUndefined();
156
+ expect(c1.isChanged()).toBeFalsy();
157
+ });
158
+
159
+ test('resource error', async () => {
160
+ const tree = new PlTreeState(TestDynamicRootId1);
161
+ const c1 = Computable.make((c) =>
162
+ c.accessor(tree.entry()).node().traverse('a', 'b')?.getKeyValueAsString('thekey')
163
+ );
164
+
165
+ expect(c1.isChanged()).toBeTruthy();
166
+ await expect(async () => await c1.getValue()).rejects.toThrow(/not found/);
167
+ expect(c1.isChanged()).toBeFalsy();
168
+
169
+ tree.updateFromResourceData([
170
+ { ...TestDynamicRootState1, error: rid(7n), fields: [] },
171
+ {
172
+ ...TestErrorResourceState2,
173
+ id: rid(7n),
174
+ data: Buffer.from('"error"'),
175
+ fields: []
176
+ }
177
+ ]);
178
+
179
+ expect((await c1.getValueOrError()).type).toEqual('error');
180
+ });
181
+
182
+ test('field error', async () => {
183
+ const tree = new PlTreeState(TestDynamicRootId1);
184
+ const c1 = Computable.make((c) =>
185
+ c.accessor(tree.entry()).node().traverse('b', 'a')?.getKeyValueAsString('thekey')
186
+ );
187
+
188
+ expect(c1.isChanged()).toBeTruthy();
189
+ await expect(async () => await c1.getValue()).rejects.toThrow(/not found/);
190
+ expect(c1.isChanged()).toBeFalsy();
191
+
192
+ tree.updateFromResourceData([
193
+ {
194
+ ...TestDynamicRootState1,
195
+ fields: [dField('b', NullResourceId, rid(7n))]
196
+ },
197
+ {
198
+ ...TestErrorResourceState2,
199
+ id: rid(7n),
200
+ data: Buffer.from('"error"'),
201
+ fields: []
202
+ }
203
+ ]);
204
+
205
+ expect((await c1.getValueOrError()).type).toEqual('error');
206
+ });
207
+
208
+ test('exception - deletion of input field', () => {
209
+ const tree = new PlTreeState(TestDynamicRootId1);
210
+
211
+ tree.updateFromResourceData([
212
+ { ...TestDynamicRootState1, fields: [dField('b'), dField('a', rid(1n))] },
213
+ { ...TestStructuralResourceState1, id: rid(1n), fields: [iField('b', rid(2n))] },
214
+ {
215
+ ...TestValueResourceState1,
216
+ id: rid(2n),
217
+ data: new TextEncoder().encode('Test1')
218
+ }
219
+ ]);
220
+
221
+ expect(() =>
222
+ tree.updateFromResourceData([{ ...TestStructuralResourceState1, id: rid(1n), fields: [] }])
223
+ ).toThrow(/removal of Input field/);
224
+ });
225
+
226
+ test('exception - addition of input field', () => {
227
+ const tree = new PlTreeState(TestDynamicRootId1);
228
+
229
+ tree.updateFromResourceData([
230
+ { ...TestDynamicRootState1, fields: [dField('b'), dField('a', rid(1n))] },
231
+ {
232
+ ...TestStructuralResourceState1,
233
+ id: rid(1n),
234
+ fields: [iField('b', rid(2n))],
235
+ ...ResourceReady
236
+ },
237
+ {
238
+ ...TestValueResourceState1,
239
+ id: rid(2n),
240
+ data: new TextEncoder().encode('Test1')
241
+ }
242
+ ]);
243
+
244
+ expect(() =>
245
+ tree.updateFromResourceData([
246
+ {
247
+ ...TestStructuralResourceState1,
248
+ id: rid(1n),
249
+ fields: [iField('b', rid(2n)), iField('df')],
250
+ ...ResourceReady
251
+ }
252
+ ])
253
+ ).toThrow(/adding Input/);
254
+ });
255
+
256
+ test('exception - ready without locks 1', () => {
257
+ const tree = new PlTreeState(TestDynamicRootId1);
258
+
259
+ expect(() =>
260
+ tree.updateFromResourceData([
261
+ {
262
+ ...TestDynamicRootState1,
263
+ fields: [dField('b'), dField('a', rid(1n))]
264
+ },
265
+ {
266
+ ...TestStructuralResourceState1,
267
+ id: rid(1n),
268
+ fields: [iField('b', rid(2n))],
269
+ resourceReady: true
270
+ },
271
+ {
272
+ ...TestValueResourceState1,
273
+ id: rid(2n),
274
+ data: new TextEncoder().encode('Test1')
275
+ }
276
+ ])
277
+ ).toThrow(/ready without input or output lock/);
278
+ });
279
+
280
+ test('exception - ready without locks 2', () => {
281
+ const tree = new PlTreeState(TestDynamicRootId1);
282
+
283
+ tree.updateFromResourceData([
284
+ { ...TestDynamicRootState1, fields: [dField('b'), dField('a', rid(1n))] },
285
+ {
286
+ ...TestStructuralResourceState1,
287
+ id: rid(1n),
288
+ fields: [iField('b', rid(2n))]
289
+ },
290
+ {
291
+ ...TestValueResourceState1,
292
+ id: rid(2n),
293
+ data: new TextEncoder().encode('Test1')
294
+ }
295
+ ]);
296
+
297
+ expect(() =>
298
+ tree.updateFromResourceData([
299
+ {
300
+ ...TestDynamicRootState1,
301
+ fields: [dField('b'), dField('a', rid(1n))]
302
+ },
303
+ {
304
+ ...TestStructuralResourceState1,
305
+ id: rid(1n),
306
+ fields: [iField('b', rid(2n))],
307
+ resourceReady: true
308
+ },
309
+ {
310
+ ...TestValueResourceState1,
311
+ id: rid(2n),
312
+ data: new TextEncoder().encode('Test1')
313
+ }
314
+ ])
315
+ ).toThrow(/ready without input or output lock/);
316
+ });