@larc-iu/plaid-client 0.2.0-alpha.3 → 0.2.0-alpha.30

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.
Files changed (73) hide show
  1. package/README.md +39 -0
  2. package/package.json +13 -6
  3. package/src/apiAnnotations.d.ts +508 -0
  4. package/src/apiAnnotations.js +543 -0
  5. package/src/apiDocuments.d.ts +569 -0
  6. package/src/apiDocuments.js +551 -0
  7. package/src/apiLayers.d.ts +369 -0
  8. package/src/apiLayers.js +222 -0
  9. package/src/apiMessages.d.ts +293 -0
  10. package/src/apiMessages.js +265 -0
  11. package/src/apiPeople.d.ts +626 -0
  12. package/src/apiPeople.js +698 -0
  13. package/src/apiProject.d.ts +761 -0
  14. package/src/apiProject.js +774 -0
  15. package/src/apiServer.d.ts +455 -0
  16. package/src/apiServer.js +422 -0
  17. package/src/apiVocab.d.ts +456 -0
  18. package/src/apiVocab.js +425 -0
  19. package/src/batch.d.ts +54 -0
  20. package/src/batch.js +323 -0
  21. package/src/batchRef.d.ts +30 -0
  22. package/src/batchRef.js +128 -0
  23. package/src/client.d.ts +1321 -0
  24. package/src/client.js +743 -0
  25. package/src/codepoint.d.ts +94 -0
  26. package/src/codepoint.js +131 -1
  27. package/src/constraints.d.ts +85 -0
  28. package/src/constraints.js +112 -0
  29. package/src/created.d.ts +2 -0
  30. package/src/created.js +29 -0
  31. package/src/documentLock.d.ts +172 -0
  32. package/src/documentLock.js +384 -0
  33. package/src/entryForms.d.ts +63 -0
  34. package/src/entryForms.js +152 -0
  35. package/src/events.d.ts +60 -0
  36. package/src/events.js +271 -0
  37. package/src/families.d.ts +349 -0
  38. package/src/families.js +396 -0
  39. package/src/http.d.ts +233 -0
  40. package/src/http.js +809 -93
  41. package/src/ids.d.ts +26 -0
  42. package/src/ids.js +96 -0
  43. package/src/index.d.ts +101 -0
  44. package/src/index.js +183 -2105
  45. package/src/metadataOps.d.ts +15 -0
  46. package/src/metadataOps.js +124 -0
  47. package/src/operations.d.ts +36 -0
  48. package/src/operations.js +164 -0
  49. package/src/pagination.d.ts +49 -0
  50. package/src/pagination.js +20 -53
  51. package/src/provenance.d.ts +243 -0
  52. package/src/provenance.js +458 -36
  53. package/src/replayed.d.ts +3 -0
  54. package/src/replayed.js +31 -0
  55. package/src/requester.d.ts +60 -0
  56. package/src/requester.js +74 -0
  57. package/src/roles.d.ts +104 -0
  58. package/src/roles.js +64 -2
  59. package/src/serviceSchema.d.ts +337 -0
  60. package/src/serviceSchema.js +177 -60
  61. package/src/services.d.ts +319 -0
  62. package/src/services.js +742 -58
  63. package/src/sse.d.ts +30 -0
  64. package/src/sse.js +80 -3
  65. package/src/testing.d.ts +128 -0
  66. package/src/testing.js +379 -0
  67. package/src/textEdits.d.ts +59 -0
  68. package/src/textEdits.js +234 -0
  69. package/src/transforms.d.ts +25 -0
  70. package/src/transforms.js +11 -1
  71. package/src/types.d.ts +867 -0
  72. package/src/types.js +663 -0
  73. package/index.d.ts +0 -532
package/src/client.js ADDED
@@ -0,0 +1,743 @@
1
+ /**
2
+ * PlaidClient: a client's state (token, versions, strict mode, the open
3
+ * operation, the server's facts), its bundles, its batches and logical
4
+ * operations, and the static entry points that make one (login, invites,
5
+ * health, info). The package root (index.js) re-exports it with the rest of
6
+ * the package.
7
+ */
8
+
9
+ import { transformResponse } from "./transforms.js";
10
+ import {
11
+ makeRequest,
12
+ parseErrorBody,
13
+ makeHttpError,
14
+ makeNetworkError,
15
+ timeoutSignal,
16
+ DEFAULT_TIMEOUT_MS,
17
+ DEFAULT_BATCH_TIMEOUT_MS,
18
+ queryTimeout,
19
+ } from "./http.js";
20
+ import { normalizeUserId } from "./ids.js";
21
+ import { openBatch, postBatch } from "./batch.js";
22
+ import { messagesApi } from "./apiMessages.js";
23
+ import { commentsApi, eventsApi, guidelinesApi, operationGroupsApi, projectsApi } from "./apiProject.js";
24
+ import { adminApi, auditApi, serverApi } from "./apiServer.js";
25
+ import { apiTokensApi, authApi, invitesApi, userDataApi, usersApi } from "./apiPeople.js";
26
+ import { documentsApi, textsApi } from "./apiDocuments.js";
27
+ import { vocabItemsApi, vocabLayersApi } from "./apiVocab.js";
28
+ import { relationLayersApi, spanLayersApi, textLayersApi, tokenLayersApi } from "./apiLayers.js";
29
+ import { relationsApi, spansApi, tokensApi, vocabLinksApi } from "./apiAnnotations.js";
30
+ import {
31
+ beginOperation,
32
+ endOperation,
33
+ keySeed,
34
+ withOperation,
35
+ } from "./operations.js";
36
+ import { bodyOf } from "./families.js";
37
+
38
+ // ---- Types --------------------------------------------------------------
39
+ // The shapes the API answers with and takes are in types.js, named here so
40
+ // this module's JSDoc can use them.
41
+
42
+ /**
43
+ * @typedef {import("./types.js").TextEditOp} TextEditOp
44
+ * @typedef {import("./types.js").TextGap} TextGap
45
+ * @typedef {import("./types.js").MetadataOp} MetadataOp
46
+ * @typedef {import("./types.js").LayerConstraint} LayerConstraint
47
+ * @typedef {import("./types.js").ConstraintWriteOptions} ConstraintWriteOptions
48
+ * @typedef {import("./types.js").ConstraintViolation} ConstraintViolation
49
+ * @typedef {import("./types.js").Requester} Requester
50
+ * @typedef {import("./types.js").BatchRef} BatchRef
51
+ * @typedef {import("./types.js").ServiceParam} ServiceParam
52
+ * @typedef {import("./types.js").ServiceInfo} ServiceInfo
53
+ * @typedef {import("./types.js").DiscoveredService} DiscoveredService
54
+ * @typedef {import("./types.js").ResponseHelper} ResponseHelper
55
+ * @typedef {import("./types.js").ServiceRegistration} ServiceRegistration
56
+ * @typedef {import("./types.js").ServiceStatusEvent} ServiceStatusEvent
57
+ * @typedef {import("./types.js").OperationGroupsBundle} OperationGroupsBundle
58
+ * @typedef {import("./types.js").PlaidClientOptions} PlaidClientOptions
59
+ * @typedef {import("./types.js").OperationKind} OperationKind
60
+ * @typedef {import("./types.js").KeySeed} KeySeed
61
+ * @typedef {import("./types.js").PlaidBatch} PlaidBatch
62
+ * @typedef {import("./types.js").AuditPageOptions} AuditPageOptions
63
+ * @typedef {import("./types.js").VocabItemRestoreSummary} VocabItemRestoreSummary
64
+ * @typedef {import("./types.js").UserDataEntry} UserDataEntry
65
+ * @typedef {import("./types.js").AdminUserDataEntry} AdminUserDataEntry
66
+ * @typedef {import("./types.js").ActivityTallyRow} ActivityTallyRow
67
+ * @typedef {import("./types.js").ConstraintRepair} ConstraintRepair
68
+ * @typedef {import("./types.js").ValueSetRule} ValueSetRule
69
+ * @typedef {import("./types.js").RefusedValues} RefusedValues
70
+ * @typedef {import("./types.js").CommentableType} CommentableType
71
+ * @typedef {import("./types.js").Comment} Comment
72
+ * @typedef {import("./types.js").Guideline} Guideline
73
+ * @typedef {import("./types.js").ClientEventType} ClientEventType
74
+ * @typedef {import("./types.js").ClientEvent} ClientEvent
75
+ * @typedef {import("./types.js").Invite} Invite
76
+ * @typedef {import("./types.js").ServerLimits} ServerLimits
77
+ * @typedef {import("./types.js").ServerHealth} ServerHealth
78
+ * @typedef {import("./types.js").RateLimitBucket} RateLimitBucket
79
+ * @typedef {import("./types.js").RateLimitSnapshot} RateLimitSnapshot
80
+ * @typedef {import("./types.js").BackupStatus} BackupStatus
81
+ * @typedef {import("./types.js").ServerReport} ServerReport
82
+ * @typedef {import("./types.js").LiveLog} LiveLog
83
+ * @typedef {import("./types.js").SSEConnection} SSEConnection
84
+ * @typedef {import("./types.js").SSEStats} SSEStats
85
+ * @typedef {import("./types.js").AtomicValue} AtomicValue
86
+ * @typedef {import("./types.js").Created} Created
87
+ * @typedef {import("./types.js").Project} Project
88
+ * @typedef {import("./types.js").ProjectTextLayer} ProjectTextLayer
89
+ * @typedef {import("./types.js").ProjectTokenLayer} ProjectTokenLayer
90
+ * @typedef {import("./types.js").ProjectSpanLayer} ProjectSpanLayer
91
+ * @typedef {import("./types.js").ProjectRelationLayer} ProjectRelationLayer
92
+ * @typedef {import("./types.js").ProjectVocab} ProjectVocab
93
+ * @typedef {import("./types.js").PlaidDocument} PlaidDocument
94
+ * @typedef {import("./types.js").DocumentTextLayer} DocumentTextLayer
95
+ * @typedef {import("./types.js").DocumentTokenLayer} DocumentTokenLayer
96
+ * @typedef {import("./types.js").DocumentSpanLayer} DocumentSpanLayer
97
+ * @typedef {import("./types.js").PlaidText} PlaidText
98
+ * @typedef {import("./types.js").TextReshape} TextReshape
99
+ * @typedef {import("./types.js").Token} Token
100
+ * @typedef {import("./types.js").Span} Span
101
+ * @typedef {import("./types.js").Relation} Relation
102
+ * @typedef {import("./types.js").VocabItem} VocabItem
103
+ * @typedef {import("./types.js").VocabLink} VocabLink
104
+ * @typedef {import("./types.js").VocabLayer} VocabLayer
105
+ * @typedef {import("./types.js").TextLayer} TextLayer
106
+ * @typedef {import("./types.js").TokenLayer} TokenLayer
107
+ * @typedef {import("./types.js").SpanLayer} SpanLayer
108
+ * @typedef {import("./types.js").RelationLayer} RelationLayer
109
+ * @typedef {import("./types.js").MetadataRecords} MetadataRecords
110
+ * @typedef {import("./types.js").LayerRecords} LayerRecords
111
+ * @typedef {import("./types.js").User} User
112
+ * @typedef {import("./types.js").ProjectListEntry} ProjectListEntry
113
+ * @typedef {import("./types.js").DocumentListEntry} DocumentListEntry
114
+ * @typedef {import("./types.js").ApiToken} ApiToken
115
+ * @typedef {import("./types.js").AuditUser} AuditUser
116
+ * @typedef {import("./types.js").AuditTarget} AuditTarget
117
+ * @typedef {import("./types.js").AuditOp} AuditOp
118
+ * @typedef {import("./types.js").AuditEntry} AuditEntry
119
+ * @typedef {import("./types.js").LockGrant} LockGrant
120
+ * @typedef {import("./types.js").DocumentRestoreSummary} DocumentRestoreSummary
121
+ * @typedef {import("./types.js").RestoreCounts} RestoreCounts
122
+ * @typedef {import("./types.js").OperationGroup} OperationGroup
123
+ * @typedef {import("./types.js").QueryResult} QueryResult
124
+ */
125
+
126
+ /**
127
+ * @template [T=any]
128
+ * @typedef {import("./types.js").Page<T>} Page
129
+ */
130
+
131
+ // A request with no Authorization header: login, invite lookup and redemption
132
+ // (POST, with a body), /health and /api/v1/info (GET). Deliberately does not
133
+ // go through PlaidClient._request: these are called before any client exists,
134
+ // which is the whole point.
135
+ async function anonymousRequest(method, baseUrl, path, body, options = {}) {
136
+ const base = baseUrl.replace(/\/$/, "");
137
+ const url = `${base}${path}`;
138
+ try {
139
+ const fetchOptions =
140
+ method === "GET"
141
+ ? { method }
142
+ : {
143
+ method,
144
+ headers: { "Content-Type": "application/json" },
145
+ body: JSON.stringify(body),
146
+ };
147
+ const signal = timeoutSignal(
148
+ options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS,
149
+ );
150
+ if (signal) fetchOptions.signal = signal;
151
+
152
+ const response = await fetch(url, fetchOptions);
153
+ if (!response.ok) {
154
+ throw makeHttpError(response, await parseErrorBody(response), url, method);
155
+ }
156
+ return transformResponse(await response.json());
157
+ } catch (error) {
158
+ if (error.status) throw error;
159
+ throw makeNetworkError(error, url, method);
160
+ }
161
+ }
162
+
163
+ class PlaidClient {
164
+ /**
165
+ * Create a new PlaidClient instance
166
+ * @param {string} baseUrl - The base URL for the API
167
+ * @param {string} token - The authentication token
168
+ * @param {PlaidClientOptions} [options] - Client options
169
+ */
170
+ constructor(baseUrl, token, options = {}) {
171
+ this.baseUrl = baseUrl.replace(/\/$/, "");
172
+ this.token = token;
173
+ /** @type {number | null} */
174
+ this.timeout =
175
+ options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS;
176
+ // Batch submissions get a longer budget than a single request. Aborting a
177
+ // batch does NOT stop the server: it keeps running the transaction and
178
+ // holding the single SQLite write lock, so a client that gives up early
179
+ // just adds a retry on top of a write that is still in progress. Falls
180
+ // back to an explicit `timeout` when one was given.
181
+ /** @type {number | null} */
182
+ this.batchTimeout =
183
+ options.batchTimeout !== undefined
184
+ ? options.batchTimeout
185
+ : options.timeout !== undefined
186
+ ? options.timeout
187
+ : DEFAULT_BATCH_TIMEOUT_MS;
188
+ // Delays (ms) before sending a keyed write again when its answer was lost
189
+ // (no response, 502, 504). See the Idempotency-Key note in http.js.
190
+ /** @type {number[] | undefined} */
191
+ this.retryDelaysMs = options.retryDelaysMs;
192
+ /**
193
+ * The latest version seen for each document written through this client,
194
+ * keyed by document id.
195
+ * @type {Record<string, number>}
196
+ */
197
+ this.documentVersions = {};
198
+ /**
199
+ * True once a response left its version list out
200
+ * (X-Document-Versions-Omitted, past fifty documents). Every version held
201
+ * was then forgotten, and the strict-mode document's is read again before
202
+ * the next write.
203
+ * @type {boolean | undefined}
204
+ */
205
+ this.documentVersionsOmitted = undefined;
206
+ // The server's clock minus this machine's, in ms, from the last response
207
+ // with a Date header (null before one). See serverNow().
208
+ /** @type {number | null} */
209
+ this.serverClockOffsetMs = null;
210
+ /**
211
+ * The document strict mode is entered for, or null.
212
+ * @type {string | null}
213
+ */
214
+ this.strictModeDocumentId = null;
215
+ // Set to a DocumentLockLost while a `documents.locked()` block's keep-alive
216
+ // has failed. Every write throws it until the block exits; see
217
+ // documentLock.js.
218
+ /** @type {import("./documentLock.js").DocumentLockLost | null} */
219
+ this.documentLockLost = null;
220
+ // The keeper of each `documents.locked()` block that renews on a beat, by
221
+ // document id: a write that lands on a held document renewed its lock,
222
+ // and the request layer tells that keeper.
223
+ this.lockKeepers = new Map();
224
+ // The open logical operation (audit-log group), or null. While set, every
225
+ // write is stamped with `?group-id=` (+ `group-message`) so the audit log
226
+ // folds them into ONE expandable entry. See beginOperation / withOperation.
227
+ // Shape: { id, message, depth, written, refined, frames }, where each of
228
+ // `frames` is { keys, count, minted, depth, owned } and the last numbers.
229
+ /**
230
+ * @type {{id: string, message: string | null, kind: OperationKind | null,
231
+ * ref: string | null, depth: number, written: boolean, refined: any,
232
+ * frames: object[]} | null}
233
+ */
234
+ this.operationGroup = null;
235
+ // Optional callback fired once (per client) when any request returns HTTP
236
+ // 401 — i.e. the token is missing/expired/invalid. Apps use it to discard
237
+ // the stored token and route back to login. See makeRequest in http.js.
238
+ /** @type {((error: Error) => void) | null} */
239
+ this.onAuthError = options.onAuthError || null;
240
+ /** @private */
241
+ this._authErrorFired = false;
242
+ // Who hears that a request is being sent again (onRetry).
243
+ /** @private */
244
+ this._retryListeners = new Set();
245
+ // GET /info as last read (`server.info()`), and whether a stream of this
246
+ // client dropped since.
247
+ /** @private */
248
+ this._serverFacts = { info: null, stale: false };
249
+ // The key frame the last beginOperation opened, or null (withOperation).
250
+ /** @private @type {object | null} */
251
+ this._openedFrame = null;
252
+
253
+ this._installResources();
254
+ }
255
+
256
+ /**
257
+ * A stream of this client ended without being closed here: the server may
258
+ * have restarted, with other limits.
259
+ * @private
260
+ */
261
+ _noteStreamDropped() {
262
+ this._serverFacts.stale = true;
263
+ }
264
+
265
+ /**
266
+ * A stream of this client is open. After a drop the limits are read again,
267
+ * once however many streams reopen.
268
+ * @private
269
+ */
270
+ _noteStreamOpened() {
271
+ if (!this._serverFacts.stale) return;
272
+ this.server.refresh().catch(() => {
273
+ // The figures known before stand, and the next open asks again.
274
+ this._serverFacts.stale = true;
275
+ });
276
+ }
277
+
278
+ /**
279
+ * Hear every time a request is sent again: after a 503 (the database was
280
+ * busy), or for a keyed write after its answer was lost (no response, 502,
281
+ * 504). The listener gets `{ attempt, retries, delay, error }` before the
282
+ * wait. A screen that writes through the client directly can say it is
283
+ * retrying while the server does not answer, though the browser is online.
284
+ * @param {(info: {attempt: number, retries: number, delay: number, error: Error}) => void} listener
285
+ * @returns {() => void} the unsubscribe
286
+ */
287
+ onRetry(listener) {
288
+ this._retryListeners.add(listener);
289
+ return () => this._retryListeners.delete(listener);
290
+ }
291
+
292
+ /**
293
+ * Tell every `onRetry` listener. A listener that throws is passed over.
294
+ * @private
295
+ */
296
+ _noteRetry(info) {
297
+ for (const listener of this._retryListeners) {
298
+ try {
299
+ listener(info);
300
+ } catch (_) {
301
+ /* a listener must not stop the retry */
302
+ }
303
+ }
304
+ }
305
+
306
+ /**
307
+ * The server's time now, as its last response's Date header put it (to the
308
+ * second), else this machine's. Judge a time the server stamped, such as an
309
+ * audit entry's `ts`, against this rather than `Date.now()`: a browser
310
+ * clock can be minutes off.
311
+ * @returns {Date}
312
+ */
313
+ serverNow() {
314
+ return new Date(Date.now() + (this.serverClockOffsetMs ?? 0));
315
+ }
316
+
317
+ /**
318
+ * The API bundles (`client.documents`, `client.tokens`, ...). Installed on
319
+ * the client, and again on every batch it opens (see `batch()`): a batch is
320
+ * a view of the client with its own `_request`, so the same bundles built
321
+ * on it queue instead of sending.
322
+ * @private
323
+ */
324
+ _installResources() {
325
+ this.documents = documentsApi(this);
326
+ this.texts = textsApi(this);
327
+
328
+ this.tokens = tokensApi(this);
329
+ this.spans = spansApi(this);
330
+ this.relations = relationsApi(this);
331
+ this.vocabLinks = vocabLinksApi(this);
332
+
333
+ this.textLayers = textLayersApi(this);
334
+ this.tokenLayers = tokenLayersApi(this);
335
+ this.spanLayers = spanLayersApi(this);
336
+ this.relationLayers = relationLayersApi(this);
337
+
338
+ this.vocabLayers = vocabLayersApi(this);
339
+ this.vocabItems = vocabItemsApi(this);
340
+
341
+ this.users = usersApi(this);
342
+ this.apiTokens = apiTokensApi(this);
343
+ this.invites = invitesApi(this);
344
+ this.auth = authApi(this);
345
+ this.userData = userDataApi(this);
346
+
347
+ this.projects = projectsApi(this);
348
+ this.guidelines = guidelinesApi(this);
349
+ this.comments = commentsApi(this);
350
+ this.events = eventsApi(this);
351
+ this.operationGroups = operationGroupsApi(this);
352
+
353
+ this.server = serverApi(this);
354
+ this.admin = adminApi(this);
355
+ this.audit = auditApi(this);
356
+
357
+ this.messages = messagesApi(this);
358
+
359
+ // outOfBand: a query is a read that travels as a POST (see the note at
360
+ // the top of http.js). Made on a batch it goes over the wire like any
361
+ // read, and it never joins a logical operation. It waits past core's own
362
+ // query limit, so a query too broad answers with core's 408, and a 503
363
+ // from core's full queue of large queries is not retried.
364
+ /**
365
+ * Run a query over every project you can read.
366
+ *
367
+ * `body` is the query AST. Its keys follow the usual client convention
368
+ * (camelCase, e.g. `scope.projectIds`) and are converted to the wire
369
+ * format automatically; clause heads and variables are plain strings you
370
+ * write literally (e.g. `'span'`, `'?s1'`, `'vocab-link'`). Example:
371
+ *
372
+ * await client.query({
373
+ * find: ['?s1', '?s2'],
374
+ * where: [
375
+ * ['span', '?s1', { layer: posLayerId, value: 'NOUN' }],
376
+ * ['span', '?s2', { layer: posLayerId, value: 'VERB' }],
377
+ * ['covers', '?s1', '?t1'], ['covers', '?s2', '?t2'],
378
+ * ['precedes', '?t1', '?t2'],
379
+ * ],
380
+ * return: 'entities', // 'ids' (default) | 'entities' | 'count'
381
+ * limit: 100,
382
+ * });
383
+ *
384
+ * A `layer` is referenced by its id (its UUID) only — not by name or
385
+ * path. To match a layer by name, bind it with a `*-layer` clause (e.g.
386
+ * `['span-layer', '?sl', { name: 'pos' }]`) and use the variable.
387
+ *
388
+ * A vocab link's provenance is on the link itself: bind it as a `link`
389
+ * entity to filter or group by it, e.g. the machine-made links nobody
390
+ * confirmed, with the tokens they sit on:
391
+ *
392
+ * ['link', '?l', { metadata: { prov: 'inferred' } }],
393
+ * ['not', ['link', '?l', { metadata: { provConfirmed: true } }]],
394
+ * ['link-token', '?l', '?t'], ['link-item', '?l', '?v']
395
+ *
396
+ * Optional keys: `scope` (restrict to projects by id, `{projectIds}`), `orderBy`
397
+ * (sort rows), and `bindings` (substitute `?name` placeholders with literals).
398
+ * `return` may also be an aggregate spec `{group, aggregates}`. See the query
399
+ * language reference.
400
+ *
401
+ * @param {Object} body - The query AST ({find, where, scope?, limit?, orderBy?,
402
+ * return?, bindings?}).
403
+ * @returns {Promise<QueryResult>} For 'ids'/'entities': {columns, results, count, truncated}.
404
+ * For 'count': {return: 'count', count, truncated}, where `truncated` says
405
+ * the count stopped at the server's cap. Entity cells are full entity
406
+ * objects (same shape as the GET endpoints).
407
+ */
408
+ this.query = (body) =>
409
+ this._request("POST", "/api/v1/query", {
410
+ body,
411
+ outOfBand: true,
412
+ timeout: queryTimeout(this),
413
+ noBusyRetry: true,
414
+ });
415
+ }
416
+
417
+ // --- Core methods ---
418
+
419
+ /** @private */
420
+ async _request(method, path, options = {}) {
421
+ return makeRequest(this, method, path, options);
422
+ }
423
+
424
+ /**
425
+ * Enter strict mode for a specific document, requiring document version
426
+ * headers so that conflicting concurrent writes are rejected.
427
+ * @param {string} documentId - The ID of the document to track versions for
428
+ */
429
+ enterStrictMode(documentId) {
430
+ this.strictModeDocumentId = documentId;
431
+ }
432
+
433
+ /** Exit strict mode and stop tracking document versions for writes. */
434
+ exitStrictMode() {
435
+ this.strictModeDocumentId = null;
436
+ }
437
+
438
+ /**
439
+ * Begin a LOGICAL OPERATION: a user-meaningful action ("Merge morphemes",
440
+ * "Re-transcribe") implemented as many low-level writes, possibly across
441
+ * several batches and even a service round-trip. Until `endOperation()`,
442
+ * every write is stamped with a client-minted `?group-id=` (and the message)
443
+ * so the audit log shows the whole run as ONE expandable entry labeled
444
+ * `message`, with each write's own description underneath.
445
+ *
446
+ * Grouping is orthogonal to batches: a batch is a transaction boundary, an
447
+ * operation is an intent boundary. An operation is NOT atomic — if write 3
448
+ * of 5 fails, writes 1–2 stay committed (and logged under the group). Use a
449
+ * batch inside the operation for any step that must be all-or-nothing.
450
+ *
451
+ * Nesting flattens: a `beginOperation` while one is open is a no-op that
452
+ * joins the outer operation (the outer label wins), and the matching
453
+ * `endOperation` is likewise a no-op. The label is recorded on the FIRST
454
+ * write, so an operation that is never ended (crash, closed tab) is still
455
+ * labeled in the log.
456
+ *
457
+ * "Every write" is the writes of project data. Reads never join, and neither
458
+ * do the out-of-band signals shaped like a write (a document lock taken or
459
+ * renewed, a stopped service request, a service reporting itself, an admin
460
+ * control), and neither does a broadcast message (`messages.sendMessage`):
461
+ * none of them is audited, so there would be nothing under the label.
462
+ *
463
+ * `kind` says what kind of operation this is, for a program reading the
464
+ * log: one of `assistant-plan`, `service-run`, `import`, `bulk-edit`,
465
+ * `guess-adoption`, `repair` or `review` (the server refuses any other).
466
+ * Any other kind throws here, before anything is written.
467
+ * `ref` is a short string naming what the operation came from, in the
468
+ * shape its kind documents (the core manual, "Kinds of operation"). Both
469
+ * are recorded from the first write like the label, and a nested operation
470
+ * keeps the outer one's.
471
+ *
472
+ * `keys` (from `keySeed()`) makes a run of the operation send the same
473
+ * requests as an earlier run with the same keys: the nth keyed request that
474
+ * joins it takes the Idempotency-Key `<seed>.<n>` and the document-version
475
+ * its first run claimed, so a request that landed is answered from its
476
+ * first send and writes nothing again. The count starts at 0 at each
477
+ * outermost begin. A nested operation joins the outer one's keys, unless it
478
+ * brings its own: then it numbers from 0 under its own seed until its
479
+ * matching end, and the outer numbering resumes after it.
480
+ *
481
+ * @param {string} message - Human label for the operation.
482
+ * @param {{groupId?: string, kind?: OperationKind, ref?: string, keys?: KeySeed, minted?: Iterable<string>}} [opts] - Optional `{ groupId, kind, ref, keys, minted }`. `groupId` adopts an existing group id instead of minting one (a service joining the requester's operation; `requestService` propagates an open operation to the service automatically). Python's is `group_id`. `kind`, `ref` and `keys` are described above. `minted` is the ids the operation mints for what it creates: a create refused 409 id-taken for one of them was made by an earlier send of the operation, and answers as made.
483
+ * @returns {string} The operation's group id.
484
+ */
485
+ beginOperation(message, { groupId, kind, ref, keys, minted } = {}) {
486
+ return beginOperation(this, message, { groupId, kind, ref, keys, minted });
487
+ }
488
+
489
+ /**
490
+ * A seed for the Idempotency-Keys of a logical operation that may be run
491
+ * again from the top (see beginOperation's `keys`). Keep it with the work
492
+ * it belongs to and pass it to every run.
493
+ * @returns {KeySeed}
494
+ */
495
+ keySeed() {
496
+ return keySeed();
497
+ }
498
+
499
+ /**
500
+ * End the current logical operation. With no argument this is purely local
501
+ * (no request). Pass a refined `message` to relabel the group now that the
502
+ * outcome is known (e.g. `endOperation('Merged 3 morphemes')`) — that sends
503
+ * one PATCH, skipped if the operation never wrote anything. A refine from a
504
+ * nested (flattened) `endOperation` is ignored; the outer label wins.
505
+ * @param {string} [message] - Optional refined label.
506
+ * @returns {Promise<void>}
507
+ */
508
+ async endOperation(message) {
509
+ return endOperation(this, message);
510
+ }
511
+
512
+ /**
513
+ * Run `fn` as one logical operation (see beginOperation), ending it when
514
+ * `fn` settles — including on throw. `fn` receives a `setMessage(msg)`
515
+ * callback to refine the label once the outcome is known.
516
+ *
517
+ * await client.withOperation('Merge morphemes', async (setMessage) => {
518
+ * await client.batched(async (b) => { ... });
519
+ * setMessage(`Merged ${n} morphemes`);
520
+ * });
521
+ *
522
+ * The kind and reference (see beginOperation) come after `fn`:
523
+ *
524
+ * await client.withOperation('Import ELAN corpus', run, { kind: 'import', ref: 'format:elan' });
525
+ *
526
+ * @template T
527
+ * @param {string} message - Human label for the operation.
528
+ * @param {(setMessage: (msg: string) => void) => T | Promise<T>} fn - The work to run; receives `setMessage(msg)` to refine the label once the outcome is known.
529
+ * @param {{kind?: OperationKind, ref?: string, groupId?: string, keys?: KeySeed, minted?: Iterable<string>}} [opts] - Optional `{ kind, ref, groupId, keys, minted }`, as for beginOperation. `kind` is one of `assistant-plan`, `service-run`, `import`, `bulk-edit`, `guess-adoption`, `repair` or `review`, and any other throws before `fn` runs.
530
+ * @returns {Promise<T>} Whatever `fn` resolves to.
531
+ */
532
+ async withOperation(message, fn, { kind, ref, groupId, keys, minted } = {}) {
533
+ return withOperation(this, message, fn, { kind, ref, groupId, keys, minted });
534
+ }
535
+
536
+
537
+ /**
538
+ * Open a batch: a view of this client with the same bundles, on which every
539
+ * write of project data queues instead of going out. `submit()` sends the
540
+ * queued operations as ONE atomic request (larger than the server's cap,
541
+ * as consecutive requests with the results concatenated in queue order,
542
+ * each atomic on its own, so a failure in a later one leaves the earlier
543
+ * ones committed, and the error it throws carries `committed`, the count
544
+ * saved, and `committedResults`, their results) and
545
+ * resolves to one result per operation; `abort()` drops them. A call made
546
+ * on the client itself is never touched by an open batch, and a read or an
547
+ * out-of-band signal made on the batch goes over the wire now (see the note
548
+ * at the top of `http.js`).
549
+ *
550
+ * const b = client.batch();
551
+ * b.tokens.bulkCreate(sentenceOps);
552
+ * b.tokens.bulkCreate(wordOps);
553
+ * const [sentRes, wordRes] = await b.submit();
554
+ *
555
+ * Server-side a batch runs sequentially in one transaction: a child op sees
556
+ * parents created earlier in the same batch, and any op's failure rolls the
557
+ * whole batch back. A batch is not nestable. Prefer `batched()`, which
558
+ * submits or aborts for you.
559
+ *
560
+ * `timeout` (ms, or null for none) is how long each of this batch's
561
+ * requests may wait for its answer, in place of the client's
562
+ * `batchTimeout`, for this batch alone.
563
+ * @param {{timeout?: number | null}} [options]
564
+ * @returns {PlaidBatch} the batch view
565
+ */
566
+ batch({ timeout } = {}) {
567
+ return openBatch(this, { timeout });
568
+ }
569
+
570
+ /**
571
+ * POST one batch request (at most MAX_BATCH_OPS operations). Resolves to
572
+ * one `{ status, headers, body }` per operation, with only the body
573
+ * recased: the headers keep the server's spelling. `key` is the request's
574
+ * Idempotency-Key: a batch whose answer is lost (no response, 502, 504) is
575
+ * sent again under it, and answered from the first send if that landed.
576
+ * `timeout` is this request's, in place of `batchTimeout` when given.
577
+ * See postBatch in http.js.
578
+ * @param {string} url
579
+ * @param {object[]} body
580
+ * @param {{key?: string, timeout?: number | null}} [options]
581
+ * @private
582
+ */
583
+ async _postBatch(url, body, options) {
584
+ return postBatch(this, url, body, options);
585
+ }
586
+
587
+
588
+ /**
589
+ * Run `fn` with a batch, then submit all queued ops as `submit()` does (ONE
590
+ * atomic request up to MAX_BATCH_OPS operations, consecutive requests past
591
+ * it), or abort the batch if `fn` throws. `fn` receives the batch and makes its
592
+ * writes on it; it must NOT call `submit()` itself. Resolves to the batch
593
+ * results array (`[]` if `fn` queued nothing).
594
+ *
595
+ * const [sentRes, wordRes] = await client.batched(async (b) => {
596
+ * b.tokens.bulkCreate(sentenceOps);
597
+ * b.tokens.bulkCreate(wordOps);
598
+ * });
599
+ *
600
+ * A write made on `client` inside `fn` is not part of the batch: it goes
601
+ * over the wire at once, as it would anywhere else. See `batch()`, which
602
+ * also says what `timeout` does.
603
+ * @param {(batch: PlaidBatch) => (void | Promise<void>)} fn
604
+ * @param {{timeout?: number | null}} [options]
605
+ * @returns {Promise<any[]>}
606
+ */
607
+ async batched(fn, { timeout } = {}) {
608
+ const batch = this.batch({ timeout });
609
+ try {
610
+ await fn(batch);
611
+ } catch (e) {
612
+ batch.abort();
613
+ throw e;
614
+ }
615
+ return batch.submit();
616
+ }
617
+
618
+ /**
619
+ * Liveness, version and database size, with NO authentication and no client
620
+ * instance — for a launcher or status page checking whether a server is up
621
+ * before anyone logs in.
622
+ * @param {string} baseUrl - The API base URL
623
+ * @param {{timeout?: number | null}} [options]
624
+ * @returns {Promise<ServerHealth>}
625
+ */
626
+ static async health(baseUrl, options = {}) {
627
+ return anonymousRequest("GET", baseUrl, "/health", undefined, options);
628
+ }
629
+
630
+ /**
631
+ * The limits this server enforces, with NO authentication and no client
632
+ * instance. See the instance method for the shape.
633
+ * @param {string} baseUrl - The API base URL
634
+ * @param {{timeout?: number | null}} [options]
635
+ * @returns {Promise<{limits: ServerLimits}>}
636
+ */
637
+ static async info(baseUrl, options = {}) {
638
+ return anonymousRequest("GET", baseUrl, "/api/v1/info", undefined, options);
639
+ }
640
+
641
+ /**
642
+ * Build the link to hand someone for an invite code. The server never sees
643
+ * an app URL, so the app that minted the invite is the one that names it.
644
+ * Both SPAs use HashRouter, so the code rides in the fragment — which also
645
+ * keeps it out of server access logs.
646
+ * @param {string} appUrl - Where the SPA lives, e.g. "https://plaid.example.org/igt/"
647
+ * @param {string} code - The code returned by invites.create()
648
+ * @returns {string}
649
+ */
650
+ static inviteUrl(appUrl, code) {
651
+ return `${appUrl.replace(/#.*$/, "").replace(/\/$/, "")}/#/invite/${encodeURIComponent(code)}`;
652
+ }
653
+
654
+ /**
655
+ * Describe an invite code, with NO authentication — this is what a signup
656
+ * page calls before the redeemer has an account. Returns the kind of link
657
+ * ("signup" or "password-reset"), its status ("active", "used", "expired",
658
+ * "revoked", or "inactive" when its creator can no longer grant what it
659
+ * grants), and the project it grants access to, if any.
660
+ *
661
+ * Throws a 404-shaped error if the code is unknown. A known-but-dead code
662
+ * resolves normally with a non-"active" status, so the page can say why.
663
+ * @param {string} baseUrl - The API base URL
664
+ * @param {string} code - The invite code
665
+ * @param {object} [options]
666
+ * @returns {Promise<{kind: string, status: string, expiresAt: string, projectName?: string, projectRole?: string, email?: string, grantAdmin: boolean}>}
667
+ */
668
+ static async lookupInvite(baseUrl, code, options = {}) {
669
+ return anonymousRequest("POST", baseUrl, "/api/v1/invites/lookup", { code }, options);
670
+ }
671
+
672
+ /**
673
+ * Redeem an invite code, with NO authentication.
674
+ *
675
+ * For a signup invite, pass `email` and `password` to create the account
676
+ * (and optionally `displayName`); the invite's grants are applied in the
677
+ * same transaction. For a password reset link, pass `password` only.
678
+ * Resolves to a logged-in client, exactly like login() — the redeemer just
679
+ * chose these credentials, so there is no reason to send them to a login
680
+ * form to retype them.
681
+ * @param {string} baseUrl - The API base URL
682
+ * @param {string} code - The invite code
683
+ * @param {object} credentials
684
+ * @param {string} [credentials.email] - The new account's email address,
685
+ * which becomes its id and login (signup only)
686
+ * @param {string} [credentials.displayName] - How the new user is shown in
687
+ * the UI; defaults to the local part of the email (signup only)
688
+ * @param {string} credentials.password - Desired password (min 8 characters)
689
+ * @param {object} [options]
690
+ * @returns {Promise<{client: PlaidClient, userId: string, kind: string}>}
691
+ */
692
+ static async redeemInvite(
693
+ baseUrl,
694
+ code,
695
+ { email, password, displayName } = {},
696
+ options = {},
697
+ ) {
698
+ const data = await anonymousRequest(
699
+ "POST",
700
+ baseUrl,
701
+ "/api/v1/invites/redeem",
702
+ bodyOf({
703
+ code,
704
+ email: normalizeUserId(email),
705
+ password,
706
+ "display-name": displayName,
707
+ }),
708
+ options,
709
+ );
710
+ return {
711
+ client: new PlaidClient(
712
+ baseUrl.replace(/\/$/, ""),
713
+ data.token || "",
714
+ options,
715
+ ),
716
+ userId: data.userId,
717
+ kind: data.kind,
718
+ };
719
+ }
720
+
721
+ /**
722
+ * Authenticate and return a new client instance with token. This is the
723
+ * single auth entry point — there is no `client.login` resource.
724
+ * @param {string} baseUrl - The base URL for the API
725
+ * @param {string} userId - User ID for authentication
726
+ * @param {string} password - Password for authentication
727
+ * @param {object} [options] - Client options forwarded to the constructor (e.g. { timeout })
728
+ * @returns {Promise<PlaidClient>} - Authenticated client instance
729
+ */
730
+ static async login(baseUrl, userId, password, options = {}) {
731
+ baseUrl = baseUrl.replace(/\/$/, "");
732
+ const data = await anonymousRequest(
733
+ "POST",
734
+ baseUrl,
735
+ "/api/v1/login",
736
+ { "user-id": normalizeUserId(userId), password },
737
+ options,
738
+ );
739
+ return new PlaidClient(baseUrl, data.token || "", options);
740
+ }
741
+ }
742
+
743
+ export { PlaidClient };