@milaboratories/pl-client 3.4.1 → 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.
Files changed (67) hide show
  1. package/dist/core/final.cjs.map +1 -1
  2. package/dist/core/final.js.map +1 -1
  3. package/dist/core/ll_client.cjs +7 -1
  4. package/dist/core/ll_client.cjs.map +1 -1
  5. package/dist/core/ll_client.d.ts.map +1 -1
  6. package/dist/core/ll_client.js +7 -1
  7. package/dist/core/ll_client.js.map +1 -1
  8. package/dist/core/ll_transaction.cjs +151 -26
  9. package/dist/core/ll_transaction.cjs.map +1 -1
  10. package/dist/core/ll_transaction.d.ts +1 -0
  11. package/dist/core/ll_transaction.d.ts.map +1 -1
  12. package/dist/core/ll_transaction.js +151 -26
  13. package/dist/core/ll_transaction.js.map +1 -1
  14. package/dist/core/transaction.cjs +89 -0
  15. package/dist/core/transaction.cjs.map +1 -1
  16. package/dist/core/transaction.d.ts +47 -1
  17. package/dist/core/transaction.d.ts.map +1 -1
  18. package/dist/core/transaction.js +90 -1
  19. package/dist/core/transaction.js.map +1 -1
  20. package/dist/core/tree_filter.cjs +106 -0
  21. package/dist/core/tree_filter.cjs.map +1 -0
  22. package/dist/core/tree_filter.d.ts +85 -0
  23. package/dist/core/tree_filter.d.ts.map +1 -0
  24. package/dist/core/tree_filter.js +106 -0
  25. package/dist/core/tree_filter.js.map +1 -0
  26. package/dist/core/type_conversion.cjs +1 -0
  27. package/dist/core/type_conversion.cjs.map +1 -1
  28. package/dist/core/type_conversion.js +1 -1
  29. package/dist/core/type_conversion.js.map +1 -1
  30. package/dist/index.cjs +5 -0
  31. package/dist/index.d.ts +4 -2
  32. package/dist/index.js +3 -1
  33. package/dist/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.cjs.map +1 -1
  34. package/dist/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.js.map +1 -1
  35. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs +450 -4
  36. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs.map +1 -1
  37. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts +328 -2
  38. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts.map +1 -1
  39. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js +449 -5
  40. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js.map +1 -1
  41. package/dist/proto-grpc/google/protobuf/timestamp.cjs.map +1 -1
  42. package/dist/proto-grpc/google/protobuf/timestamp.d.ts +9 -8
  43. package/dist/proto-grpc/google/protobuf/timestamp.d.ts.map +1 -1
  44. package/dist/proto-grpc/google/protobuf/timestamp.js.map +1 -1
  45. package/dist/proto-grpc/google/rpc/code.cjs.map +1 -1
  46. package/dist/proto-grpc/google/rpc/code.js.map +1 -1
  47. package/package.json +5 -5
  48. package/src/core/final.ts +1 -1
  49. package/src/core/ll_client.ts +11 -1
  50. package/src/core/ll_transaction.test.ts +13 -18
  51. package/src/core/ll_transaction.ts +237 -60
  52. package/src/core/transaction.test.ts +38 -0
  53. package/src/core/transaction.ts +136 -1
  54. package/src/core/tree_filter.test.ts +217 -0
  55. package/src/core/tree_filter.ts +182 -0
  56. package/src/core/type_conversion.ts +1 -1
  57. package/src/index.ts +1 -0
  58. package/src/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.ts +1 -1
  59. package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.ts +604 -6
  60. package/src/proto-grpc/google/api/http.ts +1 -1
  61. package/src/proto-grpc/google/protobuf/descriptor.ts +242 -12
  62. package/src/proto-grpc/google/protobuf/timestamp.ts +9 -8
  63. package/src/proto-grpc/google/protobuf/wrappers.ts +38 -4
  64. package/src/proto-grpc/google/rpc/code.ts +1 -1
  65. package/src/proto-grpc/google/rpc/error_details.ts +5 -5
  66. package/src/proto-grpc/google/rpc/http.ts +1 -1
  67. 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<Kind extends ServerMessageResponse["oneofKind"]> {
28
- kind: Kind;
29
- expectMultiResponse: false;
30
- resolve: (v: OneOfKind<ServerMessageResponse, Kind>) => void;
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<Kind extends ServerMessageResponse["oneofKind"]> {
35
- kind: Kind;
36
- expectMultiResponse: true;
37
- resolve: (v: OneOfKind<ServerMessageResponse, Kind>[]) => void;
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
- type AnySingleResponseHandler = SingleResponseHandler<ServerMessageResponse["oneofKind"]>;
41
+ interface MultiStreamResponseHandler {
42
+ mode: "multiStream";
43
+ kind: ServerMessageResponse["oneofKind"];
44
+ stream: AsyncMessageStream<ServerMessageResponse>;
45
+ }
42
46
 
43
- type AnyMultiResponseHandler = MultiResponseHandler<ServerMessageResponse["oneofKind"]>;
47
+ type AnySingleResponseHandler = SingleResponseHandler;
48
+ type AnyMultiStreamResponseHandler = MultiStreamResponseHandler;
44
49
 
45
50
  type AnyResponseHandler =
46
- | SingleResponseHandler<ServerMessageResponse["oneofKind"]>
47
- | MultiResponseHandler<ServerMessageResponse["oneofKind"]>;
48
-
49
- function createResponseHandler<Kind extends ServerMessageResponse["oneofKind"]>(
50
- kind: Kind,
51
- expectMultiResponse: boolean,
52
- resolve:
53
- | ((v: OneOfKind<ServerMessageResponse, Kind>) => void)
54
- | ((v: OneOfKind<ServerMessageResponse, Kind>[]) => void),
55
- reject: (e: Error) => void,
56
- ): AnyResponseHandler {
57
- return { kind, expectMultiResponse, resolve, reject } as AnyResponseHandler;
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
- // allocating response aggregator array
143
- if (currentHandler.expectMultiResponse) responseAggregator = [];
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
- currentHandler.reject(
161
- new RethrowError(() => {
162
- throw new RecoverablePlError(status);
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
- throw new UnrecoverablePlError(status);
179
- }, currentHandler.reject);
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
- throw new Error(errorMessage);
195
- }, currentHandler.reject);
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
- if (currentHandler.expectMultiResponse !== (message.multiMessage !== undefined)) {
202
- const errorMessage = `inconsistent multi state: ${currentHandler.expectMultiResponse} !== ${message.multiMessage !== undefined}`;
203
-
204
- this.assignErrorFactoryIfNotSet(() => {
205
- throw new Error(errorMessage);
206
- }, currentHandler.reject);
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 !== responseAggregator!.length + 1) {
217
- const errorMessage = `inconsistent multi id: ${message.multiMessage.id} !== ${responseAggregator!.length + 1}`;
218
-
219
- this.assignErrorFactoryIfNotSet(() => {
220
- throw new Error(errorMessage);
221
- }, currentHandler.reject);
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
- responseAggregator!.push(message.response);
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 as AnyMultiResponseHandler).resolve(responseAggregator!);
232
- responseAggregator = undefined;
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
- rethrowMeaningfulError(e, true);
250
- }, currentHandler?.reject);
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
- if (this.errorFactory) handler.reject(new RethrowError(this.errorFactory));
269
- else handler.reject(new Error("no reply"));
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<OneOfKind<ServerMessageResponse, Kind>>((resolve, reject) => {
315
- this.responseHandlerQueue.push(
316
- createResponseHandler(r.oneofKind, expectMultiResponse, resolve, reject),
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
+ });
@@ -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 { fieldTypeToProto, protoToField, protoToResource } from "./type_conversion";
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
  //