@milaboratories/pl-client 3.4.2 → 3.5.0
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/core/final.cjs.map +1 -1
- package/dist/core/final.js.map +1 -1
- package/dist/core/ll_client.cjs +7 -1
- package/dist/core/ll_client.cjs.map +1 -1
- package/dist/core/ll_client.d.ts.map +1 -1
- package/dist/core/ll_client.js +7 -1
- package/dist/core/ll_client.js.map +1 -1
- package/dist/core/ll_transaction.cjs +151 -26
- package/dist/core/ll_transaction.cjs.map +1 -1
- package/dist/core/ll_transaction.d.ts +1 -0
- package/dist/core/ll_transaction.d.ts.map +1 -1
- package/dist/core/ll_transaction.js +151 -26
- package/dist/core/ll_transaction.js.map +1 -1
- package/dist/core/transaction.cjs +89 -0
- package/dist/core/transaction.cjs.map +1 -1
- package/dist/core/transaction.d.ts +47 -1
- package/dist/core/transaction.d.ts.map +1 -1
- package/dist/core/transaction.js +90 -1
- package/dist/core/transaction.js.map +1 -1
- package/dist/core/tree_filter.cjs +106 -0
- package/dist/core/tree_filter.cjs.map +1 -0
- package/dist/core/tree_filter.d.ts +85 -0
- package/dist/core/tree_filter.d.ts.map +1 -0
- package/dist/core/tree_filter.js +106 -0
- package/dist/core/tree_filter.js.map +1 -0
- package/dist/core/type_conversion.cjs +1 -0
- package/dist/core/type_conversion.cjs.map +1 -1
- package/dist/core/type_conversion.js +1 -1
- package/dist/core/type_conversion.js.map +1 -1
- package/dist/index.cjs +5 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.cjs.map +1 -1
- package/dist/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.js.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs +450 -4
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts +328 -2
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js +449 -5
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js.map +1 -1
- package/dist/proto-grpc/google/protobuf/timestamp.cjs.map +1 -1
- package/dist/proto-grpc/google/protobuf/timestamp.d.ts +9 -8
- package/dist/proto-grpc/google/protobuf/timestamp.d.ts.map +1 -1
- package/dist/proto-grpc/google/protobuf/timestamp.js.map +1 -1
- package/dist/proto-grpc/google/rpc/code.cjs.map +1 -1
- package/dist/proto-grpc/google/rpc/code.js.map +1 -1
- package/package.json +4 -4
- package/src/core/final.ts +1 -1
- package/src/core/ll_client.ts +11 -1
- package/src/core/ll_transaction.test.ts +13 -18
- package/src/core/ll_transaction.ts +237 -60
- package/src/core/transaction.test.ts +38 -0
- package/src/core/transaction.ts +136 -1
- package/src/core/tree_filter.test.ts +217 -0
- package/src/core/tree_filter.ts +182 -0
- package/src/core/type_conversion.ts +1 -1
- package/src/index.ts +1 -0
- package/src/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.ts +1 -1
- package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.ts +604 -6
- package/src/proto-grpc/google/api/http.ts +1 -1
- package/src/proto-grpc/google/protobuf/descriptor.ts +242 -12
- package/src/proto-grpc/google/protobuf/timestamp.ts +9 -8
- package/src/proto-grpc/google/protobuf/wrappers.ts +38 -4
- package/src/proto-grpc/google/rpc/code.ts +1 -1
- package/src/proto-grpc/google/rpc/error_details.ts +5 -5
- package/src/proto-grpc/google/rpc/http.ts +1 -1
- package/src/proto-grpc/google/rpc/status.ts +1 -1
|
@@ -24,37 +24,125 @@ export type OneOfKind<T extends { oneofKind: unknown }, Kind extends T["oneofKin
|
|
|
24
24
|
{ oneofKind: Kind }
|
|
25
25
|
>;
|
|
26
26
|
|
|
27
|
-
interface SingleResponseHandler
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
resolve: (v:
|
|
27
|
+
interface SingleResponseHandler {
|
|
28
|
+
mode: "single";
|
|
29
|
+
kind: ServerMessageResponse["oneofKind"];
|
|
30
|
+
resolve: (v: ServerMessageResponse) => void;
|
|
31
31
|
reject: (e: Error) => void;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
interface MultiResponseHandler
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
resolve: (v:
|
|
34
|
+
interface MultiResponseHandler {
|
|
35
|
+
mode: "multiBuffered";
|
|
36
|
+
kind: ServerMessageResponse["oneofKind"];
|
|
37
|
+
resolve: (v: ServerMessageResponse[]) => void;
|
|
38
38
|
reject: (e: Error) => void;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
interface MultiStreamResponseHandler {
|
|
42
|
+
mode: "multiStream";
|
|
43
|
+
kind: ServerMessageResponse["oneofKind"];
|
|
44
|
+
stream: AsyncMessageStream<ServerMessageResponse>;
|
|
45
|
+
}
|
|
42
46
|
|
|
43
|
-
type
|
|
47
|
+
type AnySingleResponseHandler = SingleResponseHandler;
|
|
48
|
+
type AnyMultiStreamResponseHandler = MultiStreamResponseHandler;
|
|
44
49
|
|
|
45
50
|
type AnyResponseHandler =
|
|
46
|
-
|
|
|
47
|
-
| MultiResponseHandler
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
| AnySingleResponseHandler
|
|
52
|
+
| MultiResponseHandler
|
|
53
|
+
| AnyMultiStreamResponseHandler;
|
|
54
|
+
|
|
55
|
+
// Implements both AsyncIterable and AsyncIterator: `[Symbol.asyncIterator]()` returns
|
|
56
|
+
// `this`, so multiple `for await` loops over the same stream share state instead of
|
|
57
|
+
// creating independent iterators that would race on `waitingResolve`/`waitingReject`.
|
|
58
|
+
// Only one active consumer is supported; concurrent `next()` calls are not safe.
|
|
59
|
+
class AsyncMessageStream<T> implements AsyncIterable<T>, AsyncIterator<T> {
|
|
60
|
+
private readonly queue = new Denque<T>();
|
|
61
|
+
private done = false;
|
|
62
|
+
private cancelled = false;
|
|
63
|
+
private failure?: Error;
|
|
64
|
+
private waitingResolve?: (value: IteratorResult<T>) => void;
|
|
65
|
+
private waitingReject?: (reason?: unknown) => void;
|
|
66
|
+
|
|
67
|
+
public push(value: T): void {
|
|
68
|
+
if (this.done || this.cancelled) return;
|
|
69
|
+
if (this.waitingResolve) {
|
|
70
|
+
const resolve = this.waitingResolve;
|
|
71
|
+
this.waitingResolve = undefined;
|
|
72
|
+
this.waitingReject = undefined;
|
|
73
|
+
resolve({ value, done: false });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.queue.push(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public end(): void {
|
|
80
|
+
if (this.done) return;
|
|
81
|
+
this.done = true;
|
|
82
|
+
if (this.waitingResolve) {
|
|
83
|
+
const resolve = this.waitingResolve;
|
|
84
|
+
this.waitingResolve = undefined;
|
|
85
|
+
this.waitingReject = undefined;
|
|
86
|
+
resolve({ value: undefined, done: true });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Fail-fast: any frames still buffered in `this.queue` are intentionally dropped.
|
|
91
|
+
// `next()` checks `this.failure` before draining the queue, so consumers see the
|
|
92
|
+
// error immediately. This differs from `end()`, which drains buffered frames first.
|
|
93
|
+
// Rationale: when the upstream stream errors, the in-flight response is treated as
|
|
94
|
+
// corrupt and partial frames must not be surfaced.
|
|
95
|
+
public fail(error: Error): void {
|
|
96
|
+
if (this.done) return;
|
|
97
|
+
this.failure = error;
|
|
98
|
+
this.done = true;
|
|
99
|
+
if (this.waitingReject) {
|
|
100
|
+
const reject = this.waitingReject;
|
|
101
|
+
this.waitingResolve = undefined;
|
|
102
|
+
this.waitingReject = undefined;
|
|
103
|
+
reject(error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public cancel(): void {
|
|
108
|
+
this.cancelled = true;
|
|
109
|
+
this.done = true;
|
|
110
|
+
this.queue.clear();
|
|
111
|
+
if (this.waitingResolve) {
|
|
112
|
+
const resolve = this.waitingResolve;
|
|
113
|
+
this.waitingResolve = undefined;
|
|
114
|
+
this.waitingReject = undefined;
|
|
115
|
+
resolve({ value: undefined, done: true });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public async next(): Promise<IteratorResult<T>> {
|
|
120
|
+
if (this.failure) throw this.failure;
|
|
121
|
+
|
|
122
|
+
const value = this.queue.shift();
|
|
123
|
+
if (value !== undefined) return { value, done: false };
|
|
124
|
+
|
|
125
|
+
if (this.done) return { value: undefined, done: true };
|
|
126
|
+
|
|
127
|
+
return await new Promise<IteratorResult<T>>((resolve, reject) => {
|
|
128
|
+
this.waitingResolve = resolve;
|
|
129
|
+
this.waitingReject = reject;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public async return(): Promise<IteratorResult<T>> {
|
|
134
|
+
this.cancel();
|
|
135
|
+
return { value: undefined, done: true };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public async throw(error?: unknown): Promise<IteratorResult<T>> {
|
|
139
|
+
this.cancel();
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
[Symbol.asyncIterator](): this {
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
58
146
|
}
|
|
59
147
|
|
|
60
148
|
function isRecoverable(status: Status): boolean {
|
|
@@ -127,6 +215,7 @@ export class LLPlTransaction {
|
|
|
127
215
|
// to the specific request on which it happened
|
|
128
216
|
let currentHandler: AnyResponseHandler | undefined = undefined;
|
|
129
217
|
let responseAggregator: ServerMessageResponse[] | undefined = undefined;
|
|
218
|
+
let currentMultiIdx = 0;
|
|
130
219
|
try {
|
|
131
220
|
for await (const message of this.stream.responses) {
|
|
132
221
|
if (currentHandler === undefined) {
|
|
@@ -139,9 +228,8 @@ export class LLPlTransaction {
|
|
|
139
228
|
break;
|
|
140
229
|
}
|
|
141
230
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
231
|
+
if (currentHandler.mode === "multiBuffered") responseAggregator = [];
|
|
232
|
+
currentMultiIdx = 0;
|
|
145
233
|
expectedId++;
|
|
146
234
|
}
|
|
147
235
|
|
|
@@ -157,11 +245,14 @@ export class LLPlTransaction {
|
|
|
157
245
|
const status = message.error;
|
|
158
246
|
|
|
159
247
|
if (isRecoverable(status)) {
|
|
160
|
-
|
|
161
|
-
new
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
248
|
+
const recoverableError = new RethrowError(() => {
|
|
249
|
+
throw new RecoverablePlError(status);
|
|
250
|
+
});
|
|
251
|
+
if (currentHandler.mode === "single" || currentHandler.mode === "multiBuffered") {
|
|
252
|
+
currentHandler.reject(recoverableError);
|
|
253
|
+
} else {
|
|
254
|
+
currentHandler.stream.fail(recoverableError);
|
|
255
|
+
}
|
|
165
256
|
currentHandler = undefined;
|
|
166
257
|
|
|
167
258
|
if (message.multiMessage !== undefined && !message.multiMessage.isLast) {
|
|
@@ -174,9 +265,17 @@ export class LLPlTransaction {
|
|
|
174
265
|
// We can continue to work after recoverable errors
|
|
175
266
|
continue;
|
|
176
267
|
} else {
|
|
177
|
-
this.assignErrorFactoryIfNotSet(
|
|
178
|
-
|
|
179
|
-
|
|
268
|
+
this.assignErrorFactoryIfNotSet(
|
|
269
|
+
() => {
|
|
270
|
+
throw new UnrecoverablePlError(status);
|
|
271
|
+
},
|
|
272
|
+
currentHandler.mode === "single" || currentHandler.mode === "multiBuffered"
|
|
273
|
+
? currentHandler.reject
|
|
274
|
+
: undefined,
|
|
275
|
+
);
|
|
276
|
+
if (currentHandler.mode === "multiStream") {
|
|
277
|
+
currentHandler.stream.fail(new UnrecoverablePlError(status));
|
|
278
|
+
}
|
|
180
279
|
currentHandler = undefined;
|
|
181
280
|
|
|
182
281
|
// In case of unrecoverable errors we close the transaction
|
|
@@ -190,20 +289,38 @@ export class LLPlTransaction {
|
|
|
190
289
|
) {
|
|
191
290
|
const errorMessage = `inconsistent request response types: ${currentHandler.kind} !== ${message.response.oneofKind}`;
|
|
192
291
|
|
|
193
|
-
this.assignErrorFactoryIfNotSet(
|
|
194
|
-
|
|
195
|
-
|
|
292
|
+
this.assignErrorFactoryIfNotSet(
|
|
293
|
+
() => {
|
|
294
|
+
throw new Error(errorMessage);
|
|
295
|
+
},
|
|
296
|
+
currentHandler.mode === "single" || currentHandler.mode === "multiBuffered"
|
|
297
|
+
? currentHandler.reject
|
|
298
|
+
: undefined,
|
|
299
|
+
);
|
|
300
|
+
if (currentHandler.mode === "multiStream") {
|
|
301
|
+
currentHandler.stream.fail(new Error(errorMessage));
|
|
302
|
+
}
|
|
196
303
|
currentHandler = undefined;
|
|
197
304
|
|
|
198
305
|
break;
|
|
199
306
|
}
|
|
200
307
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
308
|
+
const expectMultiResponse =
|
|
309
|
+
currentHandler.mode === "multiBuffered" || currentHandler.mode === "multiStream";
|
|
310
|
+
if (expectMultiResponse !== (message.multiMessage !== undefined)) {
|
|
311
|
+
const errorMessage = `inconsistent multi state: ${expectMultiResponse} !== ${message.multiMessage !== undefined}`;
|
|
312
|
+
|
|
313
|
+
this.assignErrorFactoryIfNotSet(
|
|
314
|
+
() => {
|
|
315
|
+
throw new Error(errorMessage);
|
|
316
|
+
},
|
|
317
|
+
currentHandler.mode === "single" || currentHandler.mode === "multiBuffered"
|
|
318
|
+
? currentHandler.reject
|
|
319
|
+
: undefined,
|
|
320
|
+
);
|
|
321
|
+
if (currentHandler.mode === "multiStream") {
|
|
322
|
+
currentHandler.stream.fail(new Error(errorMessage));
|
|
323
|
+
}
|
|
207
324
|
currentHandler = undefined;
|
|
208
325
|
|
|
209
326
|
break;
|
|
@@ -213,23 +330,38 @@ export class LLPlTransaction {
|
|
|
213
330
|
|
|
214
331
|
if (message.multiMessage !== undefined) {
|
|
215
332
|
if (!message.multiMessage.isEmpty) {
|
|
216
|
-
if (message.multiMessage.id !==
|
|
217
|
-
const errorMessage = `inconsistent multi id: ${message.multiMessage.id} !== ${
|
|
218
|
-
|
|
219
|
-
this.assignErrorFactoryIfNotSet(
|
|
220
|
-
|
|
221
|
-
|
|
333
|
+
if (message.multiMessage.id !== currentMultiIdx + 1) {
|
|
334
|
+
const errorMessage = `inconsistent multi id: ${message.multiMessage.id} !== ${currentMultiIdx + 1}`;
|
|
335
|
+
|
|
336
|
+
this.assignErrorFactoryIfNotSet(
|
|
337
|
+
() => {
|
|
338
|
+
throw new Error(errorMessage);
|
|
339
|
+
},
|
|
340
|
+
currentHandler.mode === "multiBuffered" ? currentHandler.reject : undefined,
|
|
341
|
+
);
|
|
342
|
+
if (currentHandler.mode === "multiStream") {
|
|
343
|
+
currentHandler.stream.fail(new Error(errorMessage));
|
|
344
|
+
}
|
|
222
345
|
currentHandler = undefined;
|
|
223
346
|
|
|
224
347
|
break;
|
|
225
348
|
}
|
|
226
349
|
|
|
227
|
-
|
|
350
|
+
currentMultiIdx++;
|
|
351
|
+
if (currentHandler.mode === "multiBuffered") {
|
|
352
|
+
responseAggregator!.push(message.response);
|
|
353
|
+
} else if (currentHandler.mode === "multiStream") {
|
|
354
|
+
currentHandler.stream.push(message.response);
|
|
355
|
+
}
|
|
228
356
|
}
|
|
229
357
|
|
|
230
358
|
if (message.multiMessage.isLast) {
|
|
231
|
-
(currentHandler
|
|
232
|
-
|
|
359
|
+
if (currentHandler.mode === "multiBuffered") {
|
|
360
|
+
currentHandler.resolve(responseAggregator!);
|
|
361
|
+
responseAggregator = undefined;
|
|
362
|
+
} else if (currentHandler.mode === "multiStream") {
|
|
363
|
+
currentHandler.stream.end();
|
|
364
|
+
}
|
|
233
365
|
currentHandler = undefined;
|
|
234
366
|
}
|
|
235
367
|
} else {
|
|
@@ -245,9 +377,15 @@ export class LLPlTransaction {
|
|
|
245
377
|
}
|
|
246
378
|
}
|
|
247
379
|
} catch (e: any) {
|
|
248
|
-
return this.assignErrorFactoryIfNotSet(
|
|
249
|
-
|
|
250
|
-
|
|
380
|
+
return this.assignErrorFactoryIfNotSet(
|
|
381
|
+
() => {
|
|
382
|
+
rethrowMeaningfulError(e, true);
|
|
383
|
+
},
|
|
384
|
+
currentHandler &&
|
|
385
|
+
(currentHandler.mode === "single" || currentHandler.mode === "multiBuffered")
|
|
386
|
+
? currentHandler.reject
|
|
387
|
+
: undefined,
|
|
388
|
+
);
|
|
251
389
|
} finally {
|
|
252
390
|
await this.close();
|
|
253
391
|
}
|
|
@@ -265,8 +403,14 @@ export class LLPlTransaction {
|
|
|
265
403
|
while (true) {
|
|
266
404
|
const handler = this.responseHandlerQueue.shift();
|
|
267
405
|
if (!handler) break;
|
|
268
|
-
|
|
269
|
-
|
|
406
|
+
const noReplyError = this.errorFactory
|
|
407
|
+
? new RethrowError(this.errorFactory)
|
|
408
|
+
: new Error("no reply");
|
|
409
|
+
if (handler.mode === "single" || handler.mode === "multiBuffered") {
|
|
410
|
+
handler.reject(noReplyError);
|
|
411
|
+
} else {
|
|
412
|
+
handler.stream.fail(noReplyError);
|
|
413
|
+
}
|
|
270
414
|
}
|
|
271
415
|
|
|
272
416
|
// closing outgoing stream
|
|
@@ -311,10 +455,24 @@ export class LLPlTransaction {
|
|
|
311
455
|
|
|
312
456
|
// Note: Promise synchronously executes a callback passed to a constructor
|
|
313
457
|
const result = StatefulPromise.fromDeferredReject(
|
|
314
|
-
new Promise<
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
)
|
|
458
|
+
new Promise<
|
|
459
|
+
OneOfKind<ServerMessageResponse, Kind> | OneOfKind<ServerMessageResponse, Kind>[]
|
|
460
|
+
>((resolve, reject) => {
|
|
461
|
+
if (expectMultiResponse) {
|
|
462
|
+
this.responseHandlerQueue.push({
|
|
463
|
+
mode: "multiBuffered",
|
|
464
|
+
kind: r.oneofKind,
|
|
465
|
+
resolve: resolve as (v: ServerMessageResponse[]) => void,
|
|
466
|
+
reject,
|
|
467
|
+
});
|
|
468
|
+
} else {
|
|
469
|
+
this.responseHandlerQueue.push({
|
|
470
|
+
mode: "single",
|
|
471
|
+
kind: r.oneofKind,
|
|
472
|
+
resolve: resolve as (v: ServerMessageResponse) => void,
|
|
473
|
+
reject,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
318
476
|
}),
|
|
319
477
|
);
|
|
320
478
|
|
|
@@ -333,6 +491,25 @@ export class LLPlTransaction {
|
|
|
333
491
|
}
|
|
334
492
|
}
|
|
335
493
|
|
|
494
|
+
public sendStream<Kind extends ClientMessageRequest["oneofKind"]>(
|
|
495
|
+
r: OneOfKind<ClientMessageRequest, Kind>,
|
|
496
|
+
): AsyncIterable<OneOfKind<ServerMessageResponse, Kind>> {
|
|
497
|
+
if (this.errorFactory) throw new RethrowError(this.errorFactory);
|
|
498
|
+
if (this.closed) throw new Error("Transaction already closed");
|
|
499
|
+
|
|
500
|
+
const stream = new AsyncMessageStream<ServerMessageResponse>();
|
|
501
|
+
this.responseHandlerQueue.push({ mode: "multiStream", kind: r.oneofKind, stream });
|
|
502
|
+
|
|
503
|
+
void this.stream.requests
|
|
504
|
+
.send({ requestId: this.requestIdxCounter++, request: r })
|
|
505
|
+
.catch((e: unknown) => {
|
|
506
|
+
if (e instanceof Error) stream.fail(e);
|
|
507
|
+
else stream.fail(new Error("Error while sending request", { cause: e }));
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
return stream as AsyncIterable<OneOfKind<ServerMessageResponse, Kind>>;
|
|
511
|
+
}
|
|
512
|
+
|
|
336
513
|
private _completed = false;
|
|
337
514
|
|
|
338
515
|
/** Safe to call multiple times */
|
|
@@ -177,3 +177,41 @@ test("handle KV storage 2", async () => {
|
|
|
177
177
|
});
|
|
178
178
|
});
|
|
179
179
|
});
|
|
180
|
+
|
|
181
|
+
test("resourceTree accepts mixed seed types and returns kv entries", async () => {
|
|
182
|
+
await withTempRoot(async (pl) => {
|
|
183
|
+
const [rootId, childId] = await pl.withWriteTx("resourceTreeSetup", async (tx) => {
|
|
184
|
+
const root = tx.createStruct(StructTestResource);
|
|
185
|
+
const child = tx.createStruct(StructTestResource);
|
|
186
|
+
|
|
187
|
+
tx.createField(field(root, "child"), "Dynamic", child);
|
|
188
|
+
tx.createField(field(tx.clientRoot, "treeRoot"), "Dynamic", root);
|
|
189
|
+
tx.setKValue(root, "root-k", "root-v");
|
|
190
|
+
|
|
191
|
+
await tx.commit();
|
|
192
|
+
return [await root.globalId, await child.globalId];
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await pl.withReadTx("resourceTreeRead", async (tx) => {
|
|
196
|
+
const rootData = await tx.getResourceData(rootId, true);
|
|
197
|
+
const visitedIds = new Set<string>();
|
|
198
|
+
const iter = tx.resourceTree([rootData, childId], { includeKv: true });
|
|
199
|
+
|
|
200
|
+
for await (const item of iter) {
|
|
201
|
+
visitedIds.add(item.id);
|
|
202
|
+
expect(typeof item.traverseWasStopped).toBe("boolean");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
expect(visitedIds.has(rootId)).toBe(true);
|
|
206
|
+
expect(visitedIds.has(childId)).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("resourceTree empty seeds fails", async () => {
|
|
212
|
+
await withTempRoot(async (pl) => {
|
|
213
|
+
await pl.withReadTx("resourceTreeEmptySeeds", async (tx) => {
|
|
214
|
+
expect(() => tx.resourceTree([])).toThrow("resourceTree: at least one seed must be provided");
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
package/src/core/transaction.ts
CHANGED
|
@@ -30,10 +30,19 @@ import type {
|
|
|
30
30
|
OneOfKind,
|
|
31
31
|
ServerMessageResponse,
|
|
32
32
|
} from "./ll_transaction";
|
|
33
|
+
import type {
|
|
34
|
+
ResourceAPI_Tree_Filter,
|
|
35
|
+
ResourceAPI_Tree_SeedResource,
|
|
36
|
+
} from "../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api";
|
|
33
37
|
import { TxAPI_Open_Request_WritableTx } from "../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api";
|
|
34
38
|
import type { NonUndefined } from "utility-types";
|
|
35
39
|
import { toBytes } from "../util/util";
|
|
36
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
fieldTypeToProto,
|
|
42
|
+
protoToField,
|
|
43
|
+
protoToResource,
|
|
44
|
+
resourceIsDeleted,
|
|
45
|
+
} from "./type_conversion";
|
|
37
46
|
import {
|
|
38
47
|
canonicalJsonBytes,
|
|
39
48
|
canonicalJsonGzBytes,
|
|
@@ -74,6 +83,19 @@ export interface KeyValueString {
|
|
|
74
83
|
value: string;
|
|
75
84
|
}
|
|
76
85
|
|
|
86
|
+
export type ResourceTreeItem = ResourceData & { kv: KeyValue[]; traverseWasStopped: boolean };
|
|
87
|
+
|
|
88
|
+
/** A single frame from the resourceTree() stream.
|
|
89
|
+
*
|
|
90
|
+
* When the server advertises `treeStopMarker:v1`, a stop-matched node is emitted
|
|
91
|
+
* as a lightweight marker instead of a full resource payload. Callers must
|
|
92
|
+
* narrow on `frameKind` before accessing type-specific fields.
|
|
93
|
+
* Note: `frameKind` is distinct from `ResourceData.kind` (which is the resource kind).
|
|
94
|
+
*/
|
|
95
|
+
export type ResourceTreeFrame =
|
|
96
|
+
| (ResourceTreeItem & { frameKind: "resource" })
|
|
97
|
+
| { frameKind: "stopMarker"; id: SignedResourceId; traverseWasStopped: true };
|
|
98
|
+
|
|
77
99
|
interface _FieldId<RId> {
|
|
78
100
|
/** Parent resource id */
|
|
79
101
|
resourceId: RId;
|
|
@@ -1057,6 +1079,119 @@ export class PlTransaction {
|
|
|
1057
1079
|
await this.ll.await();
|
|
1058
1080
|
}
|
|
1059
1081
|
|
|
1082
|
+
//
|
|
1083
|
+
// Tree streaming
|
|
1084
|
+
//
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Perform a single `ResourceTree` walk and yield each visited resource.
|
|
1088
|
+
*
|
|
1089
|
+
* Items are yielded incrementally as stream frames arrive from server.
|
|
1090
|
+
*
|
|
1091
|
+
* The low-level transaction stream is still ordered, so each active request
|
|
1092
|
+
* must be consumed to completion (or closed via iterator `return()`/`throw()`)
|
|
1093
|
+
* before later responses can be dispatched.
|
|
1094
|
+
*
|
|
1095
|
+
* @param seeds - Roots for the traversal. May include both {@link ResourceData}
|
|
1096
|
+
* and {@link SignedResourceId} values in the same array.
|
|
1097
|
+
* @param opts.fieldFilter - Per-field predicate; matching field edges are not descended.
|
|
1098
|
+
* This wire field accepts only one filter. Compose multiple rules via
|
|
1099
|
+
* `treeFilter.and(...)` / `treeFilter.or(...)`.
|
|
1100
|
+
* @param opts.traverseStopRules - Per-resource predicate; matching resources are returned
|
|
1101
|
+
* with `traverseWasStopped = true` and their children are not visited.
|
|
1102
|
+
* This wire field accepts only one filter. Compose multiple rules via
|
|
1103
|
+
* `treeFilter.and(...)` / `treeFilter.or(...)`.
|
|
1104
|
+
* @param opts.includeKv - When true, each yielded item includes KV entries.
|
|
1105
|
+
* @param opts.maxDepth - Optional depth cap.
|
|
1106
|
+
*/
|
|
1107
|
+
public resourceTree(
|
|
1108
|
+
seeds: (ResourceData | SignedResourceId)[],
|
|
1109
|
+
opts?: {
|
|
1110
|
+
fieldFilter?: ResourceAPI_Tree_Filter;
|
|
1111
|
+
traverseStopRules?: ResourceAPI_Tree_Filter;
|
|
1112
|
+
includeKv?: boolean;
|
|
1113
|
+
maxDepth?: number;
|
|
1114
|
+
},
|
|
1115
|
+
): AsyncIterable<ResourceTreeFrame> {
|
|
1116
|
+
if (seeds.length === 0) {
|
|
1117
|
+
throw new Error("resourceTree: at least one seed must be provided");
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const seedProtos = seeds.map((seed): ResourceAPI_Tree_SeedResource => {
|
|
1121
|
+
const signedId = typeof seed === "string" ? seed : seed.id;
|
|
1122
|
+
const { globalId, signature } = parseSignedResourceId(signedId);
|
|
1123
|
+
return { resourceId: globalId, resourceSignature: signature };
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
const firstSeed = seedProtos[0];
|
|
1127
|
+
|
|
1128
|
+
const responseStream = this.ll.sendStream({
|
|
1129
|
+
oneofKind: "resourceTree",
|
|
1130
|
+
resourceTree: {
|
|
1131
|
+
// Keep compatibility with request shape that requires non-optional fields.
|
|
1132
|
+
resourceId: firstSeed?.resourceId ?? 0n,
|
|
1133
|
+
resourceSignature: firstSeed?.resourceSignature ?? new Uint8Array(0),
|
|
1134
|
+
seeds: seedProtos,
|
|
1135
|
+
fieldFilter: opts?.fieldFilter,
|
|
1136
|
+
traverseStopRules: opts?.traverseStopRules,
|
|
1137
|
+
includeKv: opts?.includeKv ?? false,
|
|
1138
|
+
maxDepth: opts?.maxDepth,
|
|
1139
|
+
},
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
return {
|
|
1143
|
+
[Symbol.asyncIterator](): AsyncIterator<ResourceTreeFrame> {
|
|
1144
|
+
const iterator = responseStream[Symbol.asyncIterator]();
|
|
1145
|
+
|
|
1146
|
+
return {
|
|
1147
|
+
async next(): Promise<IteratorResult<ResourceTreeFrame>> {
|
|
1148
|
+
while (true) {
|
|
1149
|
+
const item = await iterator.next();
|
|
1150
|
+
if (item.done) return { value: undefined, done: true };
|
|
1151
|
+
|
|
1152
|
+
const frame = item.value.resourceTree;
|
|
1153
|
+
if (frame === undefined) continue;
|
|
1154
|
+
|
|
1155
|
+
if (frame.resource && resourceIsDeleted(frame.resource)) {
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (!frame.resource) {
|
|
1160
|
+
// Stop-marker frame: no resource body; server advertised treeStopMarker:v1.
|
|
1161
|
+
const id = createSignedResourceId(
|
|
1162
|
+
frame.resourceId,
|
|
1163
|
+
toResourceSignature(frame.resourceSignature),
|
|
1164
|
+
);
|
|
1165
|
+
return {
|
|
1166
|
+
value: { frameKind: "stopMarker", id, traverseWasStopped: true },
|
|
1167
|
+
done: false,
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
return {
|
|
1172
|
+
value: {
|
|
1173
|
+
frameKind: "resource",
|
|
1174
|
+
...protoToResource(frame.resource),
|
|
1175
|
+
kv: frame.kv,
|
|
1176
|
+
traverseWasStopped: frame.traverseWasStopped,
|
|
1177
|
+
},
|
|
1178
|
+
done: false,
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
},
|
|
1182
|
+
async return(): Promise<IteratorResult<ResourceTreeFrame>> {
|
|
1183
|
+
if (iterator.return) await iterator.return();
|
|
1184
|
+
return { value: undefined, done: true };
|
|
1185
|
+
},
|
|
1186
|
+
async throw(error?: unknown): Promise<IteratorResult<ResourceTreeFrame>> {
|
|
1187
|
+
if (iterator.throw) await iterator.throw(error);
|
|
1188
|
+
throw error;
|
|
1189
|
+
},
|
|
1190
|
+
};
|
|
1191
|
+
},
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1060
1195
|
//
|
|
1061
1196
|
// Helpers
|
|
1062
1197
|
//
|