@larc-iu/plaid-client 0.2.0-alpha.23 → 0.2.0-alpha.29
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/README.md +4 -0
- package/package.json +11 -5
- package/src/batchRef.d.ts +30 -0
- package/src/codepoint.d.ts +94 -0
- package/src/codepoint.js +131 -1
- package/src/constraints.d.ts +77 -0
- package/src/constraints.js +26 -1
- package/src/created.d.ts +2 -0
- package/src/documentLock.d.ts +158 -0
- package/src/documentLock.js +27 -3
- package/src/events.d.ts +60 -0
- package/src/events.js +7 -3
- package/src/http.d.ts +212 -0
- package/src/http.js +7 -0
- package/src/ids.d.ts +26 -0
- package/src/ids.js +22 -0
- package/src/index.d.ts +4568 -0
- package/src/index.js +1597 -758
- package/src/metadataOps.d.ts +15 -0
- package/src/metadataOps.js +13 -8
- package/src/pagination.d.ts +49 -0
- package/src/pagination.js +2 -19
- package/src/provenance.d.ts +200 -0
- package/src/provenance.js +34 -6
- package/src/replayed.d.ts +3 -0
- package/src/replayed.js +5 -1
- package/src/requester.d.ts +60 -0
- package/src/requester.js +21 -3
- package/src/roles.d.ts +65 -0
- package/src/roles.js +3 -2
- package/src/serviceSchema.d.ts +331 -0
- package/src/serviceSchema.js +75 -9
- package/src/services.d.ts +302 -0
- package/src/services.js +82 -11
- package/src/sse.d.ts +30 -0
- package/src/sse.js +11 -0
- package/src/textEdits.d.ts +59 -0
- package/src/textEdits.js +33 -6
- package/src/transforms.d.ts +25 -0
- package/index.d.ts +0 -2026
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One line about a failure that is safe to show the person who asked.
|
|
3
|
+
*
|
|
4
|
+
* Strips absolute URLs (so an internal host never reaches a requester's screen)
|
|
5
|
+
* and any `secrets` given (a model provider's API key can come back inside its
|
|
6
|
+
* own error text). The whole error still goes to the operator's console: this
|
|
7
|
+
* is the requester's half only. The Python twin is `requester_message`.
|
|
8
|
+
*
|
|
9
|
+
* Only a connection that never opened is "could not be reached". A write whose
|
|
10
|
+
* answer never came (a reset, a timeout) may have been saved, and says so
|
|
11
|
+
* (`UNKNOWN_OUTCOME`).
|
|
12
|
+
*
|
|
13
|
+
* @param {any} error
|
|
14
|
+
* @param {string[]} [secrets]
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
export function requesterMessage(error: any, secrets?: string[]): string;
|
|
18
|
+
/**
|
|
19
|
+
* What the requester is told when a request fails, named by the service.
|
|
20
|
+
*
|
|
21
|
+
* Every path that reports a failed request goes through here, so a raw message
|
|
22
|
+
* cannot reach a requester from one of them. (Python's twin additionally lets a
|
|
23
|
+
* `ValueError` through unprefixed, as an authored refusal; JS has no
|
|
24
|
+
* distinguished refusal type, so every failure is named by the service.)
|
|
25
|
+
*
|
|
26
|
+
* @param {any} error
|
|
27
|
+
* @param {string} [serviceName]
|
|
28
|
+
* @param {string[]} [secrets]
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
export function serviceErrorMessage(error: any, serviceName?: string, secrets?: string[]): string;
|
|
32
|
+
/**
|
|
33
|
+
* Discover the services seen on a project — a synchronous GET. Returns every
|
|
34
|
+
* service ever registered on the project: currently connected ones carry
|
|
35
|
+
* `online: true`; previously-seen offline ones carry `online: false` plus a
|
|
36
|
+
* `lastSeenAt` stamp. Callers that need a service they can actually submit
|
|
37
|
+
* work to should filter on `online`. A connected service also names who runs
|
|
38
|
+
* it, as the caller sees it: `runnerName` (the runner's display name),
|
|
39
|
+
* `runByYou`, and `servesYou`, whether it would take the caller's requests on
|
|
40
|
+
* this project. A delegating service serves members other than its runner
|
|
41
|
+
* only where the runner is a maintainer or an admin. Goes over the wire even
|
|
42
|
+
* while a batch is open on the client.
|
|
43
|
+
*
|
|
44
|
+
* @param {Object} client - PlaidClient instance
|
|
45
|
+
* @param {string} projectId - Project UUID
|
|
46
|
+
* @returns {Promise<Array>} [{serviceId, serviceName, description, extras, online, lastSeenAt, runnerName, runByYou, servesYou}]
|
|
47
|
+
*/
|
|
48
|
+
export function discoverServices(client: any, projectId: string): Promise<any[]>;
|
|
49
|
+
/**
|
|
50
|
+
* Forget a previously-seen (offline) service: removes its row from the
|
|
51
|
+
* project's persistent registry. Maintainer-only; 409 if the service is
|
|
52
|
+
* currently connected (it would just re-register).
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} client - PlaidClient instance
|
|
55
|
+
* @param {string} projectId - Project UUID
|
|
56
|
+
* @param {string} serviceId - Service ID to forget
|
|
57
|
+
* @returns {Promise<void>}
|
|
58
|
+
*/
|
|
59
|
+
export function discardService(client: any, projectId: string, serviceId: string): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* POST a progress, result or error event for an in-flight request; the server
|
|
62
|
+
* relays it to the waiting requester. The Python twin is `_report_event`.
|
|
63
|
+
*
|
|
64
|
+
* outOfBand: these are signals to whoever is waiting, not writes to the
|
|
65
|
+
* project (see the note at the top of http.js). A service may report them on
|
|
66
|
+
* the batch it is filling, where queueing them would hold every one back until
|
|
67
|
+
* submit, deliver none at all if the batch aborts, and take slots in the
|
|
68
|
+
* batch's results. The requester would hear nothing and wait out its idle
|
|
69
|
+
* timeout on work that had already finished.
|
|
70
|
+
*
|
|
71
|
+
* @param {Object} client - PlaidClient instance
|
|
72
|
+
* @param {string} projectId - Project UUID
|
|
73
|
+
* @param {string} requestId - The request id
|
|
74
|
+
* @param {Object} body - The event payload
|
|
75
|
+
* @returns {Promise<any>}
|
|
76
|
+
*/
|
|
77
|
+
export function reportRequestEvent(client: any, projectId: string, requestId: string, body: any): Promise<any>;
|
|
78
|
+
/**
|
|
79
|
+
* The cancellation half of a responseHelper, on its own.
|
|
80
|
+
* @typedef {object} CancelScope
|
|
81
|
+
* @property {boolean} cancelled True once the requester has asked this request
|
|
82
|
+
* to stop.
|
|
83
|
+
* @property {() => void} raiseIfCancelled Stop here if the requester has asked
|
|
84
|
+
* the request to stop.
|
|
85
|
+
* @property {<T>(fn: () => T | Promise<T>) => Promise<T>} critical Run `fn` as
|
|
86
|
+
* a stretch that must finish once begun, usually the writes.
|
|
87
|
+
*/
|
|
88
|
+
/**
|
|
89
|
+
* What a `serve` handler gets for each request.
|
|
90
|
+
* @typedef {CancelScope & {
|
|
91
|
+
* requestId: string,
|
|
92
|
+
* requesterId: string | null,
|
|
93
|
+
* progress(percent: number, message: string, extra?: Record<string, any>): void,
|
|
94
|
+
* complete(data: any): void,
|
|
95
|
+
* stopped(data?: any): void,
|
|
96
|
+
* error(error: string | Error): void,
|
|
97
|
+
* }} ResponseHelper `progress` is also a cancellation checkpoint: it throws
|
|
98
|
+
* `ServiceCancelled` once a stop has been asked for. `stopped` ends the
|
|
99
|
+
* request because it was asked to stop, and its result carries `stopped: true`.
|
|
100
|
+
*/
|
|
101
|
+
/**
|
|
102
|
+
* A connection-state transition of a service registration. "stopped": the
|
|
103
|
+
* server refused the channel for a reason retrying cannot fix (a revoked
|
|
104
|
+
* token, no write access, no such project), which ends the registration.
|
|
105
|
+
* @typedef {"registered" | "reconnected" | "disconnected" | "stopped"} ServiceStatusEvent
|
|
106
|
+
*/
|
|
107
|
+
/**
|
|
108
|
+
* What `serve` returns.
|
|
109
|
+
* @typedef {object} ServiceRegistration
|
|
110
|
+
* @property {() => void} stop
|
|
111
|
+
* @property {() => boolean} isRunning Still serving, including while retrying
|
|
112
|
+
* a channel through a server restart.
|
|
113
|
+
* @property {() => boolean} isConnected Channel open right now, so the server
|
|
114
|
+
* sees this service as online.
|
|
115
|
+
* @property {import("./serviceSchema.js").ServiceInfo & {extras: any}} serviceInfo
|
|
116
|
+
*/
|
|
117
|
+
/**
|
|
118
|
+
* Register a service and handle incoming work requests.
|
|
119
|
+
*
|
|
120
|
+
* Opens the service's dedicated request channel — which registers it for
|
|
121
|
+
* discovery (presence = open channel) — and handles work on it. For each
|
|
122
|
+
* request, runs `onServiceRequest(data, responseHelper)` where `responseHelper`
|
|
123
|
+
* has `progress(percent, msg)` / `complete(data)` / `error(err)`.
|
|
124
|
+
*
|
|
125
|
+
* @param {Object} client - PlaidClient instance
|
|
126
|
+
* @param {string} projectId - Project UUID
|
|
127
|
+
* @param {import("./serviceSchema.js").ServiceInfo} serviceInfo - {serviceId, serviceName, description}
|
|
128
|
+
* @param {(data: any, responseHelper: ResponseHelper) => void} onServiceRequest - Handler callback (data, responseHelper)
|
|
129
|
+
* @param {Object} [extras] - Optional additional metadata
|
|
130
|
+
* @param {((event: ServiceStatusEvent, projectId: string, detail?: string) => void) | null} [onStatus] - Optional callback (event, projectId, detail) for
|
|
131
|
+
* connection-state transitions: 'registered', 'reconnected', 'disconnected',
|
|
132
|
+
* and 'stopped' when the server refuses the channel for a reason retrying
|
|
133
|
+
* cannot fix (see PERMANENT_STATUSES), which ends the registration.
|
|
134
|
+
* Called once per transition, not once per retry.
|
|
135
|
+
* @returns {ServiceRegistration}
|
|
136
|
+
*/
|
|
137
|
+
export function serve(client: any, projectId: string, serviceInfo: import("./serviceSchema.js").ServiceInfo, onServiceRequest: (data: any, responseHelper: ResponseHelper) => void, extras?: any, onStatus?: ((event: ServiceStatusEvent, projectId: string, detail?: string) => void) | null): ServiceRegistration;
|
|
138
|
+
/**
|
|
139
|
+
* Submit work to a service and await its result.
|
|
140
|
+
*
|
|
141
|
+
* Streams the service's progress + result back over a single server-mediated
|
|
142
|
+
* response (no broadcast). Rejects if no service is connected (503), if the
|
|
143
|
+
* service reports an error, or after `timeout` of SILENCE.
|
|
144
|
+
*
|
|
145
|
+
* The request outlives this call: after a timeout, an abort, or a dropped
|
|
146
|
+
* connection the service goes on, and `attachServiceRequest` collects the
|
|
147
|
+
* result given the request id, which `opts.onAccepted` receives as soon as
|
|
148
|
+
* the server has taken the request. Pass `opts.requestId` (a UUID you mint)
|
|
149
|
+
* to know the id before submitting; submitting an id that names a request
|
|
150
|
+
* you already made rejoins it instead of starting another.
|
|
151
|
+
*
|
|
152
|
+
* Errors that leave the request alive carry `pending: true`: a timeout, a
|
|
153
|
+
* stop, a dropped connection, a proxy's 502 or 504. An error without it is
|
|
154
|
+
* the end of the request. With `opts.requestId`, a request whose answer went
|
|
155
|
+
* missing (a dropped connection, a 502 or 504) is rejoined by that id a few
|
|
156
|
+
* times before the error is given.
|
|
157
|
+
*
|
|
158
|
+
* @param {Object} client - PlaidClient instance
|
|
159
|
+
* @param {string} projectId - Project UUID
|
|
160
|
+
* @param {string} serviceId - Service ID to request
|
|
161
|
+
* @param {any} data - Request payload
|
|
162
|
+
* @param {number} [timeout=10000] - How long the service may say NOTHING, in ms.
|
|
163
|
+
* Every event it sends starts the clock again, so this does not cap a long run.
|
|
164
|
+
* 0, a negative number or null: no limit (wait until `signal` stops it)
|
|
165
|
+
* @param {function} [onProgress] - Called with each progress payload {percent, message}
|
|
166
|
+
* @param {AbortSignal} [signal] - Abort to stop waiting; rejects with an AbortError
|
|
167
|
+
* @param {Object} [opts]
|
|
168
|
+
* @param {string} [opts.requestId] - A client-minted request id (UUID)
|
|
169
|
+
* @param {function} [opts.onAccepted] - Called with the request id once the server has it
|
|
170
|
+
* @param {string[]} [opts.projectIds] - Other projects the request is about, beside
|
|
171
|
+
* `projectId`. A delegating service's token is scoped to `projectId` and to those of
|
|
172
|
+
* these the requester can read, and reaches nothing else
|
|
173
|
+
* @param {boolean} [opts.noOperation] - Carry no open operation: the service's writes
|
|
174
|
+
* are a group of their own, not part of whatever operation this client has open
|
|
175
|
+
* @returns {Promise<any>} The service's result
|
|
176
|
+
*/
|
|
177
|
+
export function requestService(client: any, projectId: string, serviceId: string, data: any, timeout?: number, onProgress?: Function, signal?: AbortSignal, opts?: {
|
|
178
|
+
requestId?: string;
|
|
179
|
+
onAccepted?: Function;
|
|
180
|
+
projectIds?: string[];
|
|
181
|
+
noOperation?: boolean;
|
|
182
|
+
}): Promise<any>;
|
|
183
|
+
/**
|
|
184
|
+
* Rejoin a service request made earlier (by this user) and await its result:
|
|
185
|
+
* the latest progress is replayed, then the result comes, or at once if the
|
|
186
|
+
* request already finished. Rejects with an error whose `status` is 404 when
|
|
187
|
+
* the request is unknown or expired (the server keeps a finished request's
|
|
188
|
+
* result for a while, not forever). As with `requestService`, `timeout` is
|
|
189
|
+
* how long the service may be silent, and an error carrying `pending: true`
|
|
190
|
+
* means the request is still there to rejoin again.
|
|
191
|
+
*
|
|
192
|
+
* @param {Object} client - PlaidClient instance
|
|
193
|
+
* @param {string} projectId - Project UUID
|
|
194
|
+
* @param {string} requestId - The request id (from `onAccepted` or your own)
|
|
195
|
+
* @param {number} [timeout=10000] - How long the service may say NOTHING, in ms
|
|
196
|
+
* (0, a negative number or null: no limit)
|
|
197
|
+
* @param {function} [onProgress] - Called with each progress payload {percent, message}
|
|
198
|
+
* @param {AbortSignal} [signal] - Abort to stop waiting; rejects with an AbortError
|
|
199
|
+
* @returns {Promise<any>} The service's result
|
|
200
|
+
*/
|
|
201
|
+
export function attachServiceRequest(client: any, projectId: string, requestId: string, timeout?: number, onProgress?: Function, signal?: AbortSignal): Promise<any>;
|
|
202
|
+
/**
|
|
203
|
+
* The cancellation half of a responseHelper: a flag, a depth counter, and one
|
|
204
|
+
* rule about when a checkpoint may raise. Split from the transport so it can
|
|
205
|
+
* be reasoned about on its own. Mirrors `CancelScope` in the Python client.
|
|
206
|
+
* @param {() => boolean} isCancelled
|
|
207
|
+
* @returns {CancelScope}
|
|
208
|
+
*/
|
|
209
|
+
export function createCancelScope(isCancelled: () => boolean): CancelScope;
|
|
210
|
+
/**
|
|
211
|
+
* Ask the service to stop a request made earlier (by this user). The request
|
|
212
|
+
* still ends with whatever the service then reports, on the stream of whoever
|
|
213
|
+
* is awaiting it. Rejects with 404 if unknown or expired, 409 once finished.
|
|
214
|
+
*
|
|
215
|
+
* outOfBand: a DELETE, but not a write. It signals a running service and
|
|
216
|
+
* changes no project data (see the note at the top of http.js).
|
|
217
|
+
*
|
|
218
|
+
* @param {Object} client - PlaidClient instance
|
|
219
|
+
* @param {string} projectId - Project UUID
|
|
220
|
+
* @param {string} requestId - The request id
|
|
221
|
+
* @returns {Promise<void>}
|
|
222
|
+
*/
|
|
223
|
+
export function cancelServiceRequest(client: any, projectId: string, requestId: string): Promise<void>;
|
|
224
|
+
/**
|
|
225
|
+
* What a requester is told when an error carries no message of its own.
|
|
226
|
+
* Naming the JS class instead would say nothing they can act on.
|
|
227
|
+
*/
|
|
228
|
+
export const UNKNOWN_FAILURE: "The service could not finish this request.";
|
|
229
|
+
/** A write sent to the server whose answer never came back. */
|
|
230
|
+
export const UNKNOWN_OUTCOME: "The Plaid server did not answer. This change may or may not have been saved.";
|
|
231
|
+
/** The server was never reached, so nothing was sent. */
|
|
232
|
+
export const UNREACHABLE: "The Plaid server could not be reached.";
|
|
233
|
+
/**
|
|
234
|
+
* Thrown inside a handler when the requester has asked it to stop.
|
|
235
|
+
*
|
|
236
|
+
* Cooperative cancellation: nothing interrupts a handler, so the request ends
|
|
237
|
+
* at the next point the handler looks. `responseHelper.progress` is that
|
|
238
|
+
* point, which is why a long service that already reports progress needs no
|
|
239
|
+
* changes at all. `serve` catches this and ends the request as STOPPED rather
|
|
240
|
+
* than failed.
|
|
241
|
+
*/
|
|
242
|
+
export class ServiceCancelled extends Error {
|
|
243
|
+
constructor(message?: string);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* The cancellation half of a responseHelper, on its own.
|
|
247
|
+
*/
|
|
248
|
+
export type CancelScope = {
|
|
249
|
+
/**
|
|
250
|
+
* True once the requester has asked this request
|
|
251
|
+
* to stop.
|
|
252
|
+
*/
|
|
253
|
+
cancelled: boolean;
|
|
254
|
+
/**
|
|
255
|
+
* Stop here if the requester has asked
|
|
256
|
+
* the request to stop.
|
|
257
|
+
*/
|
|
258
|
+
raiseIfCancelled: () => void;
|
|
259
|
+
/**
|
|
260
|
+
* Run `fn` as
|
|
261
|
+
* a stretch that must finish once begun, usually the writes.
|
|
262
|
+
*/
|
|
263
|
+
critical: <T>(fn: () => T | Promise<T>) => Promise<T>;
|
|
264
|
+
};
|
|
265
|
+
/**
|
|
266
|
+
* `progress` is also a cancellation checkpoint: it throws
|
|
267
|
+
* `ServiceCancelled` once a stop has been asked for. `stopped` ends the
|
|
268
|
+
* request because it was asked to stop, and its result carries `stopped: true`.
|
|
269
|
+
*/
|
|
270
|
+
export type ResponseHelper = CancelScope & {
|
|
271
|
+
requestId: string;
|
|
272
|
+
requesterId: string | null;
|
|
273
|
+
progress(percent: number, message: string, extra?: Record<string, any>): void;
|
|
274
|
+
complete(data: any): void;
|
|
275
|
+
stopped(data?: any): void;
|
|
276
|
+
error(error: string | Error): void;
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* A connection-state transition of a service registration. "stopped": the
|
|
280
|
+
* server refused the channel for a reason retrying cannot fix (a revoked
|
|
281
|
+
* token, no write access, no such project), which ends the registration.
|
|
282
|
+
*/
|
|
283
|
+
export type ServiceStatusEvent = "registered" | "reconnected" | "disconnected" | "stopped";
|
|
284
|
+
/**
|
|
285
|
+
* What `serve` returns.
|
|
286
|
+
*/
|
|
287
|
+
export type ServiceRegistration = {
|
|
288
|
+
stop: () => void;
|
|
289
|
+
/**
|
|
290
|
+
* Still serving, including while retrying
|
|
291
|
+
* a channel through a server restart.
|
|
292
|
+
*/
|
|
293
|
+
isRunning: () => boolean;
|
|
294
|
+
/**
|
|
295
|
+
* Channel open right now, so the server
|
|
296
|
+
* sees this service as online.
|
|
297
|
+
*/
|
|
298
|
+
isConnected: () => boolean;
|
|
299
|
+
serviceInfo: import("./serviceSchema.js").ServiceInfo & {
|
|
300
|
+
extras: any;
|
|
301
|
+
};
|
|
302
|
+
};
|
package/src/services.js
CHANGED
|
@@ -132,12 +132,16 @@ export function serviceErrorMessage(error, serviceName = '', secrets = []) {
|
|
|
132
132
|
* service ever registered on the project: currently connected ones carry
|
|
133
133
|
* `online: true`; previously-seen offline ones carry `online: false` plus a
|
|
134
134
|
* `lastSeenAt` stamp. Callers that need a service they can actually submit
|
|
135
|
-
* work to should filter on `online`.
|
|
136
|
-
*
|
|
135
|
+
* work to should filter on `online`. A connected service also names who runs
|
|
136
|
+
* it, as the caller sees it: `runnerName` (the runner's display name),
|
|
137
|
+
* `runByYou`, and `servesYou`, whether it would take the caller's requests on
|
|
138
|
+
* this project. A delegating service serves members other than its runner
|
|
139
|
+
* only where the runner is a maintainer or an admin. Goes over the wire even
|
|
140
|
+
* while a batch is open on the client.
|
|
137
141
|
*
|
|
138
142
|
* @param {Object} client - PlaidClient instance
|
|
139
143
|
* @param {string} projectId - Project UUID
|
|
140
|
-
* @returns {Promise<Array>} [{serviceId, serviceName, description, extras, online, lastSeenAt}]
|
|
144
|
+
* @returns {Promise<Array>} [{serviceId, serviceName, description, extras, online, lastSeenAt, runnerName, runByYou, servesYou}]
|
|
141
145
|
*/
|
|
142
146
|
export function discoverServices(client, projectId) {
|
|
143
147
|
return client._request('GET', `/api/v1/projects/${projectId}/services`);
|
|
@@ -193,6 +197,49 @@ export function reportRequestEvent(client, projectId, requestId, body) {
|
|
|
193
197
|
*/
|
|
194
198
|
const PERMANENT_STATUSES = new Set([400, 401, 403, 404, 405, 422]);
|
|
195
199
|
|
|
200
|
+
/**
|
|
201
|
+
* The cancellation half of a responseHelper, on its own.
|
|
202
|
+
* @typedef {object} CancelScope
|
|
203
|
+
* @property {boolean} cancelled True once the requester has asked this request
|
|
204
|
+
* to stop.
|
|
205
|
+
* @property {() => void} raiseIfCancelled Stop here if the requester has asked
|
|
206
|
+
* the request to stop.
|
|
207
|
+
* @property {<T>(fn: () => T | Promise<T>) => Promise<T>} critical Run `fn` as
|
|
208
|
+
* a stretch that must finish once begun, usually the writes.
|
|
209
|
+
*/
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* What a `serve` handler gets for each request.
|
|
213
|
+
* @typedef {CancelScope & {
|
|
214
|
+
* requestId: string,
|
|
215
|
+
* requesterId: string | null,
|
|
216
|
+
* progress(percent: number, message: string, extra?: Record<string, any>): void,
|
|
217
|
+
* complete(data: any): void,
|
|
218
|
+
* stopped(data?: any): void,
|
|
219
|
+
* error(error: string | Error): void,
|
|
220
|
+
* }} ResponseHelper `progress` is also a cancellation checkpoint: it throws
|
|
221
|
+
* `ServiceCancelled` once a stop has been asked for. `stopped` ends the
|
|
222
|
+
* request because it was asked to stop, and its result carries `stopped: true`.
|
|
223
|
+
*/
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* A connection-state transition of a service registration. "stopped": the
|
|
227
|
+
* server refused the channel for a reason retrying cannot fix (a revoked
|
|
228
|
+
* token, no write access, no such project), which ends the registration.
|
|
229
|
+
* @typedef {"registered" | "reconnected" | "disconnected" | "stopped"} ServiceStatusEvent
|
|
230
|
+
*/
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* What `serve` returns.
|
|
234
|
+
* @typedef {object} ServiceRegistration
|
|
235
|
+
* @property {() => void} stop
|
|
236
|
+
* @property {() => boolean} isRunning Still serving, including while retrying
|
|
237
|
+
* a channel through a server restart.
|
|
238
|
+
* @property {() => boolean} isConnected Channel open right now, so the server
|
|
239
|
+
* sees this service as online.
|
|
240
|
+
* @property {import("./serviceSchema.js").ServiceInfo & {extras: any}} serviceInfo
|
|
241
|
+
*/
|
|
242
|
+
|
|
196
243
|
/**
|
|
197
244
|
* Register a service and handle incoming work requests.
|
|
198
245
|
*
|
|
@@ -203,16 +250,15 @@ const PERMANENT_STATUSES = new Set([400, 401, 403, 404, 405, 422]);
|
|
|
203
250
|
*
|
|
204
251
|
* @param {Object} client - PlaidClient instance
|
|
205
252
|
* @param {string} projectId - Project UUID
|
|
206
|
-
* @param {
|
|
207
|
-
* @param {
|
|
208
|
-
* @param {Object} extras - Optional additional metadata
|
|
209
|
-
* @param {
|
|
253
|
+
* @param {import("./serviceSchema.js").ServiceInfo} serviceInfo - {serviceId, serviceName, description}
|
|
254
|
+
* @param {(data: any, responseHelper: ResponseHelper) => void} onServiceRequest - Handler callback (data, responseHelper)
|
|
255
|
+
* @param {Object} [extras] - Optional additional metadata
|
|
256
|
+
* @param {((event: ServiceStatusEvent, projectId: string, detail?: string) => void) | null} [onStatus] - Optional callback (event, projectId, detail) for
|
|
210
257
|
* connection-state transitions: 'registered', 'reconnected', 'disconnected',
|
|
211
258
|
* and 'stopped' when the server refuses the channel for a reason retrying
|
|
212
259
|
* cannot fix (see PERMANENT_STATUSES), which ends the registration.
|
|
213
260
|
* Called once per transition, not once per retry.
|
|
214
|
-
* @returns {
|
|
215
|
-
* .serviceInfo
|
|
261
|
+
* @returns {ServiceRegistration}
|
|
216
262
|
*/
|
|
217
263
|
export function serve(client, projectId, serviceInfo, onServiceRequest, extras = {}, onStatus = null) {
|
|
218
264
|
const { serviceId, serviceName, description = '' } = serviceInfo;
|
|
@@ -327,11 +373,25 @@ export function serve(client, projectId, serviceInfo, onServiceRequest, extras =
|
|
|
327
373
|
// requester has stopped the request, so a service that reports progress
|
|
328
374
|
// through its work is cancellable without doing anything else. Use
|
|
329
375
|
// `critical()` around writes to hold it off.
|
|
376
|
+
//
|
|
377
|
+
// A request the server answers 404 for is one it no longer knows (it
|
|
378
|
+
// ended, or the server restarted and forgot it), so nobody is waiting
|
|
379
|
+
// for the work: it counts as a stop, and the next checkpoint throws.
|
|
380
|
+
// Any other failure to send is only logged.
|
|
330
381
|
progress: (percent, msg, extra) => {
|
|
331
382
|
scope.raiseIfCancelled();
|
|
332
|
-
return
|
|
383
|
+
return reportRequestEvent(client, projectId, requestId, {
|
|
333
384
|
status: 'progress',
|
|
334
385
|
progress: { percent, message: msg, ...extra },
|
|
386
|
+
}).catch((error) => {
|
|
387
|
+
if (error?.status === 404) {
|
|
388
|
+
if (cancelled.get(requestId) === false) {
|
|
389
|
+
console.warn(`The server no longer knows request ${requestId}, so it is stopped`);
|
|
390
|
+
cancelled.set(requestId, true);
|
|
391
|
+
}
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
console.warn('Failed to report request event:', error.message || error);
|
|
335
395
|
});
|
|
336
396
|
},
|
|
337
397
|
complete: (data) => {
|
|
@@ -573,6 +633,8 @@ export class ServiceCancelled extends Error {
|
|
|
573
633
|
* The cancellation half of a responseHelper: a flag, a depth counter, and one
|
|
574
634
|
* rule about when a checkpoint may raise. Split from the transport so it can
|
|
575
635
|
* be reasoned about on its own. Mirrors `CancelScope` in the Python client.
|
|
636
|
+
* @param {() => boolean} isCancelled
|
|
637
|
+
* @returns {CancelScope}
|
|
576
638
|
*/
|
|
577
639
|
export function createCancelScope(isCancelled) {
|
|
578
640
|
// Depth of nested `critical()` blocks; while non-zero, a checkpoint will
|
|
@@ -767,8 +829,17 @@ function streamServiceRequest(client, { url, method, body, onStatus, what }, tim
|
|
|
767
829
|
return;
|
|
768
830
|
}
|
|
769
831
|
if (!response.ok) {
|
|
770
|
-
|
|
832
|
+
// A refusal says why in its body (`{error}`), as on every other route.
|
|
833
|
+
let said = null;
|
|
834
|
+
try {
|
|
835
|
+
said = await response.json();
|
|
836
|
+
} catch {
|
|
837
|
+
said = null;
|
|
838
|
+
}
|
|
839
|
+
const why = typeof said?.error === 'string' ? `: ${said.error}` : '';
|
|
840
|
+
const err = new Error(`${what} failed: HTTP ${response.status} ${response.statusText}${why}`);
|
|
771
841
|
err.status = response.status;
|
|
842
|
+
if (said && typeof said === 'object') err.responseData = said;
|
|
772
843
|
// A proxy's 502 or 504 is no answer from the server: the request may
|
|
773
844
|
// have been taken, and the service may be running it.
|
|
774
845
|
finish(reject, response.status === 502 || response.status === 504 ? lostAnswer(err) : err);
|
package/src/sse.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project a parsed SSE `data:` object onto what a listener callback sees.
|
|
3
|
+
*
|
|
4
|
+
* The envelope is API surface and gets the usual `transformResponse` recasing.
|
|
5
|
+
* A `message` event's `data` is NOT: it is opaque application data (whatever
|
|
6
|
+
* `sendMessage` was handed), so its keys must survive verbatim, exactly as
|
|
7
|
+
* `metadata` and `config` do elsewhere. Without this, a key written
|
|
8
|
+
* `case-marker` reaches a JavaScript listener as `caseMarker` and a Python
|
|
9
|
+
* listener as `case_marker`, so the same broadcast reads differently depending
|
|
10
|
+
* on the reader's language.
|
|
11
|
+
*/
|
|
12
|
+
export function eventPayload(eventType: any, parsed: any): any;
|
|
13
|
+
/**
|
|
14
|
+
* Create an SSE connection to the listen endpoint using fetch-based streaming.
|
|
15
|
+
* Automatically handles heartbeat confirmations and event parsing.
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} client - PlaidClient instance
|
|
18
|
+
* @param {string} projectId - Project UUID
|
|
19
|
+
* @param {function} onEvent - Callback (eventType, data). Return true to stop.
|
|
20
|
+
* @param {string} [path] - Stream path under baseUrl. Defaults to the project
|
|
21
|
+
* /listen bus; service request channels pass their own. Only /listen emits
|
|
22
|
+
* `heartbeat` events needing a POST confirmation — other streams keep
|
|
23
|
+
* themselves alive with ignored SSE comments.
|
|
24
|
+
* @returns {Object} SSE connection with .close(), .getStats(), .readyState,
|
|
25
|
+
* and .error: once CLOSED, why (null when this side closed it). A refused
|
|
26
|
+
* stream's error carries the HTTP `status`, and a stream the server ended
|
|
27
|
+
* carries an error named `StreamClosed`.
|
|
28
|
+
*/
|
|
29
|
+
export function createSSEConnection(client: any, projectId: string, onEvent: Function, path?: string): any;
|
|
30
|
+
export const SSE_IDLE_TIMEOUT_MS: 60000;
|
package/src/sse.js
CHANGED
|
@@ -36,6 +36,12 @@ export function eventPayload(eventType, parsed) {
|
|
|
36
36
|
return payload;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Tell the client a stream of its own dropped or opened (a stand-in client in
|
|
40
|
+
// a test may not listen).
|
|
41
|
+
function tell(client, hook) {
|
|
42
|
+
try { client[hook]?.(); } catch (_) { /* the stream must not die of it */ }
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
/**
|
|
40
46
|
* Create an SSE connection to the listen endpoint using fetch-based streaming.
|
|
41
47
|
* Automatically handles heartbeat confirmations and event parsing.
|
|
@@ -147,6 +153,8 @@ export function createSSEConnection(client, projectId, onEvent, path) {
|
|
|
147
153
|
isConnected = true;
|
|
148
154
|
sseConnection.readyState = 1; // OPEN
|
|
149
155
|
lastWarning.delete(url);
|
|
156
|
+
// After a drop the server may have restarted with other limits.
|
|
157
|
+
tell(client, '_noteStreamOpened');
|
|
150
158
|
|
|
151
159
|
const reader = response.body.getReader();
|
|
152
160
|
const decoder = new TextDecoder();
|
|
@@ -219,10 +227,13 @@ export function createSSEConnection(client, projectId, onEvent, path) {
|
|
|
219
227
|
}
|
|
220
228
|
}
|
|
221
229
|
} finally {
|
|
230
|
+
// Ended without a close() of this side's own: a drop.
|
|
231
|
+
const dropped = !isClosed;
|
|
222
232
|
clearIdleTimer();
|
|
223
233
|
isConnected = false;
|
|
224
234
|
isClosed = true;
|
|
225
235
|
sseConnection.readyState = 2; // CLOSED
|
|
236
|
+
if (dropped) tell(client, '_noteStreamDropped');
|
|
226
237
|
}
|
|
227
238
|
})();
|
|
228
239
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The code points of `body`, as a frozen array shared with other callers.
|
|
3
|
+
* @param {string} body
|
|
4
|
+
* @returns {readonly string[]}
|
|
5
|
+
*/
|
|
6
|
+
export function bodyCodePoints(body: string): readonly string[];
|
|
7
|
+
/**
|
|
8
|
+
* The net change of `ops` applied in turn to `body`, as gaps in the old
|
|
9
|
+
* body's code points: sorted, and never touching (at least one old code point
|
|
10
|
+
* is kept between two gaps). Old text deleted and typed back is typed text,
|
|
11
|
+
* and a gap whose value is the old text it removes is dropped. Throws on a
|
|
12
|
+
* malformed or out-of-bounds op.
|
|
13
|
+
* @param {string} body
|
|
14
|
+
* @param {TextEditOp[]} ops
|
|
15
|
+
* @returns {TextGap[]}
|
|
16
|
+
*/
|
|
17
|
+
export function composeTextEdits(body: string, ops: TextEditOp[]): TextGap[];
|
|
18
|
+
/**
|
|
19
|
+
* Gaps (old-body coordinates, sorted, not overlapping) as running ops: an
|
|
20
|
+
* insert where a gap removes nothing, a delete where it types nothing, and a
|
|
21
|
+
* replace otherwise.
|
|
22
|
+
* @param {TextGap[]} gaps
|
|
23
|
+
* @returns {TextEditOp[]}
|
|
24
|
+
*/
|
|
25
|
+
export function gapsToOps(gaps: TextGap[]): TextEditOp[];
|
|
26
|
+
/**
|
|
27
|
+
* `body` with `ops` applied in turn (running coordinates, code points).
|
|
28
|
+
* Throws on a malformed or out-of-bounds op.
|
|
29
|
+
* @param {string} body
|
|
30
|
+
* @param {TextEditOp[]} ops
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
export function applyTextOps(body: string, ops: TextEditOp[]): string;
|
|
34
|
+
/**
|
|
35
|
+
* One op as `PATCH /texts/:id` takes it. Each index is in the body the ops
|
|
36
|
+
* before it left.
|
|
37
|
+
*/
|
|
38
|
+
export type TextEditOp = {
|
|
39
|
+
type: "insert";
|
|
40
|
+
index: number;
|
|
41
|
+
value: string;
|
|
42
|
+
} | {
|
|
43
|
+
type: "delete";
|
|
44
|
+
index: number;
|
|
45
|
+
value: number;
|
|
46
|
+
} | {
|
|
47
|
+
type: "replace";
|
|
48
|
+
index: number;
|
|
49
|
+
length: number;
|
|
50
|
+
value: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Old-body code points [start, end) give way to `value`.
|
|
54
|
+
*/
|
|
55
|
+
export type TextGap = {
|
|
56
|
+
start: number;
|
|
57
|
+
end: number;
|
|
58
|
+
value: string;
|
|
59
|
+
};
|
package/src/textEdits.js
CHANGED
|
@@ -18,6 +18,19 @@
|
|
|
18
18
|
* `plaid_client.text_edits` is the twin of this module.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* One op as `PATCH /texts/:id` takes it. Each index is in the body the ops
|
|
23
|
+
* before it left.
|
|
24
|
+
* @typedef {{type: "insert", index: number, value: string}
|
|
25
|
+
* | {type: "delete", index: number, value: number}
|
|
26
|
+
* | {type: "replace", index: number, length: number, value: string}} TextEditOp
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Old-body code points [start, end) give way to `value`.
|
|
31
|
+
* @typedef {{start: number, end: number, value: string}} TextGap
|
|
32
|
+
*/
|
|
33
|
+
|
|
21
34
|
const codePoints = (s) => [...s];
|
|
22
35
|
|
|
23
36
|
// The last body split into code points, kept: an edit log composes the same
|
|
@@ -27,7 +40,11 @@ const codePoints = (s) => [...s];
|
|
|
27
40
|
let lastBody = null;
|
|
28
41
|
let lastPoints = Object.freeze([]);
|
|
29
42
|
|
|
30
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* The code points of `body`, as a frozen array shared with other callers.
|
|
45
|
+
* @param {string} body
|
|
46
|
+
* @returns {readonly string[]}
|
|
47
|
+
*/
|
|
31
48
|
export function bodyCodePoints(body) {
|
|
32
49
|
if (body !== lastBody) {
|
|
33
50
|
lastPoints = Object.freeze(codePoints(body));
|
|
@@ -36,9 +53,8 @@ export function bodyCodePoints(body) {
|
|
|
36
53
|
return lastPoints;
|
|
37
54
|
}
|
|
38
55
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
56
|
+
// The op as a refusal names it.
|
|
57
|
+
const opJson = (op) => String(JSON.stringify(op) ?? 'null');
|
|
42
58
|
|
|
43
59
|
/**
|
|
44
60
|
* The op's type after checking its shape and bounds against a body of `len`
|
|
@@ -54,11 +70,14 @@ function checkOp(op, len) {
|
|
|
54
70
|
(type === 'replace' && int(index) && int(length) && typeof value === 'string');
|
|
55
71
|
if (!wellFormed) {
|
|
56
72
|
throw new Error(
|
|
57
|
-
`Malformed text edit operation: ${
|
|
73
|
+
`Malformed text edit operation: ${opJson(op)}. Expected {type: "insert", index: int, ` +
|
|
58
74
|
`value: string}, {type: "delete", index: int, value: int} or {type: "replace", ` +
|
|
59
75
|
`index: int, length: int, value: string}`,
|
|
60
76
|
);
|
|
61
77
|
}
|
|
78
|
+
if (!(op.side == null || op.side === 'before' || op.side === 'after')) {
|
|
79
|
+
throw new Error(`A text edit's side is "before" or "after": ${opJson(op)}`);
|
|
80
|
+
}
|
|
62
81
|
const inBounds =
|
|
63
82
|
type === 'insert'
|
|
64
83
|
? index >= 0 && index <= len
|
|
@@ -67,7 +86,7 @@ function checkOp(op, len) {
|
|
|
67
86
|
: index >= 0 && length >= 0 && index + length <= len;
|
|
68
87
|
if (!inBounds) {
|
|
69
88
|
throw new Error(
|
|
70
|
-
`Text edit operation out of bounds: ${
|
|
89
|
+
`Text edit operation out of bounds: ${opJson(op)} (text length is ${len} code points)`,
|
|
71
90
|
);
|
|
72
91
|
}
|
|
73
92
|
return type;
|
|
@@ -112,6 +131,9 @@ function cutAt(segs, at) {
|
|
|
112
131
|
* is kept between two gaps). Old text deleted and typed back is typed text,
|
|
113
132
|
* and a gap whose value is the old text it removes is dropped. Throws on a
|
|
114
133
|
* malformed or out-of-bounds op.
|
|
134
|
+
* @param {string} body
|
|
135
|
+
* @param {TextEditOp[]} ops
|
|
136
|
+
* @returns {TextGap[]}
|
|
115
137
|
*/
|
|
116
138
|
export function composeTextEdits(body, ops) {
|
|
117
139
|
const old = bodyCodePoints(body);
|
|
@@ -171,6 +193,8 @@ function mergeTyped(segs) {
|
|
|
171
193
|
* Gaps (old-body coordinates, sorted, not overlapping) as running ops: an
|
|
172
194
|
* insert where a gap removes nothing, a delete where it types nothing, and a
|
|
173
195
|
* replace otherwise.
|
|
196
|
+
* @param {TextGap[]} gaps
|
|
197
|
+
* @returns {TextEditOp[]}
|
|
174
198
|
*/
|
|
175
199
|
export function gapsToOps(gaps) {
|
|
176
200
|
const ops = [];
|
|
@@ -193,6 +217,9 @@ export function gapsToOps(gaps) {
|
|
|
193
217
|
/**
|
|
194
218
|
* `body` with `ops` applied in turn (running coordinates, code points).
|
|
195
219
|
* Throws on a malformed or out-of-bounds op.
|
|
220
|
+
* @param {string} body
|
|
221
|
+
* @param {TextEditOp[]} ops
|
|
222
|
+
* @returns {string}
|
|
196
223
|
*/
|
|
197
224
|
export function applyTextOps(body, ops) {
|
|
198
225
|
let chars = codePoints(body);
|