@milaboratories/pl-client 2.16.1 → 2.16.2
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/__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
- 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
- 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
- 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
- package/dist/core/PromiseTracker.cjs +39 -0
- package/dist/core/PromiseTracker.cjs.map +1 -0
- package/dist/core/PromiseTracker.d.ts +14 -0
- package/dist/core/PromiseTracker.d.ts.map +1 -0
- package/dist/core/PromiseTracker.js +37 -0
- package/dist/core/PromiseTracker.js.map +1 -0
- package/dist/core/StatefulPromise.cjs +65 -0
- package/dist/core/StatefulPromise.cjs.map +1 -0
- package/dist/core/StatefulPromise.d.ts +39 -0
- package/dist/core/StatefulPromise.d.ts.map +1 -0
- package/dist/core/StatefulPromise.js +63 -0
- package/dist/core/StatefulPromise.js.map +1 -0
- package/dist/core/ll_transaction.cjs +3 -2
- package/dist/core/ll_transaction.cjs.map +1 -1
- package/dist/core/ll_transaction.d.ts.map +1 -1
- package/dist/core/ll_transaction.js +3 -2
- package/dist/core/ll_transaction.js.map +1 -1
- package/dist/core/transaction.cjs +577 -515
- package/dist/core/transaction.cjs.map +1 -1
- package/dist/core/transaction.d.ts +6 -4
- package/dist/core/transaction.d.ts.map +1 -1
- package/dist/core/transaction.js +578 -516
- package/dist/core/transaction.js.map +1 -1
- package/dist/test/tcp-proxy.cjs +129 -0
- package/dist/test/tcp-proxy.cjs.map +1 -0
- package/dist/test/tcp-proxy.d.ts +17 -0
- package/dist/test/tcp-proxy.d.ts.map +1 -0
- package/dist/test/tcp-proxy.js +107 -0
- package/dist/test/tcp-proxy.js.map +1 -0
- package/dist/test/test_config.cjs +54 -7
- package/dist/test/test_config.cjs.map +1 -1
- package/dist/test/test_config.d.ts +18 -2
- package/dist/test/test_config.d.ts.map +1 -1
- package/dist/test/test_config.js +54 -7
- package/dist/test/test_config.js.map +1 -1
- package/package.json +5 -6
- package/src/core/PromiseTracker.ts +39 -0
- package/src/core/StatefulPromise.ts +92 -0
- package/src/core/client.test.ts +1 -1
- package/src/core/config.test.ts +1 -1
- package/src/core/connectivity.test.ts +70 -0
- package/src/core/error.test.ts +1 -1
- package/src/core/ll_client.test.ts +1 -1
- package/src/core/ll_transaction.test.ts +1 -1
- package/src/core/ll_transaction.ts +3 -2
- package/src/core/transaction.test.ts +6 -1
- package/src/core/transaction.ts +91 -48
- package/src/core/types.test.ts +1 -1
- package/src/core/unauth_client.test.ts +1 -1
- package/src/helpers/rich_resource_types.test.ts +1 -1
- package/src/test/tcp-proxy.ts +126 -0
- package/src/test/test_config.test.ts +1 -1
- package/src/test/test_config.ts +82 -7
- package/src/util/util.test.ts +1 -1
package/src/core/transaction.ts
CHANGED
|
@@ -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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
|
241
|
+
await this.pending.awaitAll();
|
|
224
242
|
}
|
|
225
243
|
|
|
226
|
-
private
|
|
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
|
-
|
|
231
|
-
|
|
248
|
+
return this.pending.track(async () => {
|
|
249
|
+
const rawResponsePromise = this.ll.send(r, false);
|
|
232
250
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
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
|
-
|
|
243
|
-
|
|
264
|
+
return this.pending.track(async () => {
|
|
265
|
+
const rawResponsePromise = this.ll.send(r, true);
|
|
244
266
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
363
|
-
public
|
|
364
|
-
public
|
|
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
|
|
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
|
|
483
|
-
return
|
|
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
|
|
490
|
-
return
|
|
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
|
|
501
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
682
|
-
return
|
|
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
|
|
754
|
+
public getField(fId: AnyFieldRef): Promise<FieldData> {
|
|
723
755
|
this._stat.fieldsGet++;
|
|
724
|
-
return
|
|
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
|
}
|
package/src/core/types.test.ts
CHANGED
|
@@ -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 '
|
|
4
|
+
import { test, expect } from 'vitest';
|
|
5
5
|
|
|
6
6
|
test('ping test', async () => {
|
|
7
7
|
const client = new UnauthenticatedPlClient(getTestConfig().address);
|
|
@@ -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>>;
|
package/src/test/test_config.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
217
|
+
console.log('altRootId', altRootId, altRootId.toString(16));
|
|
218
|
+
const value = await body(client, proxy);
|
|
161
219
|
const rawClient = await getTestClient();
|
|
162
|
-
|
|
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
|
|
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
|
}
|
package/src/util/util.test.ts
CHANGED