@larc-iu/plaid-client 0.2.0-alpha.11 → 0.2.0-alpha.12
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/index.d.ts +227 -10
- package/package.json +1 -1
- package/src/index.js +456 -26
- package/src/provenance.js +19 -0
- package/src/roles.js +9 -0
- package/src/serviceSchema.js +12 -2
- package/src/services.js +129 -17
package/index.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ interface ServiceParam {
|
|
|
25
25
|
min?: number;
|
|
26
26
|
max?: number;
|
|
27
27
|
step?: number;
|
|
28
|
+
/** `number` only: render as a slider to drag rather than a box to type in. */
|
|
29
|
+
slider?: boolean;
|
|
28
30
|
/** `string` only. */
|
|
29
31
|
placeholder?: string;
|
|
30
32
|
multiline?: boolean;
|
|
@@ -70,14 +72,41 @@ interface ServiceRegistration {
|
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
/** Connection-state transitions reported by a service registration. */
|
|
73
|
-
type ServiceStatusEvent =
|
|
75
|
+
type ServiceStatusEvent = "registered" | "reconnected" | "disconnected";
|
|
74
76
|
|
|
75
77
|
interface ResponseHelper {
|
|
76
|
-
|
|
78
|
+
requestId: string;
|
|
79
|
+
requesterId: string | null;
|
|
80
|
+
/** True once the requester has asked this request to stop. */
|
|
81
|
+
readonly cancelled: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Report progress — and a cancellation CHECKPOINT: throws
|
|
84
|
+
* `ServiceCancelled` once a stop has been asked for, which is what makes a
|
|
85
|
+
* service that already reports progress cancellable for free.
|
|
86
|
+
*/
|
|
87
|
+
progress(percent: number, message: string, extra?: Record<string, any>): void;
|
|
88
|
+
/** Stop here if the requester has asked the request to stop. */
|
|
89
|
+
raiseIfCancelled(): void;
|
|
90
|
+
/** Run `fn` as a stretch that must finish once begun, usually the writes. */
|
|
91
|
+
critical<T>(fn: () => T | Promise<T>): Promise<T>;
|
|
77
92
|
complete(data: any): void;
|
|
93
|
+
/** End the request because it was asked to stop; result carries `stopped: true`. */
|
|
94
|
+
stopped(data?: any): void;
|
|
78
95
|
error(error: string | Error): void;
|
|
79
96
|
}
|
|
80
97
|
|
|
98
|
+
/** Thrown inside a handler when the requester has asked it to stop. */
|
|
99
|
+
export class ServiceCancelled extends Error {
|
|
100
|
+
constructor(message?: string);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The cancellation half of a responseHelper, on its own. */
|
|
104
|
+
export function createCancelScope(isCancelled: () => boolean): {
|
|
105
|
+
readonly cancelled: boolean;
|
|
106
|
+
raiseIfCancelled(): void;
|
|
107
|
+
critical<T>(fn: () => T | Promise<T>): Promise<T>;
|
|
108
|
+
};
|
|
109
|
+
|
|
81
110
|
interface SSEConnection {
|
|
82
111
|
close(): void;
|
|
83
112
|
getStats(): any;
|
|
@@ -267,6 +296,7 @@ interface UsersBundle {
|
|
|
267
296
|
endTime?: string,
|
|
268
297
|
asOf?: string,
|
|
269
298
|
): Promise<any[]>;
|
|
299
|
+
auditPage(userId: string, opts?: AuditPageOptions): Promise<Page>;
|
|
270
300
|
get(id: string, asOf?: string): Promise<any>;
|
|
271
301
|
delete(id: string, auditMessage?: string): Promise<any>;
|
|
272
302
|
activate(id: string, auditMessage?: string): Promise<any>;
|
|
@@ -313,12 +343,7 @@ interface CommentFilters {
|
|
|
313
343
|
|
|
314
344
|
/** Entities that can carry a comment. */
|
|
315
345
|
type CommentableType =
|
|
316
|
-
| "
|
|
317
|
-
| "text"
|
|
318
|
-
| "token"
|
|
319
|
-
| "span"
|
|
320
|
-
| "relation"
|
|
321
|
-
| "vocab-item";
|
|
346
|
+
"document" | "text" | "token" | "span" | "relation" | "vocab-item";
|
|
322
347
|
|
|
323
348
|
interface Comment {
|
|
324
349
|
id: string;
|
|
@@ -446,15 +471,182 @@ interface CreateInviteOptions {
|
|
|
446
471
|
note?: string;
|
|
447
472
|
}
|
|
448
473
|
|
|
474
|
+
/** Server limits, as reported by /api/v1/info. An unset limit is absent. */
|
|
475
|
+
interface ServerLimits {
|
|
476
|
+
mediaFileBytes?: number;
|
|
477
|
+
jsonBodyBytes?: number;
|
|
478
|
+
batchOperations?: number;
|
|
479
|
+
metadataDepth?: number;
|
|
480
|
+
metadataKeyCount?: number;
|
|
481
|
+
metadataStringLength?: number;
|
|
482
|
+
metadataTotalBytes?: number;
|
|
483
|
+
userDataValueBytes?: number;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
interface ServerHealth {
|
|
487
|
+
ok: boolean;
|
|
488
|
+
version: string;
|
|
489
|
+
uptimeMs: number;
|
|
490
|
+
audit: { dbSizeMb?: number; auditRows?: number; error?: string };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
interface DocumentLock {
|
|
494
|
+
documentId: string;
|
|
495
|
+
userId: string;
|
|
496
|
+
expiresAt: number;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
interface RateLimitBucket {
|
|
500
|
+
ip: string;
|
|
501
|
+
userId?: string;
|
|
502
|
+
failures: number;
|
|
503
|
+
limit: number;
|
|
504
|
+
blocked: boolean;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
interface RateLimitSnapshot {
|
|
508
|
+
windowMs: number;
|
|
509
|
+
logins: RateLimitBucket[];
|
|
510
|
+
ips: RateLimitBucket[];
|
|
511
|
+
invites: RateLimitBucket[];
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
interface BackupStatus {
|
|
515
|
+
enabled: boolean;
|
|
516
|
+
directory: string;
|
|
517
|
+
retention: number;
|
|
518
|
+
time: string;
|
|
519
|
+
backups: Array<{ name: string; bytes: number; modified: string }>;
|
|
520
|
+
ok?: boolean;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
interface ServerReport {
|
|
524
|
+
version: string;
|
|
525
|
+
jvm: {
|
|
526
|
+
uptimeMs: number;
|
|
527
|
+
startedAt: string;
|
|
528
|
+
java: string;
|
|
529
|
+
heapUsed: number;
|
|
530
|
+
heapMax: number;
|
|
531
|
+
};
|
|
532
|
+
database: {
|
|
533
|
+
path: string | null;
|
|
534
|
+
bytes: number | null;
|
|
535
|
+
walBytes: number | null;
|
|
536
|
+
slowQueryThresholdMs?: number;
|
|
537
|
+
journalMode?: string;
|
|
538
|
+
maxPoolSize?: number;
|
|
539
|
+
tables: Record<string, number | null>;
|
|
540
|
+
};
|
|
541
|
+
media: {
|
|
542
|
+
directory: string;
|
|
543
|
+
files: number;
|
|
544
|
+
bytes: number;
|
|
545
|
+
orphans: number;
|
|
546
|
+
orphanBytes: number;
|
|
547
|
+
};
|
|
548
|
+
backup: BackupStatus;
|
|
549
|
+
settings: Record<string, any>;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Server-level facts. `info` is fetched at most once per client. */
|
|
553
|
+
interface ServerBundle {
|
|
554
|
+
info(): Promise<{ limits: ServerLimits }>;
|
|
555
|
+
limits(): Promise<ServerLimits>;
|
|
556
|
+
health(): Promise<ServerHealth>;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** Instance-wide operations. Every method requires a global admin. */
|
|
560
|
+
interface AdminBundle {
|
|
561
|
+
server(): Promise<ServerReport>;
|
|
562
|
+
backup(): Promise<BackupStatus>;
|
|
563
|
+
locks(): Promise<{ entries: DocumentLock[] }>;
|
|
564
|
+
releaseLock(documentId: string): Promise<{ result: string }>;
|
|
565
|
+
rateLimits(): Promise<RateLimitSnapshot>;
|
|
566
|
+
clearRateLimits(opts?: {
|
|
567
|
+
ip?: string;
|
|
568
|
+
userId?: string;
|
|
569
|
+
}): Promise<{ result: string }>;
|
|
570
|
+
logs(opts?: {
|
|
571
|
+
lines?: number;
|
|
572
|
+
}): Promise<{ file: string | null; lines: string[]; error?: string }>;
|
|
573
|
+
userData(opts?: AdminUserDataOptions): Promise<AdminUserDataEntry[]>;
|
|
574
|
+
userDataPage(
|
|
575
|
+
opts?: AdminUserDataOptions & { limit?: number; cursor?: string },
|
|
576
|
+
): Promise<Page<AdminUserDataEntry>>;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** Narrowings for the cross-account private-data listing. */
|
|
580
|
+
interface AdminUserDataOptions {
|
|
581
|
+
/** Only keys starting with this literal head. */
|
|
582
|
+
prefix?: string;
|
|
583
|
+
/** Only keys matching this GLOB (`*` any run, `?` one character). */
|
|
584
|
+
pattern?: string;
|
|
585
|
+
includeValues?: boolean;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
type AdminUserDataEntry = UserDataEntry & { userId: string };
|
|
589
|
+
|
|
590
|
+
/** Options for a single page of any audit log. */
|
|
591
|
+
interface AuditPageOptions {
|
|
592
|
+
startTime?: string;
|
|
593
|
+
endTime?: string;
|
|
594
|
+
asOf?: string;
|
|
595
|
+
opTypes?: string[] | string;
|
|
596
|
+
/** "desc" pages newest-first; a cursor belongs to the direction that made it. */
|
|
597
|
+
order?: "asc" | "desc";
|
|
598
|
+
limit?: number;
|
|
599
|
+
cursor?: string;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** One user's activity over a scope and window. */
|
|
603
|
+
interface ActivityTallyRow {
|
|
604
|
+
user: { id: string; displayName?: string };
|
|
605
|
+
operations: number;
|
|
606
|
+
changes: number;
|
|
607
|
+
documents: number;
|
|
608
|
+
firstTs: string;
|
|
609
|
+
lastTs: string;
|
|
610
|
+
byDay?: Array<{ date: string; changes: number }>;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Instance-wide audit reads and the per-user aggregate. */
|
|
614
|
+
interface AuditBundle {
|
|
615
|
+
/** Admin only. */
|
|
616
|
+
list(opts?: {
|
|
617
|
+
startTime?: string;
|
|
618
|
+
endTime?: string;
|
|
619
|
+
opTypes?: string[] | string;
|
|
620
|
+
}): Promise<any[]>;
|
|
621
|
+
/** Admin only. */
|
|
622
|
+
listPage(opts?: AuditPageOptions): Promise<Page>;
|
|
623
|
+
/** Admin only. */
|
|
624
|
+
iterPages(opts?: {
|
|
625
|
+
startTime?: string;
|
|
626
|
+
endTime?: string;
|
|
627
|
+
opTypes?: string[] | string;
|
|
628
|
+
pageSize?: number;
|
|
629
|
+
}): AsyncGenerator<any[]>;
|
|
630
|
+
/** With projectId, open to that project's maintainers; without one, admin only. */
|
|
631
|
+
tally(opts?: {
|
|
632
|
+
projectId?: string;
|
|
633
|
+
startTime?: string;
|
|
634
|
+
endTime?: string;
|
|
635
|
+
daily?: boolean;
|
|
636
|
+
}): Promise<ActivityTallyRow[]>;
|
|
637
|
+
}
|
|
638
|
+
|
|
449
639
|
interface InvitesBundle {
|
|
450
|
-
list(opts?: { projectId?: string }): Promise<Invite[]>;
|
|
640
|
+
list(opts?: { projectId?: string; all?: boolean }): Promise<Invite[]>;
|
|
451
641
|
listPage(opts?: {
|
|
452
642
|
projectId?: string;
|
|
643
|
+
all?: boolean;
|
|
453
644
|
limit?: number;
|
|
454
645
|
cursor?: string;
|
|
455
646
|
}): Promise<Page>;
|
|
456
647
|
iterPages(opts?: {
|
|
457
648
|
projectId?: string;
|
|
649
|
+
all?: boolean;
|
|
458
650
|
pageSize?: number;
|
|
459
651
|
}): AsyncGenerator<Invite[]>;
|
|
460
652
|
/** The `code` is returned ONCE and is not recoverable afterward. */
|
|
@@ -532,6 +724,7 @@ interface DocumentsBundle {
|
|
|
532
724
|
endTime?: string,
|
|
533
725
|
asOf?: string,
|
|
534
726
|
): Promise<any[]>;
|
|
727
|
+
auditPage(documentId: string, opts?: AuditPageOptions): Promise<Page>;
|
|
535
728
|
get(documentId: string, includeBody?: boolean, asOf?: string): Promise<any>;
|
|
536
729
|
delete(documentId: string, auditMessage?: string): Promise<any>;
|
|
537
730
|
update(documentId: string, name: string, auditMessage?: string): Promise<any>;
|
|
@@ -541,6 +734,19 @@ interface DocumentsBundle {
|
|
|
541
734
|
metadata?: any,
|
|
542
735
|
auditMessage?: string,
|
|
543
736
|
): Promise<any>;
|
|
737
|
+
/** Copy a document and everything in it into a new document of the same project. */
|
|
738
|
+
copy(
|
|
739
|
+
documentId: string,
|
|
740
|
+
name: string,
|
|
741
|
+
options?: { includeMedia?: boolean },
|
|
742
|
+
auditMessage?: string,
|
|
743
|
+
): Promise<{ id: string; mediaError?: string }>;
|
|
744
|
+
restore(
|
|
745
|
+
documentId: string,
|
|
746
|
+
asOf: string,
|
|
747
|
+
options?: { dryRun?: boolean },
|
|
748
|
+
auditMessage?: string,
|
|
749
|
+
): Promise<any>;
|
|
544
750
|
}
|
|
545
751
|
|
|
546
752
|
interface MessagesBundle {
|
|
@@ -567,7 +773,11 @@ interface MessagesBundle {
|
|
|
567
773
|
serviceInfo: ServiceInfo,
|
|
568
774
|
onServiceRequest: (data: any, responseHelper: ResponseHelper) => void,
|
|
569
775
|
extras?: any,
|
|
570
|
-
onStatus?: (
|
|
776
|
+
onStatus?: (
|
|
777
|
+
event: ServiceStatusEvent,
|
|
778
|
+
projectId: string,
|
|
779
|
+
detail?: string,
|
|
780
|
+
) => void,
|
|
571
781
|
): ServiceRegistration;
|
|
572
782
|
/**
|
|
573
783
|
* Submit work to a service; streams progress to `onProgress`, resolves with
|
|
@@ -618,6 +828,7 @@ interface ProjectsBundle {
|
|
|
618
828
|
endTime?: string,
|
|
619
829
|
asOf?: string,
|
|
620
830
|
): Promise<any[]>;
|
|
831
|
+
auditPage(projectId: string, opts?: AuditPageOptions): Promise<Page>;
|
|
621
832
|
linkVocab(id: string, vocabId: string, auditMessage?: string): Promise<any>;
|
|
622
833
|
unlinkVocab(id: string, vocabId: string, auditMessage?: string): Promise<any>;
|
|
623
834
|
get(id: string, asOf?: string): Promise<any>;
|
|
@@ -852,6 +1063,9 @@ export declare class PlaidClient {
|
|
|
852
1063
|
apiTokens: ApiTokensBundle;
|
|
853
1064
|
userData: UserDataBundle;
|
|
854
1065
|
invites: InvitesBundle;
|
|
1066
|
+
server: ServerBundle;
|
|
1067
|
+
admin: AdminBundle;
|
|
1068
|
+
audit: AuditBundle;
|
|
855
1069
|
comments: CommentsBundle;
|
|
856
1070
|
tokenLayers: TokenLayersBundle;
|
|
857
1071
|
documents: DocumentsBundle;
|
|
@@ -919,6 +1133,9 @@ export const TASKS: {
|
|
|
919
1133
|
readonly TRANSCRIBE: "transcribe";
|
|
920
1134
|
readonly LINK_VOCAB: "link-vocab";
|
|
921
1135
|
readonly ANALYZE: "analyze";
|
|
1136
|
+
readonly TRANSLATE: "translate";
|
|
1137
|
+
readonly ASSIST: "assist";
|
|
1138
|
+
readonly DETECT_SPEECH: "detect-speech";
|
|
922
1139
|
};
|
|
923
1140
|
/** Whether a service serves a task (declared `extras.tasks`, legacy id-prefix fallback). */
|
|
924
1141
|
export function servesTask(service: DiscoveredService, task: string): boolean;
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -85,6 +85,31 @@ async function anonymousPost(baseUrl, path, body, options = {}) {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* GET a path with no authentication, for the two endpoints a client may need
|
|
90
|
+
* before it has a session: /health and /api/v1/info.
|
|
91
|
+
*/
|
|
92
|
+
async function anonymousGet(baseUrl, path, options = {}) {
|
|
93
|
+
const base = baseUrl.replace(/\/$/, "");
|
|
94
|
+
const url = `${base}${path}`;
|
|
95
|
+
try {
|
|
96
|
+
const fetchOptions = { method: "GET" };
|
|
97
|
+
const signal = timeoutSignal(
|
|
98
|
+
options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS,
|
|
99
|
+
);
|
|
100
|
+
if (signal) fetchOptions.signal = signal;
|
|
101
|
+
|
|
102
|
+
const response = await fetch(url, fetchOptions);
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
throw makeHttpError(response, await parseErrorBody(response), url, "GET");
|
|
105
|
+
}
|
|
106
|
+
return transformResponse(await response.json());
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error.status) throw error;
|
|
109
|
+
throw makeNetworkError(error, url, "GET");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
88
113
|
/**
|
|
89
114
|
* The server's cap on operations per batch request (plaid.rest-api.v1.batch).
|
|
90
115
|
* A batch queued past it is submitted as consecutive requests.
|
|
@@ -633,6 +658,42 @@ class PlaidClient {
|
|
|
633
658
|
}),
|
|
634
659
|
};
|
|
635
660
|
|
|
661
|
+
// Server-level facts. Fetched at most once: the limits cannot change while
|
|
662
|
+
// the server is up, and a caller asking "will this file be accepted" should
|
|
663
|
+
// not pay a round trip to find out.
|
|
664
|
+
let infoPromise = null;
|
|
665
|
+
this.server = {
|
|
666
|
+
/**
|
|
667
|
+
* This server's version and the limits it enforces, e.g.
|
|
668
|
+
* `{ limits: { mediaFileBytes, jsonBodyBytes, batchOperations, … } }`.
|
|
669
|
+
* Sizes are in bytes. Unauthenticated.
|
|
670
|
+
*/
|
|
671
|
+
info: () => {
|
|
672
|
+
if (!infoPromise) {
|
|
673
|
+
// bypassBatch: a read belongs to whoever asked for it, not to
|
|
674
|
+
// whatever batch happens to be open on this shared client.
|
|
675
|
+
infoPromise = this._request("GET", "/api/v1/info", {
|
|
676
|
+
bypassBatch: true,
|
|
677
|
+
}).catch((err) => {
|
|
678
|
+
// A failure must not be cached: a client that starts before the
|
|
679
|
+
// server is up would never see the limits at all.
|
|
680
|
+
infoPromise = null;
|
|
681
|
+
throw err;
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
return infoPromise;
|
|
685
|
+
},
|
|
686
|
+
/** Just the limits, which is what a caller almost always wants. */
|
|
687
|
+
limits: async () => (await this.server.info()).limits,
|
|
688
|
+
/**
|
|
689
|
+
* Liveness, version and database size. Unauthenticated, and served
|
|
690
|
+
* outside the REST router at /health, so it answers even while the API
|
|
691
|
+
* is refusing requests. Never cached — the point is that it is current.
|
|
692
|
+
* @returns {Promise<{ok: boolean, version: string, uptimeMs: number, audit: object}>}
|
|
693
|
+
*/
|
|
694
|
+
health: () => PlaidClient.health(this.baseUrl, { timeout: this.timeout }),
|
|
695
|
+
};
|
|
696
|
+
|
|
636
697
|
this.batch = {
|
|
637
698
|
/**
|
|
638
699
|
* Execute multiple API operations atomically. If any operation fails, all
|
|
@@ -812,6 +873,31 @@ class PlaidClient {
|
|
|
812
873
|
"op-types": opTypesParam(opTypes),
|
|
813
874
|
},
|
|
814
875
|
}),
|
|
876
|
+
/**
|
|
877
|
+
* One page of the same log, newest-first with `order: "desc"`. Use this
|
|
878
|
+
* rather than audit() wherever the caller wants the recent end of a log
|
|
879
|
+
* that may be long: audit() walks every page before it resolves.
|
|
880
|
+
* @param {object} [opts]
|
|
881
|
+
* @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
|
|
882
|
+
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
883
|
+
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
884
|
+
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
885
|
+
*/
|
|
886
|
+
auditPage: (
|
|
887
|
+
userId,
|
|
888
|
+
{ startTime, endTime, asOf, opTypes, order, limit, cursor } = {},
|
|
889
|
+
) =>
|
|
890
|
+
listPage(this, `/api/v1/users/${userId}/audit`, {
|
|
891
|
+
limit,
|
|
892
|
+
cursor,
|
|
893
|
+
query: {
|
|
894
|
+
"start-time": startTime,
|
|
895
|
+
"end-time": endTime,
|
|
896
|
+
"as-of": asOf,
|
|
897
|
+
"op-types": opTypesParam(opTypes),
|
|
898
|
+
order,
|
|
899
|
+
},
|
|
900
|
+
}),
|
|
815
901
|
/**
|
|
816
902
|
* Get a user by ID
|
|
817
903
|
* @param {string} id - The resource ID
|
|
@@ -1044,37 +1130,40 @@ class PlaidClient {
|
|
|
1044
1130
|
* Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
|
|
1045
1131
|
* @param {object} [opts]
|
|
1046
1132
|
* @param {string} [opts.projectId] - List this project's invites instead of your own
|
|
1133
|
+
* @param {boolean} [opts.all] - List every invite on the server (admin only)
|
|
1047
1134
|
*/
|
|
1048
|
-
list: ({ projectId } = {}) =>
|
|
1135
|
+
list: ({ projectId, all } = {}) =>
|
|
1049
1136
|
listAll(this, "/api/v1/invites", {
|
|
1050
|
-
query: { "project-id": projectId },
|
|
1137
|
+
query: { "project-id": projectId, all },
|
|
1051
1138
|
}),
|
|
1052
1139
|
/**
|
|
1053
1140
|
* Fetch a single page of invites.
|
|
1054
1141
|
* @param {object} [opts]
|
|
1055
1142
|
* @param {string} [opts.projectId] - List this project's invites instead of your own
|
|
1143
|
+
* @param {boolean} [opts.all] - List every invite on the server (admin only)
|
|
1056
1144
|
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
1057
1145
|
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
1058
1146
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
1059
1147
|
*/
|
|
1060
|
-
listPage: ({ projectId, limit, cursor } = {}) =>
|
|
1148
|
+
listPage: ({ projectId, all, limit, cursor } = {}) =>
|
|
1061
1149
|
listPage(this, "/api/v1/invites", {
|
|
1062
1150
|
limit,
|
|
1063
1151
|
cursor,
|
|
1064
|
-
query: { "project-id": projectId },
|
|
1152
|
+
query: { "project-id": projectId, all },
|
|
1065
1153
|
}),
|
|
1066
1154
|
/**
|
|
1067
1155
|
* Async-iterate invites page by page; yields each page's entries array.
|
|
1068
1156
|
* @param {object} [opts]
|
|
1069
1157
|
* @param {string} [opts.projectId] - List this project's invites instead of your own
|
|
1158
|
+
* @param {boolean} [opts.all] - List every invite on the server (admin only)
|
|
1070
1159
|
* @param {number} [opts.pageSize] - Per-request page size
|
|
1071
1160
|
* Cannot be used inside a batch (auto-paginates across requests); throws on first iteration if called while batching — use listPage() for a single page in a batch.
|
|
1072
1161
|
* @returns {AsyncGenerator<Array>}
|
|
1073
1162
|
*/
|
|
1074
|
-
iterPages: ({ projectId, pageSize } = {}) =>
|
|
1163
|
+
iterPages: ({ projectId, all, pageSize } = {}) =>
|
|
1075
1164
|
iterPages(this, "/api/v1/invites", {
|
|
1076
1165
|
pageSize,
|
|
1077
|
-
query: { "project-id": projectId },
|
|
1166
|
+
query: { "project-id": projectId, all },
|
|
1078
1167
|
}),
|
|
1079
1168
|
/**
|
|
1080
1169
|
* Mint an invite. The returned `code` is shown ONLY here — it is never
|
|
@@ -1134,6 +1223,206 @@ class PlaidClient {
|
|
|
1134
1223
|
this._request("DELETE", `/api/v1/invites/${id}`, { auditMessage }),
|
|
1135
1224
|
};
|
|
1136
1225
|
|
|
1226
|
+
this.admin = {
|
|
1227
|
+
/**
|
|
1228
|
+
* Everything about the server in one read: version and JVM uptime,
|
|
1229
|
+
* database size and per-table row counts, media directory usage, backup
|
|
1230
|
+
* configuration and the backups on disk, and the settings an operator
|
|
1231
|
+
* gets asked to confirm. Carries no secrets. Runs a count per table and
|
|
1232
|
+
* walks the media directory, so open it, do not poll it. Admin only.
|
|
1233
|
+
* @returns {Promise<{version: string, jvm: object, database: object, media: object, backup: object, settings: object}>}
|
|
1234
|
+
*/
|
|
1235
|
+
server: () => this._request("GET", "/api/v1/admin/server"),
|
|
1236
|
+
/**
|
|
1237
|
+
* Take a database backup right now, outside the nightly schedule.
|
|
1238
|
+
* Resolves to the backup block with `ok` reporting whether the snapshot
|
|
1239
|
+
* succeeded. Uses VACUUM INTO, which only reads, so it is safe while
|
|
1240
|
+
* people are working. Admin only.
|
|
1241
|
+
* @returns {Promise<{ok: boolean, directory: string, backups: Array}>}
|
|
1242
|
+
*/
|
|
1243
|
+
backup: () => this._request("POST", "/api/v1/admin/backup"),
|
|
1244
|
+
/**
|
|
1245
|
+
* Documents currently held by an editing lock, with who holds each and
|
|
1246
|
+
* when it expires on its own. Admin only.
|
|
1247
|
+
* @returns {Promise<{entries: Array<{documentId: string, userId: string, expiresAt: number}>}>}
|
|
1248
|
+
*/
|
|
1249
|
+
locks: () => this._request("GET", "/api/v1/admin/locks"),
|
|
1250
|
+
/**
|
|
1251
|
+
* Drop the lock on a document whoever holds it. Idempotent. For a client
|
|
1252
|
+
* that went away without releasing one. Admin only.
|
|
1253
|
+
* @param {string} documentId - The document ID
|
|
1254
|
+
*/
|
|
1255
|
+
releaseLock: (documentId) =>
|
|
1256
|
+
this._request("DELETE", `/api/v1/admin/locks/${documentId}`),
|
|
1257
|
+
/**
|
|
1258
|
+
* Live login and invite rate-limit buckets: the address, the account
|
|
1259
|
+
* where there is one, failures inside the window, the limit, and whether
|
|
1260
|
+
* it is currently blocking. Admin only.
|
|
1261
|
+
* @returns {Promise<{windowMs: number, logins: Array, ips: Array, invites: Array}>}
|
|
1262
|
+
*/
|
|
1263
|
+
rateLimits: () => this._request("GET", "/api/v1/admin/rate-limits"),
|
|
1264
|
+
/**
|
|
1265
|
+
* Forget recorded rate-limit failures. With `ip`, clears that address,
|
|
1266
|
+
* narrowed to one account with `userId`. With neither, clears every
|
|
1267
|
+
* bucket. Only ever unblocks. Admin only.
|
|
1268
|
+
* @param {object} [opts]
|
|
1269
|
+
* @param {string} [opts.ip] - Clear only this address
|
|
1270
|
+
* @param {string} [opts.userId] - Narrow to one account on that address
|
|
1271
|
+
*/
|
|
1272
|
+
clearRateLimits: ({ ip, userId } = {}) =>
|
|
1273
|
+
this._request("DELETE", "/api/v1/admin/rate-limits", {
|
|
1274
|
+
queryParams: { ip, "user-id": userId },
|
|
1275
|
+
}),
|
|
1276
|
+
/**
|
|
1277
|
+
* The tail of the configured log file. Resolves with an `error` string
|
|
1278
|
+
* instead of lines when no log file is configured or it does not exist
|
|
1279
|
+
* yet. Admin only.
|
|
1280
|
+
* @param {object} [opts]
|
|
1281
|
+
* @param {number} [opts.lines] - How many lines (default 200, max 2000)
|
|
1282
|
+
* @returns {Promise<{file: (string|null), lines: string[], error?: string}>}
|
|
1283
|
+
*/
|
|
1284
|
+
logs: ({ lines } = {}) =>
|
|
1285
|
+
this._request("GET", "/api/v1/admin/logs", {
|
|
1286
|
+
queryParams: { lines },
|
|
1287
|
+
}),
|
|
1288
|
+
/**
|
|
1289
|
+
* Private user-data entries across every account, each carrying its
|
|
1290
|
+
* `userId`. `/users/:userId/data` has always been owner-or-admin; this
|
|
1291
|
+
* is the same reach across accounts at once. Transparently follows
|
|
1292
|
+
* pagination cursors and returns the full flat array. Admin only.
|
|
1293
|
+
*
|
|
1294
|
+
* Narrow with `prefix` (the literal head of a key) and/or `pattern`, a
|
|
1295
|
+
* GLOB over the whole key (`*` any run, `?` one character) — the way to
|
|
1296
|
+
* ask for a key convention identified by a segment in the middle, e.g.
|
|
1297
|
+
* `igt:assistant:*:meta:*`. Values come only with `includeValues`, and
|
|
1298
|
+
* are recased like any other body (see `userData.put`).
|
|
1299
|
+
* Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use userDataPage() for a single page in a batch.
|
|
1300
|
+
* @param {object} [opts]
|
|
1301
|
+
* @param {string} [opts.prefix] - Only keys starting with this
|
|
1302
|
+
* @param {string} [opts.pattern] - Only keys matching this GLOB
|
|
1303
|
+
* @param {boolean} [opts.includeValues] - Also return each entry's value
|
|
1304
|
+
* @returns {Promise<Array<{userId: string, key: string, updatedAt: string, value?: *}>>}
|
|
1305
|
+
*/
|
|
1306
|
+
userData: ({ prefix, pattern, includeValues } = {}) =>
|
|
1307
|
+
listAll(this, "/api/v1/admin/user-data", {
|
|
1308
|
+
query: {
|
|
1309
|
+
prefix,
|
|
1310
|
+
pattern,
|
|
1311
|
+
"include-values": includeValues,
|
|
1312
|
+
},
|
|
1313
|
+
}),
|
|
1314
|
+
/**
|
|
1315
|
+
* One page of private user-data entries across accounts, ordered by
|
|
1316
|
+
* (user, key). Admin only.
|
|
1317
|
+
* @param {object} [opts]
|
|
1318
|
+
* @param {string} [opts.prefix] - Only keys starting with this
|
|
1319
|
+
* @param {string} [opts.pattern] - Only keys matching this GLOB
|
|
1320
|
+
* @param {boolean} [opts.includeValues] - Also return each entry's value
|
|
1321
|
+
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
1322
|
+
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
1323
|
+
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
1324
|
+
*/
|
|
1325
|
+
userDataPage: ({ prefix, pattern, includeValues, limit, cursor } = {}) =>
|
|
1326
|
+
listPage(this, "/api/v1/admin/user-data", {
|
|
1327
|
+
limit,
|
|
1328
|
+
cursor,
|
|
1329
|
+
query: {
|
|
1330
|
+
prefix,
|
|
1331
|
+
pattern,
|
|
1332
|
+
"include-values": includeValues,
|
|
1333
|
+
},
|
|
1334
|
+
}),
|
|
1335
|
+
};
|
|
1336
|
+
|
|
1337
|
+
this.audit = {
|
|
1338
|
+
/**
|
|
1339
|
+
* The audit log across every project, oldest first. Same fold, window
|
|
1340
|
+
* and op-type filter as the per-project read, with the entity scope
|
|
1341
|
+
* dropped. Admin only. Transparently follows pagination cursors and
|
|
1342
|
+
* returns the full flat array.
|
|
1343
|
+
* Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
|
|
1344
|
+
* @param {object} [opts]
|
|
1345
|
+
* @param {string} [opts.startTime] - Only operations at or after this instant
|
|
1346
|
+
* @param {string} [opts.endTime] - Only operations at or before this instant
|
|
1347
|
+
* @param {string[]|string} [opts.opTypes] - Only these op types (e.g.
|
|
1348
|
+
* `['span-layer/create']`). An entry appears when one of its operations
|
|
1349
|
+
* matches, carrying only the ones that did.
|
|
1350
|
+
*/
|
|
1351
|
+
list: ({ startTime, endTime, opTypes } = {}) =>
|
|
1352
|
+
listAll(this, "/api/v1/audit", {
|
|
1353
|
+
query: {
|
|
1354
|
+
"start-time": startTime,
|
|
1355
|
+
"end-time": endTime,
|
|
1356
|
+
"op-types": opTypesParam(opTypes),
|
|
1357
|
+
},
|
|
1358
|
+
}),
|
|
1359
|
+
/**
|
|
1360
|
+
* Fetch a single page of the instance-wide audit log. Admin only.
|
|
1361
|
+
* @param {object} [opts]
|
|
1362
|
+
* @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first, which
|
|
1363
|
+
* is what a feed wants. A cursor belongs to the direction that made it.
|
|
1364
|
+
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
1365
|
+
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
1366
|
+
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
1367
|
+
*/
|
|
1368
|
+
listPage: ({ startTime, endTime, opTypes, order, limit, cursor } = {}) =>
|
|
1369
|
+
listPage(this, "/api/v1/audit", {
|
|
1370
|
+
limit,
|
|
1371
|
+
cursor,
|
|
1372
|
+
query: {
|
|
1373
|
+
"start-time": startTime,
|
|
1374
|
+
"end-time": endTime,
|
|
1375
|
+
"op-types": opTypesParam(opTypes),
|
|
1376
|
+
order,
|
|
1377
|
+
},
|
|
1378
|
+
}),
|
|
1379
|
+
/**
|
|
1380
|
+
* Async-iterate the instance-wide audit log page by page. Admin only.
|
|
1381
|
+
* Cannot be used inside a batch (auto-paginates across requests); throws on first iteration if called while batching — use listPage() for a single page in a batch.
|
|
1382
|
+
* @returns {AsyncGenerator<Array>}
|
|
1383
|
+
*/
|
|
1384
|
+
iterPages: ({ startTime, endTime, opTypes, pageSize } = {}) =>
|
|
1385
|
+
iterPages(this, "/api/v1/audit", {
|
|
1386
|
+
pageSize,
|
|
1387
|
+
query: {
|
|
1388
|
+
"start-time": startTime,
|
|
1389
|
+
"end-time": endTime,
|
|
1390
|
+
"op-types": opTypesParam(opTypes),
|
|
1391
|
+
},
|
|
1392
|
+
}),
|
|
1393
|
+
/**
|
|
1394
|
+
* Per-user activity counts. `changes` is the number of logical actions,
|
|
1395
|
+
* folded the way the audit feed folds them, so one "Confirm word
|
|
1396
|
+
* analysis" counts once however many rows it wrote; `operations` is the
|
|
1397
|
+
* unfolded row count. Only users who did something appear — subtract
|
|
1398
|
+
* from the roster you already hold to find the ones who did not.
|
|
1399
|
+
*
|
|
1400
|
+
* With `projectId`, scoped to that project and open to its maintainers.
|
|
1401
|
+
* Without one, instance-wide and admin only.
|
|
1402
|
+
* @param {object} [opts]
|
|
1403
|
+
* @param {string} [opts.projectId] - Scope to one project
|
|
1404
|
+
* @param {string} [opts.startTime] - Only count at or after this instant
|
|
1405
|
+
* @param {string} [opts.endTime] - Only count at or before this instant
|
|
1406
|
+
* @param {boolean} [opts.daily] - Also return `byDay`, a list of
|
|
1407
|
+
* `{date, changes}` per user, oldest first, at the cost of a second
|
|
1408
|
+
* grouped scan
|
|
1409
|
+
* @returns {Promise<Array<{user: object, operations: number, changes: number, documents: number, firstTs: string, lastTs: string, byDay?: object}>>}
|
|
1410
|
+
*/
|
|
1411
|
+
tally: async ({ projectId, startTime, endTime, daily } = {}) => {
|
|
1412
|
+
const path = projectId
|
|
1413
|
+
? `/api/v1/projects/${projectId}/audit/tally`
|
|
1414
|
+
: "/api/v1/audit/tally";
|
|
1415
|
+
const result = await this._request("GET", path, {
|
|
1416
|
+
queryParams: {
|
|
1417
|
+
"start-time": startTime,
|
|
1418
|
+
"end-time": endTime,
|
|
1419
|
+
daily,
|
|
1420
|
+
},
|
|
1421
|
+
});
|
|
1422
|
+
return result.entries;
|
|
1423
|
+
},
|
|
1424
|
+
};
|
|
1425
|
+
|
|
1137
1426
|
this.tokenLayers = {
|
|
1138
1427
|
/**
|
|
1139
1428
|
* Shift a token layer's display order.
|
|
@@ -1345,6 +1634,31 @@ class PlaidClient {
|
|
|
1345
1634
|
"op-types": opTypesParam(opTypes),
|
|
1346
1635
|
},
|
|
1347
1636
|
}),
|
|
1637
|
+
/**
|
|
1638
|
+
* One page of the same log, newest-first with `order: "desc"`. Use this
|
|
1639
|
+
* rather than audit() wherever the caller wants the recent end of a log
|
|
1640
|
+
* that may be long: audit() walks every page before it resolves.
|
|
1641
|
+
* @param {object} [opts]
|
|
1642
|
+
* @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
|
|
1643
|
+
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
1644
|
+
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
1645
|
+
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
1646
|
+
*/
|
|
1647
|
+
auditPage: (
|
|
1648
|
+
documentId,
|
|
1649
|
+
{ startTime, endTime, asOf, opTypes, order, limit, cursor } = {},
|
|
1650
|
+
) =>
|
|
1651
|
+
listPage(this, `/api/v1/documents/${documentId}/audit`, {
|
|
1652
|
+
limit,
|
|
1653
|
+
cursor,
|
|
1654
|
+
query: {
|
|
1655
|
+
"start-time": startTime,
|
|
1656
|
+
"end-time": endTime,
|
|
1657
|
+
"as-of": asOf,
|
|
1658
|
+
"op-types": opTypesParam(opTypes),
|
|
1659
|
+
order,
|
|
1660
|
+
},
|
|
1661
|
+
}),
|
|
1348
1662
|
/**
|
|
1349
1663
|
* Restore a document to its state at an earlier time, as one
|
|
1350
1664
|
* operation: what was deleted since then comes back under its original
|
|
@@ -1361,7 +1675,10 @@ class PlaidClient {
|
|
|
1361
1675
|
*/
|
|
1362
1676
|
restore: (documentId, asOf, { dryRun } = {}, auditMessage) =>
|
|
1363
1677
|
this._request("POST", `/api/v1/documents/${documentId}/restore`, {
|
|
1364
|
-
queryParams: {
|
|
1678
|
+
queryParams: {
|
|
1679
|
+
"as-of": asOf,
|
|
1680
|
+
"dry-run": dryRun ? "true" : undefined,
|
|
1681
|
+
},
|
|
1365
1682
|
auditMessage,
|
|
1366
1683
|
}),
|
|
1367
1684
|
/**
|
|
@@ -1416,6 +1733,25 @@ class PlaidClient {
|
|
|
1416
1733
|
auditMessage,
|
|
1417
1734
|
body: bodyOf({ "project-id": projectId, name, metadata }),
|
|
1418
1735
|
}),
|
|
1736
|
+
/**
|
|
1737
|
+
* Copy a document and everything in it into a new document of the same
|
|
1738
|
+
* project, as one operation. The copy shares the source's layers and
|
|
1739
|
+
* the vocabulary entries its links name, and holds the source's texts,
|
|
1740
|
+
* tokens, spans, relations and vocab links under fresh ids, with their
|
|
1741
|
+
* metadata. Comments do not travel. The media file does, unless
|
|
1742
|
+
* `includeMedia` is false. Resolves to `{ id }`, plus `mediaError` when
|
|
1743
|
+
* the source had media the copy could not take with it.
|
|
1744
|
+
* @param {string} documentId - The document to copy
|
|
1745
|
+
* @param {string} name - The new document's name
|
|
1746
|
+
* @param {object} [options] - `{ includeMedia }`: false leaves the media
|
|
1747
|
+
* file behind
|
|
1748
|
+
* @param {string} [auditMessage] - Custom audit message for this operation
|
|
1749
|
+
*/
|
|
1750
|
+
copy: (documentId, name, { includeMedia } = {}, auditMessage) =>
|
|
1751
|
+
this._request("POST", `/api/v1/documents/${documentId}/copy`, {
|
|
1752
|
+
auditMessage,
|
|
1753
|
+
body: bodyOf({ name, "include-media": includeMedia }),
|
|
1754
|
+
}),
|
|
1419
1755
|
};
|
|
1420
1756
|
|
|
1421
1757
|
this.projects = {
|
|
@@ -1522,6 +1858,31 @@ class PlaidClient {
|
|
|
1522
1858
|
"op-types": opTypesParam(opTypes),
|
|
1523
1859
|
},
|
|
1524
1860
|
}),
|
|
1861
|
+
/**
|
|
1862
|
+
* One page of the same log, newest-first with `order: "desc"`. Use this
|
|
1863
|
+
* rather than audit() wherever the caller wants the recent end of a log
|
|
1864
|
+
* that may be long: audit() walks every page before it resolves.
|
|
1865
|
+
* @param {object} [opts]
|
|
1866
|
+
* @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
|
|
1867
|
+
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
1868
|
+
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
1869
|
+
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
1870
|
+
*/
|
|
1871
|
+
auditPage: (
|
|
1872
|
+
projectId,
|
|
1873
|
+
{ startTime, endTime, asOf, opTypes, order, limit, cursor } = {},
|
|
1874
|
+
) =>
|
|
1875
|
+
listPage(this, `/api/v1/projects/${projectId}/audit`, {
|
|
1876
|
+
limit,
|
|
1877
|
+
cursor,
|
|
1878
|
+
query: {
|
|
1879
|
+
"start-time": startTime,
|
|
1880
|
+
"end-time": endTime,
|
|
1881
|
+
"as-of": asOf,
|
|
1882
|
+
"op-types": opTypesParam(opTypes),
|
|
1883
|
+
order,
|
|
1884
|
+
},
|
|
1885
|
+
}),
|
|
1525
1886
|
/**
|
|
1526
1887
|
* When the calling user last wrote to each document in a project, as a
|
|
1527
1888
|
* `{documentId: timestamp}` map. Documents they have never written to
|
|
@@ -2163,8 +2524,25 @@ class PlaidClient {
|
|
|
2163
2524
|
* @param {Object} [opts] - {requestId, onAccepted}
|
|
2164
2525
|
* @returns {Promise<any>} The service's result
|
|
2165
2526
|
*/
|
|
2166
|
-
requestService: (
|
|
2167
|
-
|
|
2527
|
+
requestService: (
|
|
2528
|
+
projectId,
|
|
2529
|
+
serviceId,
|
|
2530
|
+
data,
|
|
2531
|
+
timeout,
|
|
2532
|
+
onProgress,
|
|
2533
|
+
signal,
|
|
2534
|
+
opts,
|
|
2535
|
+
) =>
|
|
2536
|
+
requestService(
|
|
2537
|
+
this,
|
|
2538
|
+
projectId,
|
|
2539
|
+
serviceId,
|
|
2540
|
+
data,
|
|
2541
|
+
timeout,
|
|
2542
|
+
onProgress,
|
|
2543
|
+
signal,
|
|
2544
|
+
opts,
|
|
2545
|
+
),
|
|
2168
2546
|
|
|
2169
2547
|
/**
|
|
2170
2548
|
* Rejoin a service request made earlier and await its result: the
|
|
@@ -2180,8 +2558,21 @@ class PlaidClient {
|
|
|
2180
2558
|
* @param {AbortSignal} [signal] - Abort to stop waiting
|
|
2181
2559
|
* @returns {Promise<any>} The service's result
|
|
2182
2560
|
*/
|
|
2183
|
-
attachServiceRequest: (
|
|
2184
|
-
|
|
2561
|
+
attachServiceRequest: (
|
|
2562
|
+
projectId,
|
|
2563
|
+
requestId,
|
|
2564
|
+
timeout,
|
|
2565
|
+
onProgress,
|
|
2566
|
+
signal,
|
|
2567
|
+
) =>
|
|
2568
|
+
attachServiceRequest(
|
|
2569
|
+
this,
|
|
2570
|
+
projectId,
|
|
2571
|
+
requestId,
|
|
2572
|
+
timeout,
|
|
2573
|
+
onProgress,
|
|
2574
|
+
signal,
|
|
2575
|
+
),
|
|
2185
2576
|
|
|
2186
2577
|
/**
|
|
2187
2578
|
* Ask the service to stop a request made earlier. The request still
|
|
@@ -2238,7 +2629,11 @@ class PlaidClient {
|
|
|
2238
2629
|
// and the server ran the query against the batch's tx Connection, which
|
|
2239
2630
|
// 500s and rolls back every write in that batch.
|
|
2240
2631
|
this.query = (body, auditMessage) =>
|
|
2241
|
-
this._request("POST", "/api/v1/query", {
|
|
2632
|
+
this._request("POST", "/api/v1/query", {
|
|
2633
|
+
auditMessage,
|
|
2634
|
+
body,
|
|
2635
|
+
bypassBatch: true,
|
|
2636
|
+
});
|
|
2242
2637
|
|
|
2243
2638
|
// Logical-operation groups (audit-log grouping). There is no create: a
|
|
2244
2639
|
// group row is made lazily by the first write carrying `?group-id=`
|
|
@@ -2333,7 +2728,10 @@ class PlaidClient {
|
|
|
2333
2728
|
* @param {string} [opts.entityId] - With entityType, one entity's thread
|
|
2334
2729
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
2335
2730
|
*/
|
|
2336
|
-
listPage: (
|
|
2731
|
+
listPage: (
|
|
2732
|
+
projectId,
|
|
2733
|
+
{ limit, cursor, documentId, entityType, entityId } = {},
|
|
2734
|
+
) =>
|
|
2337
2735
|
listPage(this, `/api/v1/projects/${projectId}/comments`, {
|
|
2338
2736
|
limit,
|
|
2339
2737
|
cursor,
|
|
@@ -2354,7 +2752,10 @@ class PlaidClient {
|
|
|
2354
2752
|
* @param {string} [opts.entityId] - With entityType, one entity's thread
|
|
2355
2753
|
* @returns {AsyncGenerator<Array>}
|
|
2356
2754
|
*/
|
|
2357
|
-
iterPages: (
|
|
2755
|
+
iterPages: (
|
|
2756
|
+
projectId,
|
|
2757
|
+
{ pageSize, documentId, entityType, entityId } = {},
|
|
2758
|
+
) =>
|
|
2358
2759
|
iterPages(this, `/api/v1/projects/${projectId}/comments`, {
|
|
2359
2760
|
pageSize,
|
|
2360
2761
|
query: {
|
|
@@ -2421,10 +2822,14 @@ class PlaidClient {
|
|
|
2421
2822
|
* @returns {Promise<Object<string, number>>}
|
|
2422
2823
|
*/
|
|
2423
2824
|
countsInVocab: (vocabId, { entityId } = {}) =>
|
|
2424
|
-
this._request(
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2825
|
+
this._request(
|
|
2826
|
+
"GET",
|
|
2827
|
+
`/api/v1/vocab-layers/${vocabId}/comments/counts`,
|
|
2828
|
+
{
|
|
2829
|
+
queryParams: { "entity-id": entityId },
|
|
2830
|
+
skipResponseTransform: true,
|
|
2831
|
+
},
|
|
2832
|
+
),
|
|
2428
2833
|
};
|
|
2429
2834
|
}
|
|
2430
2835
|
|
|
@@ -2580,7 +2985,9 @@ class PlaidClient {
|
|
|
2580
2985
|
// must not fail on its size alone.
|
|
2581
2986
|
const results = [];
|
|
2582
2987
|
for (let i = 0; i < ops.length; i += MAX_BATCH_OPS) {
|
|
2583
|
-
results.push(
|
|
2988
|
+
results.push(
|
|
2989
|
+
...(await this._postBatch(url, ops.slice(i, i + MAX_BATCH_OPS))),
|
|
2990
|
+
);
|
|
2584
2991
|
}
|
|
2585
2992
|
return results;
|
|
2586
2993
|
} finally {
|
|
@@ -2703,6 +3110,27 @@ class PlaidClient {
|
|
|
2703
3110
|
* @param {object} [options] - Client options forwarded to the constructor (e.g. { timeout })
|
|
2704
3111
|
* @returns {Promise<PlaidClient>} - Authenticated client instance
|
|
2705
3112
|
*/
|
|
3113
|
+
/**
|
|
3114
|
+
* Liveness, version and database size, with NO authentication and no client
|
|
3115
|
+
* instance — for a launcher or status page checking whether a server is up
|
|
3116
|
+
* before anyone logs in.
|
|
3117
|
+
* @param {string} baseUrl - The API base URL
|
|
3118
|
+
* @param {object} [options]
|
|
3119
|
+
*/
|
|
3120
|
+
static async health(baseUrl, options = {}) {
|
|
3121
|
+
return anonymousGet(baseUrl, "/health", options);
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
/**
|
|
3125
|
+
* The limits this server enforces, with NO authentication and no client
|
|
3126
|
+
* instance. See the instance method for the shape.
|
|
3127
|
+
* @param {string} baseUrl - The API base URL
|
|
3128
|
+
* @param {object} [options]
|
|
3129
|
+
*/
|
|
3130
|
+
static async info(baseUrl, options = {}) {
|
|
3131
|
+
return anonymousGet(baseUrl, "/api/v1/info", options);
|
|
3132
|
+
}
|
|
3133
|
+
|
|
2706
3134
|
/**
|
|
2707
3135
|
* Build the link to hand someone for an invite code. The server never sees
|
|
2708
3136
|
* an app URL, so the app that minted the invite is the one that names it.
|
|
@@ -2730,12 +3158,7 @@ class PlaidClient {
|
|
|
2730
3158
|
* @returns {Promise<{kind: string, status: string, expiresAt: string, projectName?: string, projectRole?: string, email?: string, grantAdmin: boolean}>}
|
|
2731
3159
|
*/
|
|
2732
3160
|
static async lookupInvite(baseUrl, code, options = {}) {
|
|
2733
|
-
return anonymousPost(
|
|
2734
|
-
baseUrl,
|
|
2735
|
-
"/api/v1/invite-codes/lookup",
|
|
2736
|
-
{ code },
|
|
2737
|
-
options,
|
|
2738
|
-
);
|
|
3161
|
+
return anonymousPost(baseUrl, "/api/v1/invites/lookup", { code }, options);
|
|
2739
3162
|
}
|
|
2740
3163
|
|
|
2741
3164
|
/**
|
|
@@ -2766,7 +3189,7 @@ class PlaidClient {
|
|
|
2766
3189
|
) {
|
|
2767
3190
|
const data = await anonymousPost(
|
|
2768
3191
|
baseUrl,
|
|
2769
|
-
"/api/v1/
|
|
3192
|
+
"/api/v1/invites/redeem",
|
|
2770
3193
|
bodyOf({ code, email, password, "display-name": displayName }),
|
|
2771
3194
|
options,
|
|
2772
3195
|
);
|
|
@@ -2831,6 +3254,7 @@ export {
|
|
|
2831
3254
|
export {
|
|
2832
3255
|
PLAID_NAMESPACE,
|
|
2833
3256
|
ROLE_KEY,
|
|
3257
|
+
PRESERVE_ON_SPLIT_KEY,
|
|
2834
3258
|
ROLES,
|
|
2835
3259
|
readRole,
|
|
2836
3260
|
findByRole,
|
|
@@ -2847,6 +3271,11 @@ export {
|
|
|
2847
3271
|
buildDefaultValues,
|
|
2848
3272
|
coerceParamValues,
|
|
2849
3273
|
} from "./serviceSchema.js";
|
|
3274
|
+
// Cooperative cancellation for a service AUTHORED in JS: `progress()` is a
|
|
3275
|
+
// checkpoint that throws ServiceCancelled once the requester stops the
|
|
3276
|
+
// request, and `critical()` holds that off around writes. See ./services.js
|
|
3277
|
+
// and the manual, "Stopping a request".
|
|
3278
|
+
export { ServiceCancelled, createCancelScope } from "./services.js";
|
|
2850
3279
|
// Provenance: the cross-app convention for who made an annotation (flat
|
|
2851
3280
|
// prov/provSource/provConfirmed metadata; absence = a verifier; 'inferred' =
|
|
2852
3281
|
// a machine; 'contributed' = a person whose work is reviewed), plus the
|
|
@@ -2855,6 +3284,7 @@ export {
|
|
|
2855
3284
|
PROV,
|
|
2856
3285
|
PROV_STATES,
|
|
2857
3286
|
PROV_CONFIRMED,
|
|
3287
|
+
PROVENANCE_KEYS,
|
|
2858
3288
|
stampInferred,
|
|
2859
3289
|
confirmedInferred,
|
|
2860
3290
|
stampContributed,
|
package/src/provenance.js
CHANGED
|
@@ -99,6 +99,25 @@ export const PROV = Object.freeze({
|
|
|
99
99
|
CONTRIBUTED: 'contributed',
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Every provenance key, for a caller that has to name them as a set rather
|
|
104
|
+
* than read one: what a token layer declares under
|
|
105
|
+
* `config.plaid.preserveOnSplit` so a token born of a split inherits the
|
|
106
|
+
* origin of the token it came from, and what a reshape carries by hand.
|
|
107
|
+
*
|
|
108
|
+
* Provenance is the one metadata family Plaid itself owns. The user cannot
|
|
109
|
+
* maintain it, and in a shared project usually cannot even see it, since it
|
|
110
|
+
* may belong to an app they are not using. Everything else on an entity is
|
|
111
|
+
* the user's own content and no structural operation should touch it.
|
|
112
|
+
*/
|
|
113
|
+
export const PROVENANCE_KEYS = Object.freeze([
|
|
114
|
+
PROV.key,
|
|
115
|
+
PROV.sourceKey,
|
|
116
|
+
PROV.confirmedKey,
|
|
117
|
+
PROV.probKey,
|
|
118
|
+
PROV.detailKey,
|
|
119
|
+
]);
|
|
120
|
+
|
|
102
121
|
/** The four provenance states returned by provState. */
|
|
103
122
|
export const PROV_STATES = Object.freeze({
|
|
104
123
|
HUMAN: 'human',
|
package/src/roles.js
CHANGED
|
@@ -25,6 +25,15 @@ export const PLAID_NAMESPACE = 'plaid';
|
|
|
25
25
|
export const ROLE_KEY = 'role';
|
|
26
26
|
|
|
27
27
|
/** The fixed role inventory. */
|
|
28
|
+
/**
|
|
29
|
+
* Layer config key naming the metadata keys a token born of a SPLIT inherits
|
|
30
|
+
* from the token it came from. Plaid honors it without knowing what the keys
|
|
31
|
+
* mean; see the manual's `Metadata Preserved Across a Split`. An app declares
|
|
32
|
+
* it so that a split in ANY app, including one that has never heard of these
|
|
33
|
+
* keys, does not silently drop them.
|
|
34
|
+
*/
|
|
35
|
+
export const PRESERVE_ON_SPLIT_KEY = 'preserveOnSplit';
|
|
36
|
+
|
|
28
37
|
export const ROLES = Object.freeze({
|
|
29
38
|
BASELINE: 'baseline',
|
|
30
39
|
SENTENCE: 'sentence',
|
package/src/serviceSchema.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* parameters: [ // ordered; rendered into a form
|
|
22
22
|
* { key, label, type, description?, default?, required?,
|
|
23
23
|
* options?: [{value, label}], // enum / multiselect
|
|
24
|
-
* min?, max?, step?,
|
|
24
|
+
* min?, max?, step?, slider?, // number (slider: drag, don't type)
|
|
25
25
|
* placeholder?, multiline? } // string
|
|
26
26
|
* ]
|
|
27
27
|
* }
|
|
@@ -52,7 +52,17 @@ export const TASKS = Object.freeze({
|
|
|
52
52
|
ANALYZE: "analyze",
|
|
53
53
|
/** Propose a free translation for each sentence. */
|
|
54
54
|
TRANSLATE: "translate",
|
|
55
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Propose time-aligned speech regions for a recording.
|
|
57
|
+
*
|
|
58
|
+
* The odd one out: a detect-speech service RETURNS its regions and writes
|
|
59
|
+
* nothing, because a time-aligned segment is a stretch of the baseline and
|
|
60
|
+
* cannot exist without text. The response carries
|
|
61
|
+
* `{segments: [{timeBegin, timeEnd, speaker?}]}` (seconds), and the app
|
|
62
|
+
* holds them as proposals until a person types into one.
|
|
63
|
+
*/
|
|
64
|
+
DETECT_SPEECH: "detect-speech",
|
|
65
|
+
/** A conversational assistant over a project (chat turns; see plaid-agent). */
|
|
56
66
|
ASSIST: "assist",
|
|
57
67
|
});
|
|
58
68
|
|
package/src/services.js
CHANGED
|
@@ -147,31 +147,63 @@ export function serve(client, projectId, serviceInfo, onServiceRequest, extras =
|
|
|
147
147
|
? { ...payload.data, requesterId }
|
|
148
148
|
: payload.data;
|
|
149
149
|
|
|
150
|
+
const scope = createCancelScope(() => cancelled.get(requestId) === true);
|
|
150
151
|
const responseHelper = {
|
|
151
152
|
requestId,
|
|
152
153
|
requesterId,
|
|
153
154
|
// True once the requester asked for the request to stop.
|
|
154
155
|
get cancelled() {
|
|
155
|
-
return cancelled
|
|
156
|
+
return scope.cancelled;
|
|
156
157
|
},
|
|
158
|
+
raiseIfCancelled: () => scope.raiseIfCancelled(),
|
|
159
|
+
critical: (fn) => scope.critical(fn),
|
|
157
160
|
// Extra fields ride in the progress payload (a chat service sends the
|
|
158
161
|
// reply text so far as `text`).
|
|
159
|
-
|
|
160
|
-
|
|
162
|
+
//
|
|
163
|
+
// This is a CANCELLATION CHECKPOINT: it throws ServiceCancelled if the
|
|
164
|
+
// requester has stopped the request, so a service that reports progress
|
|
165
|
+
// through its work is cancellable without doing anything else. Use
|
|
166
|
+
// `critical()` around writes to hold it off.
|
|
167
|
+
progress: (percent, msg, extra) => {
|
|
168
|
+
scope.raiseIfCancelled();
|
|
169
|
+
return reportEvent(requestId, {
|
|
170
|
+
status: 'progress',
|
|
171
|
+
progress: { percent, message: msg, ...extra },
|
|
172
|
+
});
|
|
173
|
+
},
|
|
161
174
|
complete: (data) => {
|
|
162
175
|
finished();
|
|
163
176
|
return reportEvent(requestId, { status: 'completed', data });
|
|
164
177
|
},
|
|
178
|
+
// Asked to stop, and it did. Reported as a result carrying
|
|
179
|
+
// `stopped: true`, so a client can tell "you stopped it" from "it broke".
|
|
180
|
+
stopped: (data) => {
|
|
181
|
+
finished();
|
|
182
|
+
return reportEvent(requestId, {
|
|
183
|
+
status: 'completed',
|
|
184
|
+
data: { ...(data || {}), stopped: true },
|
|
185
|
+
});
|
|
186
|
+
},
|
|
165
187
|
error: (error) => {
|
|
166
188
|
finished();
|
|
167
189
|
return reportEvent(requestId, { status: 'error', data: { error: error?.message || error } });
|
|
168
190
|
},
|
|
169
191
|
};
|
|
170
192
|
|
|
193
|
+
// A handler may be async, so a rejected promise has to be caught too, or a
|
|
194
|
+
// cancelled async service would report a failure instead of a stop.
|
|
195
|
+
const settle = (error) => {
|
|
196
|
+
if (error instanceof ServiceCancelled || error?.name === 'ServiceCancelled') {
|
|
197
|
+
responseHelper.stopped();
|
|
198
|
+
} else {
|
|
199
|
+
responseHelper.error(error?.message || error);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
171
202
|
try {
|
|
172
|
-
onServiceRequest(data, responseHelper);
|
|
203
|
+
const maybe = onServiceRequest(data, responseHelper);
|
|
204
|
+
if (maybe && typeof maybe.catch === 'function') maybe.catch(settle);
|
|
173
205
|
} catch (error) {
|
|
174
|
-
|
|
206
|
+
settle(error);
|
|
175
207
|
}
|
|
176
208
|
};
|
|
177
209
|
const openChannel = () => client.messages.listen(projectId, onChannelEvent, channelPath);
|
|
@@ -204,7 +236,7 @@ export function serve(client, projectId, serviceInfo, onServiceRequest, extras =
|
|
|
204
236
|
*
|
|
205
237
|
* Streams the service's progress + result back over a single server-mediated
|
|
206
238
|
* response (no broadcast). Rejects if no service is connected (503), if the
|
|
207
|
-
* service reports an error, or
|
|
239
|
+
* service reports an error, or after `timeout` of SILENCE.
|
|
208
240
|
*
|
|
209
241
|
* The request outlives this call: after a timeout, an abort, or a dropped
|
|
210
242
|
* connection the service goes on, and `attachServiceRequest` collects the
|
|
@@ -213,11 +245,15 @@ export function serve(client, projectId, serviceInfo, onServiceRequest, extras =
|
|
|
213
245
|
* to know the id before submitting; submitting an id that names a request
|
|
214
246
|
* you already made rejoins it instead of starting another.
|
|
215
247
|
*
|
|
248
|
+
* Errors that leave the request alive carry `pending: true`: a timeout, a
|
|
249
|
+
* stop, a dropped connection. An error without it is the end of the request.
|
|
250
|
+
*
|
|
216
251
|
* @param {Object} client - PlaidClient instance
|
|
217
252
|
* @param {string} projectId - Project UUID
|
|
218
253
|
* @param {string} serviceId - Service ID to request
|
|
219
254
|
* @param {any} data - Request payload
|
|
220
|
-
* @param {number} [timeout=10000] -
|
|
255
|
+
* @param {number} [timeout=10000] - How long the service may say NOTHING, in ms.
|
|
256
|
+
* Every event it sends starts the clock again, so this does not cap a long run
|
|
221
257
|
* @param {function} [onProgress] - Called with each progress payload {percent, message}
|
|
222
258
|
* @param {AbortSignal} [signal] - Abort to stop waiting; rejects with an AbortError
|
|
223
259
|
* @param {Object} [opts]
|
|
@@ -259,12 +295,14 @@ export function requestService(client, projectId, serviceId, data, timeout = 100
|
|
|
259
295
|
* the latest progress is replayed, then the result comes, or at once if the
|
|
260
296
|
* request already finished. Rejects with an error whose `status` is 404 when
|
|
261
297
|
* the request is unknown or expired (the server keeps a finished request's
|
|
262
|
-
* result for a while, not forever).
|
|
298
|
+
* result for a while, not forever). As with `requestService`, `timeout` is
|
|
299
|
+
* how long the service may be silent, and an error carrying `pending: true`
|
|
300
|
+
* means the request is still there to rejoin again.
|
|
263
301
|
*
|
|
264
302
|
* @param {Object} client - PlaidClient instance
|
|
265
303
|
* @param {string} projectId - Project UUID
|
|
266
304
|
* @param {string} requestId - The request id (from `onAccepted` or your own)
|
|
267
|
-
* @param {number} [timeout=10000] -
|
|
305
|
+
* @param {number} [timeout=10000] - How long the service may say NOTHING, in ms
|
|
268
306
|
* @param {function} [onProgress] - Called with each progress payload {percent, message}
|
|
269
307
|
* @param {AbortSignal} [signal] - Abort to stop waiting; rejects with an AbortError
|
|
270
308
|
* @returns {Promise<any>} The service's result
|
|
@@ -293,10 +331,68 @@ export function attachServiceRequest(client, projectId, requestId, timeout = 100
|
|
|
293
331
|
* @param {string} requestId - The request id
|
|
294
332
|
* @returns {Promise<void>}
|
|
295
333
|
*/
|
|
334
|
+
/**
|
|
335
|
+
* Thrown inside a handler when the requester has asked it to stop.
|
|
336
|
+
*
|
|
337
|
+
* Cooperative cancellation: nothing interrupts a handler, so the request ends
|
|
338
|
+
* at the next point the handler looks. `responseHelper.progress` is that
|
|
339
|
+
* point, which is why a long service that already reports progress needs no
|
|
340
|
+
* changes at all. `serve` catches this and ends the request as STOPPED rather
|
|
341
|
+
* than failed.
|
|
342
|
+
*/
|
|
343
|
+
export class ServiceCancelled extends Error {
|
|
344
|
+
constructor(message = 'The requester stopped this request') {
|
|
345
|
+
super(message);
|
|
346
|
+
this.name = 'ServiceCancelled';
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* The cancellation half of a responseHelper: a flag, a depth counter, and one
|
|
352
|
+
* rule about when a checkpoint may raise. Split from the transport so it can
|
|
353
|
+
* be reasoned about on its own. Mirrors `CancelScope` in the Python client.
|
|
354
|
+
*/
|
|
355
|
+
export function createCancelScope(isCancelled) {
|
|
356
|
+
// Depth of nested `critical()` blocks; while non-zero, a checkpoint will
|
|
357
|
+
// not throw.
|
|
358
|
+
let criticalDepth = 0;
|
|
359
|
+
return {
|
|
360
|
+
get cancelled() {
|
|
361
|
+
return !!isCancelled();
|
|
362
|
+
},
|
|
363
|
+
/** Stop here if the requester has asked the request to stop. */
|
|
364
|
+
raiseIfCancelled() {
|
|
365
|
+
if (isCancelled() && criticalDepth === 0) throw new ServiceCancelled();
|
|
366
|
+
},
|
|
367
|
+
/**
|
|
368
|
+
* Run `fn` as a stretch that must finish once begun, usually the writes.
|
|
369
|
+
* Checkpoints inside do not throw, so a half-written document is never
|
|
370
|
+
* left behind; cancellation takes effect at the first checkpoint after.
|
|
371
|
+
*/
|
|
372
|
+
async critical(fn) {
|
|
373
|
+
criticalDepth++;
|
|
374
|
+
try {
|
|
375
|
+
return await fn();
|
|
376
|
+
} finally {
|
|
377
|
+
criticalDepth--;
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
296
383
|
export function cancelServiceRequest(client, projectId, requestId) {
|
|
297
384
|
return client._request('DELETE', `/api/v1/projects/${projectId}/service-requests/${encodeURIComponent(requestId)}`);
|
|
298
385
|
}
|
|
299
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Mark an error as one that leaves the request ALIVE on the server: a timeout,
|
|
389
|
+
* a stop, a dropped connection. The service goes on working and its result
|
|
390
|
+
* still waits under the request id, so a caller holding that id can rejoin it
|
|
391
|
+
* with `attachServiceRequest`, and must not forget the id the way it forgets
|
|
392
|
+
* a request that really ended. An error without `pending` is terminal.
|
|
393
|
+
*/
|
|
394
|
+
const stillRunning = (err) => Object.assign(err, { pending: true });
|
|
395
|
+
|
|
300
396
|
/**
|
|
301
397
|
* Open a request stream (submit or attach) and read it to its terminal
|
|
302
398
|
* event: `accepted` names the request, `progress` events go to `onProgress`,
|
|
@@ -313,10 +409,23 @@ function streamServiceRequest(client, { url, method, body, onStatus, what }, tim
|
|
|
313
409
|
abortController.abort();
|
|
314
410
|
fn(arg);
|
|
315
411
|
};
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
412
|
+
|
|
413
|
+
// `timeout` is IDLE time, meaning how long the service may say nothing. It
|
|
414
|
+
// is not a deadline on the run: every event from the server restarts it,
|
|
415
|
+
// because a service that is reporting its progress is not hung. As a
|
|
416
|
+
// deadline on the whole run it killed working transcriptions at the
|
|
417
|
+
// five-minute default and left their writes to land on a document the page
|
|
418
|
+
// had already handed back to the user as editable.
|
|
419
|
+
let timer;
|
|
420
|
+
const waitAgain = () => {
|
|
421
|
+
clearTimeout(timer);
|
|
422
|
+
timer = setTimeout(
|
|
423
|
+
() =>
|
|
424
|
+
finish(reject, stillRunning(new Error(`${what} timed out after ${timeout}ms of silence`))),
|
|
425
|
+
timeout,
|
|
426
|
+
);
|
|
427
|
+
};
|
|
428
|
+
waitAgain();
|
|
320
429
|
|
|
321
430
|
// An external signal stops waiting on a long request (a UI's Stop button).
|
|
322
431
|
// Reject with an AbortError so a caller can tell a deliberate stop from a
|
|
@@ -325,7 +434,7 @@ function streamServiceRequest(client, { url, method, body, onStatus, what }, tim
|
|
|
325
434
|
const stop = () => {
|
|
326
435
|
const err = new Error('The service request was stopped');
|
|
327
436
|
err.name = 'AbortError';
|
|
328
|
-
finish(reject, err);
|
|
437
|
+
finish(reject, stillRunning(err));
|
|
329
438
|
};
|
|
330
439
|
if (signal) {
|
|
331
440
|
if (signal.aborted) {
|
|
@@ -349,7 +458,9 @@ function streamServiceRequest(client, { url, method, body, onStatus, what }, tim
|
|
|
349
458
|
signal: abortController.signal,
|
|
350
459
|
});
|
|
351
460
|
} catch (error) {
|
|
352
|
-
|
|
461
|
+
// The POST may or may not have reached the server. Treat it as alive:
|
|
462
|
+
// rejoining a request that was never made simply 404s.
|
|
463
|
+
if (error.name !== 'AbortError') finish(reject, stillRunning(new Error(`${what} could not be sent: ${error.message}`)));
|
|
353
464
|
return;
|
|
354
465
|
}
|
|
355
466
|
|
|
@@ -386,6 +497,7 @@ function streamServiceRequest(client, { url, method, body, onStatus, what }, tim
|
|
|
386
497
|
dataLine = line.slice(6);
|
|
387
498
|
} else if (line === '' && eventType && dataLine) {
|
|
388
499
|
const payload = transformResponse(JSON.parse(dataLine));
|
|
500
|
+
waitAgain(); // it spoke, so it is not hung
|
|
389
501
|
if (eventType === 'accepted') {
|
|
390
502
|
if (onAccepted) { try { onAccepted(payload.requestId); } catch (_) { /* ignore */ } }
|
|
391
503
|
} else if (eventType === 'progress') {
|
|
@@ -402,9 +514,9 @@ function streamServiceRequest(client, { url, method, body, onStatus, what }, tim
|
|
|
402
514
|
}
|
|
403
515
|
}
|
|
404
516
|
}
|
|
405
|
-
finish(reject, new Error('Service closed the connection without a result'));
|
|
517
|
+
finish(reject, stillRunning(new Error('Service closed the connection without a result')));
|
|
406
518
|
} catch (error) {
|
|
407
|
-
if (error.name !== 'AbortError') finish(reject, new Error(`${what} stream error: ${error.message}`));
|
|
519
|
+
if (error.name !== 'AbortError') finish(reject, stillRunning(new Error(`${what} stream error: ${error.message}`)));
|
|
408
520
|
}
|
|
409
521
|
})();
|
|
410
522
|
});
|