@milaboratories/pl-client 2.16.1 → 2.16.3

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 (58) hide show
  1. package/dist/__external/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.52.4_tslib@2.7.0_typescript@5.6.3/__external/tslib/tslib.es6.cjs +61 -0
  2. package/dist/__external/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.52.4_tslib@2.7.0_typescript@5.6.3/__external/tslib/tslib.es6.cjs.map +1 -0
  3. package/dist/__external/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.52.4_tslib@2.7.0_typescript@5.6.3/__external/tslib/tslib.es6.js +58 -0
  4. package/dist/__external/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.52.4_tslib@2.7.0_typescript@5.6.3/__external/tslib/tslib.es6.js.map +1 -0
  5. package/dist/core/PromiseTracker.cjs +39 -0
  6. package/dist/core/PromiseTracker.cjs.map +1 -0
  7. package/dist/core/PromiseTracker.d.ts +14 -0
  8. package/dist/core/PromiseTracker.d.ts.map +1 -0
  9. package/dist/core/PromiseTracker.js +37 -0
  10. package/dist/core/PromiseTracker.js.map +1 -0
  11. package/dist/core/StatefulPromise.cjs +65 -0
  12. package/dist/core/StatefulPromise.cjs.map +1 -0
  13. package/dist/core/StatefulPromise.d.ts +39 -0
  14. package/dist/core/StatefulPromise.d.ts.map +1 -0
  15. package/dist/core/StatefulPromise.js +63 -0
  16. package/dist/core/StatefulPromise.js.map +1 -0
  17. package/dist/core/ll_transaction.cjs +3 -2
  18. package/dist/core/ll_transaction.cjs.map +1 -1
  19. package/dist/core/ll_transaction.d.ts.map +1 -1
  20. package/dist/core/ll_transaction.js +3 -2
  21. package/dist/core/ll_transaction.js.map +1 -1
  22. package/dist/core/transaction.cjs +577 -515
  23. package/dist/core/transaction.cjs.map +1 -1
  24. package/dist/core/transaction.d.ts +6 -4
  25. package/dist/core/transaction.d.ts.map +1 -1
  26. package/dist/core/transaction.js +578 -516
  27. package/dist/core/transaction.js.map +1 -1
  28. package/dist/test/tcp-proxy.cjs +129 -0
  29. package/dist/test/tcp-proxy.cjs.map +1 -0
  30. package/dist/test/tcp-proxy.d.ts +17 -0
  31. package/dist/test/tcp-proxy.d.ts.map +1 -0
  32. package/dist/test/tcp-proxy.js +107 -0
  33. package/dist/test/tcp-proxy.js.map +1 -0
  34. package/dist/test/test_config.cjs +54 -7
  35. package/dist/test/test_config.cjs.map +1 -1
  36. package/dist/test/test_config.d.ts +18 -2
  37. package/dist/test/test_config.d.ts.map +1 -1
  38. package/dist/test/test_config.js +54 -7
  39. package/dist/test/test_config.js.map +1 -1
  40. package/package.json +7 -8
  41. package/src/core/PromiseTracker.ts +39 -0
  42. package/src/core/StatefulPromise.ts +92 -0
  43. package/src/core/client.test.ts +1 -1
  44. package/src/core/config.test.ts +1 -1
  45. package/src/core/connectivity.test.ts +70 -0
  46. package/src/core/error.test.ts +1 -1
  47. package/src/core/ll_client.test.ts +1 -1
  48. package/src/core/ll_transaction.test.ts +1 -1
  49. package/src/core/ll_transaction.ts +3 -2
  50. package/src/core/transaction.test.ts +6 -1
  51. package/src/core/transaction.ts +91 -48
  52. package/src/core/types.test.ts +1 -1
  53. package/src/core/unauth_client.test.ts +1 -1
  54. package/src/helpers/rich_resource_types.test.ts +1 -1
  55. package/src/test/tcp-proxy.ts +126 -0
  56. package/src/test/test_config.test.ts +1 -1
  57. package/src/test/test_config.ts +82 -7
  58. package/src/util/util.test.ts +1 -1
@@ -39,6 +39,7 @@ import { initialTxStatWithoutTime } from './stat';
39
39
  import type { ErrorResourceData } from './error_resource';
40
40
  import { ErrorResourceType } from './error_resource';
41
41
  import { JsonGzObject, JsonObject } from '../helpers/pl';
42
+ import { PromiseTracker } from './PromiseTracker';
42
43
 
43
44
  /** Reference to resource, used only within transaction */
44
45
  export interface ResourceRef {
@@ -140,6 +141,22 @@ async function notFoundToUndefined<T>(cb: () => Promise<T>): Promise<T | undefin
140
141
  }
141
142
  }
142
143
 
144
+ /**
145
+ * Decorator that wraps the method's returned promise with this.track()
146
+ * This ensures that the promise will be awaited before the transaction is completed.
147
+ */
148
+ function tracked<T extends (this: PlTransaction, ...a: any[]) => Promise<any>>(
149
+ value: T,
150
+ _context: ClassMethodDecoratorContext,
151
+ ) {
152
+ return function (
153
+ this: PlTransaction,
154
+ ...args: Parameters<T>
155
+ ): ReturnType<T> {
156
+ return this.track(value.apply(this, args)) as ReturnType<T>;
157
+ } as unknown as T;
158
+ }
159
+
143
160
  /**
144
161
  * Each platform transaction has 3 stages:
145
162
  * - initialization (txOpen message -> txInfo response)
@@ -162,13 +179,10 @@ export class PlTransaction {
162
179
  * Contract: there must be no async operations between setting this field to true and sending complete signal to stream. */
163
180
  private _completed = false;
164
181
 
165
- /** Void operation futures are placed into this pool, and corresponding method return immediately.
166
- * This is done to save number of round-trips. Next operation producing result will also await those
167
- * pending ops, to throw any pending errors. */
168
- private pendingVoidOps: Promise<void>[] = [];
169
-
170
182
  private globalTxIdWasAwaited: boolean = false;
171
183
 
184
+ public readonly pending = new PromiseTracker();
185
+
172
186
  private readonly _startTime = Date.now();
173
187
  private readonly _stat = initialTxStatWithoutTime();
174
188
  public get stat(): TxStat {
@@ -202,6 +216,8 @@ export class PlTransaction {
202
216
  (r) => notEmpty(r.txOpen.tx?.id),
203
217
  );
204
218
 
219
+ void this.track(this.globalTxId);
220
+
205
221
  // To avoid floating promise
206
222
  this.globalTxId.catch((err) => {
207
223
  if (!this.globalTxIdWasAwaited) {
@@ -213,38 +229,48 @@ export class PlTransaction {
213
229
  this._stat.txCount++;
214
230
  }
215
231
 
216
- private async drainAndAwaitPendingOps(): Promise<void> {
217
- if (this.pendingVoidOps.length === 0) return;
232
+ /**
233
+ * Collect all pending promises for the transaction finalization.
234
+ */
235
+ public track<T>(promiseOrCallback: Promise<T> | (() => Promise<T>)): Promise<T> {
236
+ return this.pending.track(promiseOrCallback);
237
+ }
218
238
 
219
- // drain pending operations into temp array
220
- const pending = this.pendingVoidOps;
221
- this.pendingVoidOps = [];
239
+ private async drainAndAwaitPendingOps(): Promise<void> {
222
240
  // awaiting these pending operations first, to catch any errors
223
- await Promise.all(pending);
241
+ await this.pending.awaitAll();
224
242
  }
225
243
 
226
- private async sendSingleAndParse<Kind extends NonUndefined<ClientMessageRequest['oneofKind']>, T>(
244
+ private sendSingleAndParse<Kind extends NonUndefined<ClientMessageRequest['oneofKind']>, T>(
227
245
  r: OneOfKind<ClientMessageRequest, Kind>,
228
246
  parser: (resp: OneOfKind<ServerMessageResponse, Kind>) => T,
229
247
  ): Promise<T> {
230
- // pushing operation packet to server
231
- const rawResponsePromise = this.ll.send(r, false);
248
+ return this.pending.track(async () => {
249
+ const rawResponsePromise = this.ll.send(r, false);
232
250
 
233
- await this.drainAndAwaitPendingOps();
234
- // awaiting our result, and parsing the response
235
- return parser(await rawResponsePromise);
251
+ void this.pending.track(rawResponsePromise);
252
+
253
+ await this.drainAndAwaitPendingOps();
254
+
255
+ // awaiting our result, and parsing the response
256
+ return parser(await rawResponsePromise);
257
+ });
236
258
  }
237
259
 
238
- private async sendMultiAndParse<Kind extends NonUndefined<ClientMessageRequest['oneofKind']>, T>(
260
+ private sendMultiAndParse<Kind extends NonUndefined<ClientMessageRequest['oneofKind']>, T>(
239
261
  r: OneOfKind<ClientMessageRequest, Kind>,
240
262
  parser: (resp: OneOfKind<ServerMessageResponse, Kind>[]) => T,
241
263
  ): Promise<T> {
242
- // pushing operation packet to server
243
- const rawResponsePromise = this.ll.send(r, true);
264
+ return this.pending.track(async () => {
265
+ const rawResponsePromise = this.ll.send(r, true);
244
266
 
245
- await this.drainAndAwaitPendingOps();
246
- // awaiting our result, and parsing the response
247
- return parser(await rawResponsePromise);
267
+ void this.pending.track(rawResponsePromise);
268
+
269
+ await this.drainAndAwaitPendingOps();
270
+
271
+ // awaiting our result, and parsing the response
272
+ return parser(await rawResponsePromise);
273
+ });
248
274
  }
249
275
 
250
276
  private async sendVoidSync<Kind extends NonUndefined<ClientMessageRequest['oneofKind']>>(
@@ -257,7 +283,7 @@ export class PlTransaction {
257
283
  private sendVoidAsync<Kind extends NonUndefined<ClientMessageRequest['oneofKind']>>(
258
284
  r: OneOfKind<ClientMessageRequest, Kind>,
259
285
  ): void {
260
- this.pendingVoidOps.push(this.sendVoidSync(r));
286
+ void this.track(this.sendVoidSync(r));
261
287
  }
262
288
 
263
289
  private checkTxOpen() {
@@ -278,19 +304,18 @@ export class PlTransaction {
278
304
 
279
305
  if (!this.writable) {
280
306
  // no need to explicitly commit or reject read-only tx
281
- const completeResult = this.ll.complete();
307
+ const completeResult = this.track(this.ll.complete());
282
308
  await this.drainAndAwaitPendingOps();
283
309
  await completeResult;
284
310
  await this.ll.await();
285
311
  } else {
286
- // @TODO, also floating promises
287
- const commitResponse = this.sendSingleAndParse(
312
+ const commitResponse = this.track(this.sendSingleAndParse(
288
313
  { oneofKind: 'txCommit', txCommit: {} },
289
314
  (r) => r.txCommit.success,
290
- );
315
+ ));
291
316
 
292
317
  // send closing frame right after commit to save some time on round-trips
293
- const completeResult = this.ll.complete();
318
+ const completeResult = this.track(this.ll.complete());
294
319
 
295
320
  // now when we pushed all packets into the stream, we should wait for any
296
321
  // pending void operations from before, to catch any errors
@@ -312,8 +337,9 @@ export class PlTransaction {
312
337
  this._completed = true;
313
338
 
314
339
  const discardResponse = this.sendVoidSync({ oneofKind: 'txDiscard', txDiscard: {} });
340
+ void this.track(discardResponse);
315
341
  // send closing frame right after commit to save some time on round-trips
316
- const completeResult = this.ll.complete();
342
+ const completeResult = this.track(this.ll.complete());
317
343
 
318
344
  // now when we pushed all packets into the stream, we should wait for any
319
345
  // pending void operations from before, to catch any errors
@@ -356,16 +382,18 @@ export class PlTransaction {
356
382
  (r) => r.resourceCreateSingleton.resourceId as ResourceId,
357
383
  );
358
384
 
385
+ void this.track(globalId);
386
+
359
387
  return { globalId, localId };
360
388
  }
361
389
 
362
- public async getSingleton(name: string, loadFields: true): Promise<ResourceData>;
363
- public async getSingleton(name: string, loadFields: false): Promise<BasicResourceData>;
364
- public async getSingleton(
390
+ public getSingleton(name: string, loadFields: true): Promise<ResourceData>;
391
+ public getSingleton(name: string, loadFields: false): Promise<BasicResourceData>;
392
+ public getSingleton(
365
393
  name: string,
366
394
  loadFields: boolean = true,
367
395
  ): Promise<BasicResourceData | ResourceData> {
368
- return await this.sendSingleAndParse(
396
+ return this.sendSingleAndParse(
369
397
  {
370
398
  oneofKind: 'resourceGetSingleton',
371
399
  resourceGetSingleton: {
@@ -386,6 +414,8 @@ export class PlTransaction {
386
414
 
387
415
  const globalId = this.sendSingleAndParse(req(localId), (r) => parser(r) as ResourceId);
388
416
 
417
+ void this.track(globalId);
418
+
389
419
  return { globalId, localId };
390
420
  }
391
421
 
@@ -479,15 +509,15 @@ export class PlTransaction {
479
509
  this.sendVoidAsync({ oneofKind: 'resourceNameDelete', resourceNameDelete: { name } });
480
510
  }
481
511
 
482
- public async getResourceByName(name: string): Promise<ResourceId> {
483
- return await this.sendSingleAndParse(
512
+ public getResourceByName(name: string): Promise<ResourceId> {
513
+ return this.sendSingleAndParse(
484
514
  { oneofKind: 'resourceNameGet', resourceNameGet: { name } },
485
515
  (r) => ensureResourceIdNotNull(r.resourceNameGet.resourceId as OptionalResourceId),
486
516
  );
487
517
  }
488
518
 
489
- public async checkResourceNameExists(name: string): Promise<boolean> {
490
- return await this.sendSingleAndParse(
519
+ public checkResourceNameExists(name: string): Promise<boolean> {
520
+ return this.sendSingleAndParse(
491
521
  { oneofKind: 'resourceNameExists', resourceNameExists: { name } },
492
522
  (r) => r.resourceNameExists.exists,
493
523
  );
@@ -497,8 +527,8 @@ export class PlTransaction {
497
527
  this.sendVoidAsync({ oneofKind: 'resourceRemove', resourceRemove: { id: rId } });
498
528
  }
499
529
 
500
- public async resourceExists(rId: ResourceId): Promise<boolean> {
501
- return await this.sendSingleAndParse(
530
+ public resourceExists(rId: ResourceId): Promise<boolean> {
531
+ return this.sendSingleAndParse(
502
532
  { oneofKind: 'resourceExists', resourceExists: { resourceId: rId } },
503
533
  (r) => r.resourceExists.exists,
504
534
  );
@@ -531,6 +561,7 @@ export class PlTransaction {
531
561
  loadFields: boolean,
532
562
  ignoreCache: boolean
533
563
  ): Promise<BasicResourceData | ResourceData>;
564
+ @tracked
534
565
  public async getResourceData(
535
566
  rId: AnyResourceRef,
536
567
  loadFields: boolean = true,
@@ -573,7 +604,7 @@ export class PlTransaction {
573
604
  if (fromCache) {
574
605
  if (loadFields && !fromCache.data) {
575
606
  fromCache.data = result;
576
- // updating timestamp becuse we updated the record
607
+ // updating timestamp because we updated the record
577
608
  fromCache.cacheTxOpenTimestamp = this.txOpenTimestamp;
578
609
  }
579
610
  } else {
@@ -609,17 +640,18 @@ export class PlTransaction {
609
640
  rId: AnyResourceRef,
610
641
  loadFields: boolean
611
642
  ): Promise<BasicResourceData | ResourceData | undefined>;
643
+ @tracked
612
644
  public async getResourceDataIfExists(
613
645
  rId: AnyResourceRef,
614
646
  loadFields: boolean = true,
615
647
  ): Promise<BasicResourceData | ResourceData | undefined> {
616
- // calling this mehtod will ignore cache, because user intention is to detect resource absence
648
+ // calling this method will ignore cache, because user intention is to detect resource absence
617
649
  // which cache will prevent
618
650
  const result = await notFoundToUndefined(
619
651
  async () => await this.getResourceData(rId, loadFields, true),
620
652
  );
621
653
 
622
- // cleaning cache record if resorce was removed from the db
654
+ // cleaning cache record if resource was removed from the db
623
655
  if (result === undefined && !isResourceRef(rId) && !isLocalResourceId(rId))
624
656
  this.sharedResourceDataCache.delete(rId);
625
657
 
@@ -678,8 +710,8 @@ export class PlTransaction {
678
710
  if (value !== undefined) this.setField(fId, value);
679
711
  }
680
712
 
681
- public async fieldExists(fId: AnyFieldRef): Promise<boolean> {
682
- return await this.sendSingleAndParse(
713
+ public fieldExists(fId: AnyFieldRef): Promise<boolean> {
714
+ return this.sendSingleAndParse(
683
715
  {
684
716
  oneofKind: 'fieldExists',
685
717
  fieldExists: { field: toFieldId(fId) },
@@ -719,14 +751,15 @@ export class PlTransaction {
719
751
  });
720
752
  }
721
753
 
722
- public async getField(fId: AnyFieldRef): Promise<FieldData> {
754
+ public getField(fId: AnyFieldRef): Promise<FieldData> {
723
755
  this._stat.fieldsGet++;
724
- return await this.sendSingleAndParse(
756
+ return this.sendSingleAndParse(
725
757
  { oneofKind: 'fieldGet', fieldGet: { field: toFieldId(fId) } },
726
758
  (r) => protoToField(notEmpty(r.fieldGet.field)),
727
759
  );
728
760
  }
729
761
 
762
+ @tracked
730
763
  public async getFieldIfExists(fId: AnyFieldRef): Promise<FieldData | undefined> {
731
764
  return notFoundToUndefined(async () => await this.getField(fId));
732
765
  }
@@ -743,6 +776,7 @@ export class PlTransaction {
743
776
  // KV
744
777
  //
745
778
 
779
+ @tracked
746
780
  public async listKeyValues(rId: AnyResourceRef): Promise<KeyValue[]> {
747
781
  const result = await this.sendMultiAndParse(
748
782
  {
@@ -759,6 +793,7 @@ export class PlTransaction {
759
793
  return result;
760
794
  }
761
795
 
796
+ @tracked
762
797
  public async listKeyValuesString(rId: AnyResourceRef): Promise<KeyValueString[]> {
763
798
  return (await this.listKeyValues(rId)).map(({ key, value }) => ({
764
799
  key,
@@ -766,10 +801,12 @@ export class PlTransaction {
766
801
  }));
767
802
  }
768
803
 
804
+ @tracked
769
805
  public async listKeyValuesIfResourceExists(rId: AnyResourceRef): Promise<KeyValue[] | undefined> {
770
806
  return notFoundToUndefined(async () => await this.listKeyValues(rId));
771
807
  }
772
808
 
809
+ @tracked
773
810
  public async listKeyValuesStringIfResourceExists(
774
811
  rId: AnyResourceRef,
775
812
  ): Promise<KeyValueString[] | undefined> {
@@ -799,6 +836,7 @@ export class PlTransaction {
799
836
  });
800
837
  }
801
838
 
839
+ @tracked
802
840
  public async getKValue(rId: AnyResourceRef, key: string): Promise<Uint8Array> {
803
841
  const result = await this.sendSingleAndParse(
804
842
  {
@@ -814,14 +852,17 @@ export class PlTransaction {
814
852
  return result;
815
853
  }
816
854
 
855
+ @tracked
817
856
  public async getKValueString(rId: AnyResourceRef, key: string): Promise<string> {
818
857
  return Buffer.from(await this.getKValue(rId, key)).toString();
819
858
  }
820
859
 
860
+ @tracked
821
861
  public async getKValueJson<T>(rId: AnyResourceRef, key: string): Promise<T> {
822
862
  return JSON.parse(await this.getKValueString(rId, key)) as T;
823
863
  }
824
864
 
865
+ @tracked
825
866
  public async getKValueIfExists(
826
867
  rId: AnyResourceRef,
827
868
  key: string,
@@ -841,6 +882,7 @@ export class PlTransaction {
841
882
  return result;
842
883
  }
843
884
 
885
+ @tracked
844
886
  public async getKValueStringIfExists(
845
887
  rId: AnyResourceRef,
846
888
  key: string,
@@ -849,6 +891,7 @@ export class PlTransaction {
849
891
  return data === undefined ? undefined : Buffer.from(data).toString();
850
892
  }
851
893
 
894
+ @tracked
852
895
  public async getKValueJsonIfExists<T>(rId: AnyResourceRef, key: string): Promise<T | undefined> {
853
896
  const str = await this.getKValueString(rId, key);
854
897
  if (str === undefined) return undefined;
@@ -885,7 +928,7 @@ export class PlTransaction {
885
928
  public async complete() {
886
929
  if (this._completed) return;
887
930
  this._completed = true;
888
- const completeResult = this.ll.complete();
931
+ const completeResult = this.track(this.ll.complete());
889
932
  await this.drainAndAwaitPendingOps();
890
933
  await completeResult;
891
934
  }
@@ -1,4 +1,4 @@
1
- import { expect, test } from '@jest/globals';
1
+ import { expect, test } from 'vitest';
2
2
  import {
3
3
  createGlobalResourceId,
4
4
  createLocalResourceId,
@@ -1,7 +1,7 @@
1
1
  import { UnauthenticatedPlClient } from './unauth_client';
2
2
  import { getTestConfig } from '../test/test_config';
3
3
  import { UnauthenticatedError } from './errors';
4
- import { test, expect } from '@jest/globals';
4
+ import { test, expect } from 'vitest';
5
5
 
6
6
  test('ping test', async () => {
7
7
  const client = new UnauthenticatedPlClient(getTestConfig().address);
@@ -1,4 +1,4 @@
1
- import { test } from '@jest/globals';
1
+ import { test } from 'vitest';
2
2
 
3
3
  // type TestType1 = string;
4
4
  // type TestType2 = string | number | { a: number };
@@ -0,0 +1,126 @@
1
+ import * as net from 'node:net';
2
+ import type { AddressInfo } from 'node:net';
3
+ import { Transform } from 'node:stream';
4
+ import type { TransformCallback } from 'node:stream';
5
+ import { pipeline } from 'node:stream/promises';
6
+ import * as timers from 'node:timers/promises';
7
+
8
+ export type TcpProxyOptions = {
9
+ port?: number;
10
+ targetPort: number;
11
+ latency: number;
12
+ verbose?: boolean;
13
+ };
14
+
15
+ export async function startTcpProxy(options: TcpProxyOptions) {
16
+ const { port, targetPort } = options;
17
+
18
+ const state = {
19
+ latency: options.latency,
20
+ };
21
+
22
+ const setLatency = (latency: number) => {
23
+ state.latency = latency;
24
+ };
25
+
26
+ const getLatency = () => {
27
+ return state.latency;
28
+ };
29
+
30
+ const connections = new Set<{ socket: net.Socket; client: net.Socket }>();
31
+
32
+ async function disconnectAll() {
33
+ const kill = () => {
34
+ for (const { socket, client } of connections) {
35
+ if (!socket.destroyed) socket.destroy();
36
+ if (!client.destroyed) client.destroy();
37
+ }
38
+ connections.clear();
39
+ };
40
+ await timers.setTimeout(1);
41
+ kill();
42
+ };
43
+
44
+ const server = net
45
+ .createServer((socket: net.Socket) => {
46
+ const client = net.createConnection({ port: targetPort }, () => {
47
+ if (options.verbose) console.log(`connected to ${targetPort}`);
48
+ });
49
+
50
+ const pair = { socket, client };
51
+ connections.add(pair);
52
+ const onClose = () => connections.delete(pair);
53
+ socket.on('close', onClose);
54
+ client.on('close', onClose);
55
+
56
+ class LatencyTransform extends Transform {
57
+ private pendingTimer?: NodeJS.Timeout;
58
+ constructor() {
59
+ super();
60
+ }
61
+
62
+ _transform(chunk: Buffer, _enc: BufferEncoding, callback: TransformCallback) {
63
+ // Backpressure is respected by delaying the callback until after push
64
+ this.pendingTimer = setTimeout(() => {
65
+ this.pendingTimer = undefined;
66
+ this.push(chunk);
67
+ callback();
68
+ }, state.latency);
69
+ }
70
+
71
+ _destroy(err: Error | null, cb: (error?: Error | null) => void) {
72
+ if (this.pendingTimer) clearTimeout(this.pendingTimer);
73
+ this.pendingTimer = undefined;
74
+ cb(err);
75
+ }
76
+ }
77
+
78
+ const toClientLatency = new LatencyTransform();
79
+ const toTargetLatency = new LatencyTransform();
80
+
81
+ // Bidirectional pipelines with latency and error propagation
82
+ pipeline(socket, toTargetLatency, client).catch((err) => {
83
+ socket.destroy(err);
84
+ client.destroy(err);
85
+ });
86
+
87
+ pipeline(client, toClientLatency, socket).catch((err) => {
88
+ socket.destroy(err);
89
+ client.destroy(err);
90
+ });
91
+ });
92
+
93
+ server.listen({ port: port ?? 0 }, () => {
94
+ console.log('opened server on', server.address());
95
+ });
96
+
97
+ // Wait for proxy to be ready
98
+ await new Promise<void>((resolve, reject) => {
99
+ if (server.listening) return resolve();
100
+ const onError = (err: Error) => {
101
+ server.off('listening', onListening);
102
+ reject(err);
103
+ };
104
+ const onListening = () => {
105
+ server.off('error', onError);
106
+ resolve();
107
+ };
108
+ server.once('error', onError);
109
+ server.once('listening', onListening);
110
+ });
111
+
112
+ return {
113
+ server,
114
+ get port() { return (server.address() as AddressInfo)?.port; },
115
+ setLatency,
116
+ getLatency,
117
+ disconnectAll,
118
+ close: async () => {
119
+ await new Promise<void>((resolve) => {
120
+ server.close(() => resolve());
121
+ });
122
+ },
123
+ };
124
+ }
125
+
126
+ export type TestTcpProxy = Awaited<ReturnType<typeof startTcpProxy>>;
@@ -1,5 +1,5 @@
1
1
  import { getTestClientConf } from './test_config';
2
- import { test, expect } from '@jest/globals';
2
+ import { test, expect } from 'vitest';
3
3
 
4
4
  test('test that test config have no alternative root set', async () => {
5
5
  const { conf } = await getTestClientConf();
@@ -9,6 +9,12 @@ import type { OptionalResourceId } from '../core/types';
9
9
  import { NullResourceId, resourceIdToString } from '../core/types';
10
10
  import { inferAuthRefreshTime } from '../core/auth';
11
11
  import * as path from 'node:path';
12
+ import type { TestTcpProxy } from './tcp-proxy';
13
+ import { startTcpProxy } from './tcp-proxy';
14
+
15
+ export {
16
+ TestTcpProxy,
17
+ };
12
18
 
13
19
  export interface TestConfig {
14
20
  address: string;
@@ -144,25 +150,94 @@ export async function getTestLLClient(confOverrides: Partial<PlClientConfig> = {
144
150
  return new LLPlClient({ ...conf, ...confOverrides }, { auth });
145
151
  }
146
152
 
147
- export async function getTestClient(alternativeRoot?: string) {
153
+ export async function getTestClient(
154
+ alternativeRoot?: string,
155
+ confOverrides: Partial<PlClientConfig> = {},
156
+ ) {
148
157
  const { conf, auth } = await getTestClientConf();
149
158
  if (alternativeRoot !== undefined && conf.alternativeRoot !== undefined)
150
159
  throw new Error('test pl address configured with alternative root');
151
- return await PlClient.init({ ...conf, alternativeRoot }, auth);
160
+ return await PlClient.init({ ...conf, ...confOverrides, alternativeRoot }, auth);
152
161
  }
153
162
 
154
- export async function withTempRoot<T>(body: (pl: PlClient) => Promise<T>): Promise<T> {
163
+ export type WithTempRootOptions = {
164
+ /** If true and PL_ADDRESS is http://localhost or http://127.0.0.1:<port>,
165
+ * a TCP proxy will be started and PL client will connect through it. */
166
+ viaTcpProxy: true;
167
+ /** Artificial latency for proxy (ms). Default 0 */
168
+ proxyLatencyMs?: number;
169
+ } | {
170
+ viaTcpProxy?: undefined;
171
+ };
172
+
173
+ export async function withTempRoot<T>(
174
+ body: (pl: PlClient) => Promise<T>
175
+ ): Promise<T | void>;
176
+
177
+ export async function withTempRoot<T>(
178
+ body: (pl: PlClient, proxy: Awaited<ReturnType<typeof startTcpProxy>>) => Promise<T>,
179
+ options: {
180
+ viaTcpProxy: true;
181
+ proxyLatencyMs?: number;
182
+ },
183
+ ): Promise<T>;
184
+
185
+ export async function withTempRoot<T>(
186
+ body: (pl: PlClient, proxy: any) => Promise<T>,
187
+ options: WithTempRootOptions = {},
188
+ ): Promise<T | undefined> {
155
189
  const alternativeRoot = `test_${Date.now()}_${randomUUID()}`;
156
190
  let altRootId: OptionalResourceId = NullResourceId;
191
+ // Proxy management
192
+ let proxy: Awaited<ReturnType<typeof startTcpProxy>> | undefined;
193
+ let confOverrides: Partial<PlClientConfig> = {};
157
194
  try {
158
- const client = await getTestClient(alternativeRoot);
195
+ // Optionally start TCP proxy and rewrite PL_ADDRESS to point to proxy
196
+ if (options.viaTcpProxy === true && process.env.PL_ADDRESS) {
197
+ try {
198
+ const url = new URL(process.env.PL_ADDRESS);
199
+ const isHttp = url.protocol === 'http:';
200
+ const isLocal = url.hostname === '127.0.0.1' || url.hostname === 'localhost';
201
+ const port = parseInt(url.port);
202
+ if (isHttp && isLocal && Number.isFinite(port)) {
203
+ proxy = await startTcpProxy({ targetPort: port, latency: options.proxyLatencyMs ?? 0 });
204
+ // Override client connection host:port to proxy
205
+ confOverrides = { hostAndPort: `127.0.0.1:${proxy.port}` } as Partial<PlClientConfig>;
206
+ } else {
207
+ console.warn('*** skipping proxy-based test, PL_ADDRESS is not localhost', process.env.PL_ADDRESS);
208
+ return;
209
+ }
210
+ } catch (_e) {
211
+ // ignore proxy setup errors; tests will run against original address
212
+ }
213
+ }
214
+
215
+ const client = await getTestClient(alternativeRoot, confOverrides);
159
216
  altRootId = client.clientRoot;
160
- const value = await body(client);
217
+ console.log('altRootId', altRootId, altRootId.toString(16));
218
+ const value = await body(client, proxy);
161
219
  const rawClient = await getTestClient();
162
- await rawClient.deleteAlternativeRoot(alternativeRoot);
220
+ try {
221
+ await rawClient.deleteAlternativeRoot(alternativeRoot);
222
+ } catch (cleanupErr: any) {
223
+ // Cleanup may fail if test intentionally deleted resources
224
+ console.warn(`Failed to clean up alternative root ${alternativeRoot}:`, cleanupErr.message);
225
+ }
163
226
  return value;
164
227
  } catch (err: any) {
228
+ console.log('ERROR stack trace:', err.stack);
165
229
  console.log(`ALTERNATIVE ROOT: ${alternativeRoot} (${resourceIdToString(altRootId)})`);
166
- throw new Error(err.message, { cause: err });
230
+ throw err;
231
+ // throw new Error('withTempRoot error: ' + err.message, { cause: err });
232
+ } finally {
233
+ // Stop proxy if started
234
+ if (proxy) {
235
+ try {
236
+ await proxy.disconnectAll();
237
+ } catch (_e) { /* ignore */ }
238
+ try {
239
+ await new Promise<void>((resolve) => proxy!.server.close(() => resolve()));
240
+ } catch (_e) { /* ignore */ }
241
+ }
167
242
  }
168
243
  }
@@ -1,5 +1,5 @@
1
1
  import { toBytes } from './util';
2
- import { test, expect } from '@jest/globals';
2
+ import { test, expect } from 'vitest';
3
3
 
4
4
  test('test toBytes 1', () => {
5
5
  const arr = new Uint8Array([1, 2, 3]);