@larc-iu/plaid-client 0.2.0-alpha.20 → 0.2.0-alpha.22
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 +28 -27
- package/package.json +1 -1
- package/src/constraints.js +0 -11
- package/src/documentLock.js +54 -71
- package/src/http.js +59 -19
- package/src/index.js +188 -44
- package/src/metadataOps.js +4 -2
- package/src/provenance.js +0 -3
- package/src/roles.js +4 -13
- package/src/serviceSchema.js +5 -23
- package/src/services.js +88 -8
- package/src/textEdits.js +18 -2
package/src/serviceSchema.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* {
|
|
18
18
|
* schemaVersion: 1,
|
|
19
|
-
* tasks: ["tokenize"], // controlled vocab
|
|
19
|
+
* tasks: ["tokenize"], // controlled vocab (TASKS)
|
|
20
20
|
* summary: "## markdown …", // rich human description
|
|
21
21
|
* parameters: [ // ordered; rendered into a form
|
|
22
22
|
* { key, label, type, description?, default?, required?,
|
|
@@ -63,8 +63,8 @@ export const TASKS = Object.freeze({
|
|
|
63
63
|
*
|
|
64
64
|
* Apart from `analyze` on purpose. A task is the CONTRACT a spot offers a
|
|
65
65
|
* service against, and these two share almost none of it: an analyze
|
|
66
|
-
* request names the three token layers plus the language
|
|
67
|
-
*
|
|
66
|
+
* request names the three token layers plus the language and metalanguage,
|
|
67
|
+
* and writes morphemes and glosses. Both claimed `analyze` for
|
|
68
68
|
* a while, so every app offering an analyze spot offered the other app's
|
|
69
69
|
* services, which could not answer the request they would have been given.
|
|
70
70
|
*/
|
|
@@ -93,32 +93,14 @@ export const TASKS = Object.freeze({
|
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
|
-
*
|
|
97
|
-
* declared `tasks` array. Drop once all services advertise `tasks`.
|
|
98
|
-
*/
|
|
99
|
-
const LEGACY_TASK_PREFIXES = Object.freeze({
|
|
100
|
-
[TASKS.TOKENIZE]: "tok:",
|
|
101
|
-
[TASKS.TRANSCRIBE]: "asr:",
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Does `service` serve `task`? Prefers the declared `extras.tasks` array; falls
|
|
106
|
-
* back to the legacy id-prefix convention for un-migrated services.
|
|
96
|
+
* Does `service` serve `task`, as its declared `extras.tasks` says?
|
|
107
97
|
* @param {{serviceId?: string, extras?: {tasks?: string[]}}} service
|
|
108
98
|
* @param {string} task one of TASKS
|
|
109
99
|
* @returns {boolean}
|
|
110
100
|
*/
|
|
111
101
|
export function servesTask(service, task) {
|
|
112
102
|
const declared = service?.extras?.tasks;
|
|
113
|
-
|
|
114
|
-
return declared.includes(task);
|
|
115
|
-
}
|
|
116
|
-
const prefix = LEGACY_TASK_PREFIXES[task];
|
|
117
|
-
return (
|
|
118
|
-
!!prefix &&
|
|
119
|
-
typeof service?.serviceId === "string" &&
|
|
120
|
-
service.serviceId.startsWith(prefix)
|
|
121
|
-
);
|
|
103
|
+
return Array.isArray(declared) && declared.includes(task);
|
|
122
104
|
}
|
|
123
105
|
|
|
124
106
|
/**
|
package/src/services.js
CHANGED
|
@@ -434,14 +434,18 @@ export function serve(client, projectId, serviceInfo, onServiceRequest, extras =
|
|
|
434
434
|
* you already made rejoins it instead of starting another.
|
|
435
435
|
*
|
|
436
436
|
* Errors that leave the request alive carry `pending: true`: a timeout, a
|
|
437
|
-
* stop, a dropped connection. An error without it is
|
|
437
|
+
* stop, a dropped connection, a proxy's 502 or 504. An error without it is
|
|
438
|
+
* the end of the request. With `opts.requestId`, a request whose answer went
|
|
439
|
+
* missing (a dropped connection, a 502 or 504) is rejoined by that id a few
|
|
440
|
+
* times before the error is given.
|
|
438
441
|
*
|
|
439
442
|
* @param {Object} client - PlaidClient instance
|
|
440
443
|
* @param {string} projectId - Project UUID
|
|
441
444
|
* @param {string} serviceId - Service ID to request
|
|
442
445
|
* @param {any} data - Request payload
|
|
443
446
|
* @param {number} [timeout=10000] - How long the service may say NOTHING, in ms.
|
|
444
|
-
* Every event it sends starts the clock again, so this does not cap a long run
|
|
447
|
+
* Every event it sends starts the clock again, so this does not cap a long run.
|
|
448
|
+
* 0, a negative number or null: no limit (wait until `signal` stops it)
|
|
445
449
|
* @param {function} [onProgress] - Called with each progress payload {percent, message}
|
|
446
450
|
* @param {AbortSignal} [signal] - Abort to stop waiting; rejects with an AbortError
|
|
447
451
|
* @param {Object} [opts]
|
|
@@ -485,14 +489,21 @@ export function requestService(client, projectId, serviceId, data, timeout = 100
|
|
|
485
489
|
if (opts.requestId) query.set('request-id', opts.requestId);
|
|
486
490
|
if (opts.projectIds?.length) query.set('project-ids', opts.projectIds.join(','));
|
|
487
491
|
const qs = String(query) ? `?${query}` : '';
|
|
488
|
-
|
|
492
|
+
const submit = () => streamServiceRequest(
|
|
489
493
|
client,
|
|
490
494
|
{
|
|
491
495
|
url: `${client.baseUrl}/api/v1/projects/${projectId}/services/${encodeURIComponent(serviceId)}/requests${qs}`,
|
|
492
496
|
method: 'POST',
|
|
493
497
|
body: JSON.stringify(payload === undefined ? null : transformRequest(payload)),
|
|
498
|
+
// `notLive` (with `serviceId`) lets an app say it in the service's own
|
|
499
|
+
// name rather than its id.
|
|
494
500
|
onStatus: (response) =>
|
|
495
|
-
response.status === 503
|
|
501
|
+
response.status === 503
|
|
502
|
+
? Object.assign(new Error(`No live service '${serviceId}' on this project`), {
|
|
503
|
+
notLive: true,
|
|
504
|
+
serviceId,
|
|
505
|
+
})
|
|
506
|
+
: null,
|
|
496
507
|
what: 'Service request',
|
|
497
508
|
},
|
|
498
509
|
timeout,
|
|
@@ -500,6 +511,14 @@ export function requestService(client, projectId, serviceId, data, timeout = 100
|
|
|
500
511
|
signal,
|
|
501
512
|
opts.onAccepted,
|
|
502
513
|
);
|
|
514
|
+
// A request whose id is known is rejoined when its answer goes missing, so
|
|
515
|
+
// a run the service took and finished is not reported failed.
|
|
516
|
+
if (!opts.requestId) return submit();
|
|
517
|
+
return rejoinLost(
|
|
518
|
+
submit,
|
|
519
|
+
() => attachServiceRequest(client, projectId, opts.requestId, timeout, onProgress, signal),
|
|
520
|
+
signal,
|
|
521
|
+
);
|
|
503
522
|
}
|
|
504
523
|
|
|
505
524
|
/**
|
|
@@ -515,6 +534,7 @@ export function requestService(client, projectId, serviceId, data, timeout = 100
|
|
|
515
534
|
* @param {string} projectId - Project UUID
|
|
516
535
|
* @param {string} requestId - The request id (from `onAccepted` or your own)
|
|
517
536
|
* @param {number} [timeout=10000] - How long the service may say NOTHING, in ms
|
|
537
|
+
* (0, a negative number or null: no limit)
|
|
518
538
|
* @param {function} [onProgress] - Called with each progress payload {percent, message}
|
|
519
539
|
* @param {AbortSignal} [signal] - Abort to stop waiting; rejects with an AbortError
|
|
520
540
|
* @returns {Promise<any>} The service's result
|
|
@@ -612,6 +632,62 @@ export function cancelServiceRequest(client, projectId, requestId) {
|
|
|
612
632
|
*/
|
|
613
633
|
const stillRunning = (err) => Object.assign(err, { pending: true });
|
|
614
634
|
|
|
635
|
+
/**
|
|
636
|
+
* A still-running error whose cause is that the ANSWER went missing: the
|
|
637
|
+
* request could not be sent or read to its end, or a proxy answered for the
|
|
638
|
+
* server (502, 504). Not a timeout or a stop, which are the caller's own
|
|
639
|
+
* choice to stop waiting. A submit whose answer was lost rejoins its request
|
|
640
|
+
* (`rejoinLost`).
|
|
641
|
+
*/
|
|
642
|
+
const lostAnswer = (err) => Object.assign(stillRunning(err), { lostAnswer: true });
|
|
643
|
+
|
|
644
|
+
/** How long to wait before each attempt to rejoin a request whose answer was lost. */
|
|
645
|
+
const REJOIN_DELAYS_MS = [1000, 3000, 9000];
|
|
646
|
+
|
|
647
|
+
const delay = (ms, signal) =>
|
|
648
|
+
new Promise((resolve) => {
|
|
649
|
+
if (signal?.aborted) return resolve();
|
|
650
|
+
const timer = setTimeout(resolve, ms);
|
|
651
|
+
signal?.addEventListener(
|
|
652
|
+
'abort',
|
|
653
|
+
() => {
|
|
654
|
+
clearTimeout(timer);
|
|
655
|
+
resolve();
|
|
656
|
+
},
|
|
657
|
+
{ once: true },
|
|
658
|
+
);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* `submit()` again, by attaching to the request `requestId` names, while its
|
|
663
|
+
* answer is the one that went missing. The request was taken, or never
|
|
664
|
+
* reached the server: attaching follows a taken one to its end, and a 404
|
|
665
|
+
* says the server never had it, so the first error stands. Each attempt
|
|
666
|
+
* whose answer goes missing too waits a little longer before the next.
|
|
667
|
+
*/
|
|
668
|
+
async function rejoinLost(submit, attach, signal) {
|
|
669
|
+
let first;
|
|
670
|
+
try {
|
|
671
|
+
return await submit();
|
|
672
|
+
} catch (err) {
|
|
673
|
+
if (!err?.lostAnswer) throw err;
|
|
674
|
+
first = err;
|
|
675
|
+
}
|
|
676
|
+
let last = first;
|
|
677
|
+
for (const ms of REJOIN_DELAYS_MS) {
|
|
678
|
+
await delay(ms, signal);
|
|
679
|
+
if (signal?.aborted) throw last;
|
|
680
|
+
try {
|
|
681
|
+
return await attach();
|
|
682
|
+
} catch (err) {
|
|
683
|
+
if (err?.status === 404) throw first;
|
|
684
|
+
if (!err?.lostAnswer) throw err;
|
|
685
|
+
last = err;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
throw last;
|
|
689
|
+
}
|
|
690
|
+
|
|
615
691
|
/**
|
|
616
692
|
* Open a request stream (submit or attach) and read it to its terminal
|
|
617
693
|
* event: `accepted` names the request, `progress` events go to `onProgress`,
|
|
@@ -635,9 +711,11 @@ function streamServiceRequest(client, { url, method, body, onStatus, what }, tim
|
|
|
635
711
|
// deadline on the whole run it killed working transcriptions at the
|
|
636
712
|
// five-minute default and left their writes to land on a document the page
|
|
637
713
|
// had already handed back to the user as editable.
|
|
714
|
+
// 0, a negative number or null is no limit, as for every other timeout.
|
|
638
715
|
let timer;
|
|
639
716
|
const waitAgain = () => {
|
|
640
717
|
clearTimeout(timer);
|
|
718
|
+
if (!(timeout > 0)) return;
|
|
641
719
|
timer = setTimeout(
|
|
642
720
|
() =>
|
|
643
721
|
finish(reject, stillRunning(new Error(`${what} timed out after ${timeout}ms of silence`))),
|
|
@@ -679,7 +757,7 @@ function streamServiceRequest(client, { url, method, body, onStatus, what }, tim
|
|
|
679
757
|
} catch (error) {
|
|
680
758
|
// The POST may or may not have reached the server. Treat it as alive:
|
|
681
759
|
// rejoining a request that was never made simply 404s.
|
|
682
|
-
if (error.name !== 'AbortError') finish(reject,
|
|
760
|
+
if (error.name !== 'AbortError') finish(reject, lostAnswer(new Error(`${what} could not be sent: ${error.message}`)));
|
|
683
761
|
return;
|
|
684
762
|
}
|
|
685
763
|
|
|
@@ -691,7 +769,9 @@ function streamServiceRequest(client, { url, method, body, onStatus, what }, tim
|
|
|
691
769
|
if (!response.ok) {
|
|
692
770
|
const err = new Error(`${what} failed: HTTP ${response.status} ${response.statusText}`);
|
|
693
771
|
err.status = response.status;
|
|
694
|
-
|
|
772
|
+
// A proxy's 502 or 504 is no answer from the server: the request may
|
|
773
|
+
// have been taken, and the service may be running it.
|
|
774
|
+
finish(reject, response.status === 502 || response.status === 504 ? lostAnswer(err) : err);
|
|
695
775
|
return;
|
|
696
776
|
}
|
|
697
777
|
|
|
@@ -733,9 +813,9 @@ function streamServiceRequest(client, { url, method, body, onStatus, what }, tim
|
|
|
733
813
|
}
|
|
734
814
|
}
|
|
735
815
|
}
|
|
736
|
-
finish(reject,
|
|
816
|
+
finish(reject, lostAnswer(new Error('Service closed the connection without a result')));
|
|
737
817
|
} catch (error) {
|
|
738
|
-
if (error.name !== 'AbortError') finish(reject,
|
|
818
|
+
if (error.name !== 'AbortError') finish(reject, lostAnswer(new Error(`${what} stream error: ${error.message}`)));
|
|
739
819
|
}
|
|
740
820
|
})();
|
|
741
821
|
});
|
package/src/textEdits.js
CHANGED
|
@@ -20,6 +20,22 @@
|
|
|
20
20
|
|
|
21
21
|
const codePoints = (s) => [...s];
|
|
22
22
|
|
|
23
|
+
// The last body split into code points, kept: an edit log composes the same
|
|
24
|
+
// long body once for each of its kept states, and splitting it each time made
|
|
25
|
+
// a save on a long text block the page (H31-TEXT-1). Frozen, as every caller
|
|
26
|
+
// shares it.
|
|
27
|
+
let lastBody = null;
|
|
28
|
+
let lastPoints = Object.freeze([]);
|
|
29
|
+
|
|
30
|
+
/** The code points of `body`, as a frozen array shared with other callers. */
|
|
31
|
+
export function bodyCodePoints(body) {
|
|
32
|
+
if (body !== lastBody) {
|
|
33
|
+
lastPoints = Object.freeze(codePoints(body));
|
|
34
|
+
lastBody = body;
|
|
35
|
+
}
|
|
36
|
+
return lastPoints;
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
function opName(op) {
|
|
24
40
|
return JSON.stringify(op);
|
|
25
41
|
}
|
|
@@ -98,7 +114,7 @@ function cutAt(segs, at) {
|
|
|
98
114
|
* malformed or out-of-bounds op.
|
|
99
115
|
*/
|
|
100
116
|
export function composeTextEdits(body, ops) {
|
|
101
|
-
const old =
|
|
117
|
+
const old = bodyCodePoints(body);
|
|
102
118
|
let segs = old.length ? [{ old: true, start: 0, end: old.length }] : [];
|
|
103
119
|
let len = old.length;
|
|
104
120
|
for (const op of ops) {
|
|
@@ -128,7 +144,7 @@ export function composeTextEdits(body, ops) {
|
|
|
128
144
|
close(seg.start);
|
|
129
145
|
kept = seg.end;
|
|
130
146
|
} else {
|
|
131
|
-
|
|
147
|
+
for (const c of seg.chars) typed.push(c);
|
|
132
148
|
}
|
|
133
149
|
}
|
|
134
150
|
close(old.length);
|