@larc-iu/plaid-client 0.2.0-alpha.23 → 0.2.0-alpha.26

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.
@@ -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`. Goes over the wire even while a batch is
136
- * open on the client.
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 {Object} serviceInfo - {serviceId, serviceName, description}
207
- * @param {function} onServiceRequest - Handler callback (data, responseHelper)
208
- * @param {Object} extras - Optional additional metadata
209
- * @param {function} [onStatus] - Optional callback (event, projectId, detail) for
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 {Object} ServiceRegistration with .stop(), .isRunning(), .isConnected(),
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;
@@ -573,6 +619,8 @@ export class ServiceCancelled extends Error {
573
619
  * The cancellation half of a responseHelper: a flag, a depth counter, and one
574
620
  * rule about when a checkpoint may raise. Split from the transport so it can
575
621
  * be reasoned about on its own. Mirrors `CancelScope` in the Python client.
622
+ * @param {() => boolean} isCancelled
623
+ * @returns {CancelScope}
576
624
  */
577
625
  export function createCancelScope(isCancelled) {
578
626
  // Depth of nested `critical()` blocks; while non-zero, a checkpoint will
@@ -767,8 +815,17 @@ function streamServiceRequest(client, { url, method, body, onStatus, what }, tim
767
815
  return;
768
816
  }
769
817
  if (!response.ok) {
770
- const err = new Error(`${what} failed: HTTP ${response.status} ${response.statusText}`);
818
+ // A refusal says why in its body (`{error}`), as on every other route.
819
+ let said = null;
820
+ try {
821
+ said = await response.json();
822
+ } catch {
823
+ said = null;
824
+ }
825
+ const why = typeof said?.error === 'string' ? `: ${said.error}` : '';
826
+ const err = new Error(`${what} failed: HTTP ${response.status} ${response.statusText}${why}`);
771
827
  err.status = response.status;
828
+ if (said && typeof said === 'object') err.responseData = said;
772
829
  // A proxy's 502 or 504 is no answer from the server: the request may
773
830
  // have been taken, and the service may be running it.
774
831
  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
- /** The code points of `body`, as a frozen array shared with other callers. */
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));
@@ -112,6 +129,9 @@ function cutAt(segs, at) {
112
129
  * is kept between two gaps). Old text deleted and typed back is typed text,
113
130
  * and a gap whose value is the old text it removes is dropped. Throws on a
114
131
  * malformed or out-of-bounds op.
132
+ * @param {string} body
133
+ * @param {TextEditOp[]} ops
134
+ * @returns {TextGap[]}
115
135
  */
116
136
  export function composeTextEdits(body, ops) {
117
137
  const old = bodyCodePoints(body);
@@ -171,6 +191,8 @@ function mergeTyped(segs) {
171
191
  * Gaps (old-body coordinates, sorted, not overlapping) as running ops: an
172
192
  * insert where a gap removes nothing, a delete where it types nothing, and a
173
193
  * replace otherwise.
194
+ * @param {TextGap[]} gaps
195
+ * @returns {TextEditOp[]}
174
196
  */
175
197
  export function gapsToOps(gaps) {
176
198
  const ops = [];
@@ -193,6 +215,9 @@ export function gapsToOps(gaps) {
193
215
  /**
194
216
  * `body` with `ops` applied in turn (running coordinates, code points).
195
217
  * Throws on a malformed or out-of-bounds op.
218
+ * @param {string} body
219
+ * @param {TextEditOp[]} ops
220
+ * @returns {string}
196
221
  */
197
222
  export function applyTextOps(body, ops) {
198
223
  let chars = codePoints(body);
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Convert kebab-case/namespaced key to camelCase.
3
+ * 'layer-id' -> 'layerId'
4
+ * 'relation/layer' -> 'layer' (namespace stripped)
5
+ * Hyphens before a digit are also consumed ('layer-2' -> 'layer2') so no stray
6
+ * hyphen is ever left in the key. (The Python client uses snake_case, where the
7
+ * analogous key is 'layer_2' — the local spelling differs by convention, but
8
+ * neither leaves a separator that doesn't belong to the convention.)
9
+ */
10
+ export function transformKeyToCamel(key: any): any;
11
+ /**
12
+ * Convert camelCase key to kebab-case.
13
+ * 'layerId' -> 'layer-id'
14
+ */
15
+ export function transformKeyFromCamel(key: any): any;
16
+ /**
17
+ * Recursively transform request object keys from camelCase to kebab-case.
18
+ * Preserves `metadata` and `config` contents without transformation.
19
+ */
20
+ export function transformRequest(obj: any): any;
21
+ /**
22
+ * Recursively transform response object keys from kebab-case/namespaced to camelCase.
23
+ * Preserves `metadata` and `config` contents without transformation.
24
+ */
25
+ export function transformResponse(obj: any): any;