@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.
@@ -0,0 +1,60 @@
1
+ /** The page's recorder for `client`'s server. */
2
+ export function recorderFor(client: any): any;
3
+ /** Forget every recorder (tests). */
4
+ export function resetEventRecorders(): void;
5
+ /** Every event type the server accepts. */
6
+ export const EVENT_TYPES: readonly string[];
7
+ export const FLUSH_INTERVAL_MS: 10000;
8
+ export const FLUSH_AT: 50;
9
+ export const FLAG_TTL_MS: 10000;
10
+ export const FLAG_OFF_TTL_MS: 60000;
11
+ export class EventRecorder {
12
+ /**
13
+ * @param {object} [opts]
14
+ * @param {function} [opts.now] - Clock, for tests
15
+ * @param {object} [opts.window] - Where `pagehide` fires (default globalThis)
16
+ */
17
+ constructor({ now, window }?: {
18
+ now?: Function;
19
+ window?: object;
20
+ });
21
+ client: any;
22
+ buffer: any[];
23
+ timer: number;
24
+ now: Function;
25
+ flags: Map<any, any>;
26
+ seen: Set<any>;
27
+ _onPageHide: () => void;
28
+ window: any;
29
+ /** Tell the recorder a project's switch, e.g. right after changing it. */
30
+ setEnabled(projectId: any, on: any): void;
31
+ /**
32
+ * Record one event. Returns true when it was buffered, false when it was
33
+ * not (switch off, a repeat "shown", an unknown type, no project).
34
+ * @param {any} client
35
+ * @param {string} type
36
+ * @param {{projectId?: string, documentId?: string, targetId?: string, data?: Record<string, any>}} [fields]
37
+ * @returns {boolean}
38
+ */
39
+ record(client: any, type: string, { projectId, documentId, targetId, data }?: {
40
+ projectId?: string;
41
+ documentId?: string;
42
+ targetId?: string;
43
+ data?: Record<string, any>;
44
+ }): boolean;
45
+ /**
46
+ * Send what is buffered for every project whose switch is known to be on,
47
+ * drop what is buffered for one known to be off, and keep the rest until
48
+ * the switch is known. With `keepalive` (the page is going away) nothing is
49
+ * kept.
50
+ */
51
+ flush({ keepalive }?: {
52
+ keepalive?: boolean;
53
+ }): void;
54
+ /** Stop listening for `pagehide` and forget everything (tests). */
55
+ close(): void;
56
+ _schedule(): void;
57
+ _drop(projectId: any): void;
58
+ _flag(projectId: any): any;
59
+ _send(projectId: any, events: any, keepalive: any): void;
60
+ }
package/src/events.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { transformRequest } from "./transforms.js";
2
2
 
3
3
  // ---------------------------------------------------------------------------
4
- // Client events: the opt-in research telemetry of a project (core manual,
5
- // "Research telemetry").
4
+ // Client events: the opt-in research telemetry of a project (defined in
5
+ // plaid-core sql/client_event.clj).
6
6
  //
7
7
  // `client.events.record(type, fields)` is a fire-and-forget recorder. It
8
8
  // buffers events and sends them every FLUSH_INTERVAL_MS or FLUSH_AT events,
@@ -51,7 +51,7 @@ const PER_REQUEST = FLUSH_AT;
51
51
  // A suggested or written value is a gloss, not prose. A long one is cut, in
52
52
  // code points as the server counts them, so no character is split in two.
53
53
  // The server refuses a value past this length, and a key its type does not
54
- // take (core manual, "Research telemetry").
54
+ // take (plaid-core sql/client_event.clj).
55
55
  const MAX_VALUE_CHARS = 200;
56
56
 
57
57
  const clip = (v) => {
@@ -92,6 +92,10 @@ export class EventRecorder {
92
92
  /**
93
93
  * Record one event. Returns true when it was buffered, false when it was
94
94
  * not (switch off, a repeat "shown", an unknown type, no project).
95
+ * @param {any} client
96
+ * @param {string} type
97
+ * @param {{projectId?: string, documentId?: string, targetId?: string, data?: Record<string, any>}} [fields]
98
+ * @returns {boolean}
95
99
  */
96
100
  record(client, type, { projectId, documentId, targetId, data } = {}) {
97
101
  try {
package/src/http.d.ts ADDED
@@ -0,0 +1,212 @@
1
+ /**
2
+ * The timeout for a query: the client's own, raised to
3
+ * DEFAULT_QUERY_TIMEOUT_MS when it is shorter. A disabled timeout (0, null)
4
+ * stays disabled.
5
+ */
6
+ export function queryTimeout(client: any): any;
7
+ /**
8
+ * Note how far the server's clock is from this machine's, from a response's
9
+ * Date header, for `client.serverNow()`. A time the server stamped (an audit
10
+ * entry's `ts`) has to be judged against the server's clock: a browser clock
11
+ * minutes off would misjudge it. A response with no readable Date header
12
+ * leaves the last offset as it was.
13
+ */
14
+ export function noteServerClock(client: any, headers: any): void;
15
+ /**
16
+ * Extract and update document versions from response headers and body.
17
+ *
18
+ * `historical` marks a read made with `as-of`: its body carries the version
19
+ * the document HAD then, which is not what a strict-mode write must claim
20
+ * next, so the body is ignored and only the header (always the live
21
+ * version) is learned from. Without this, viewing an old state and then
22
+ * writing 409s on a version that stopped being current long ago.
23
+ */
24
+ export function extractDocumentVersions(client: any, responseHeaders: any, responseBody?: any, { historical }?: {
25
+ historical?: boolean;
26
+ }): void;
27
+ /**
28
+ * `held` with `learned` merged in. A replayed answer only raises a version:
29
+ * it is what the document was right after the first send.
30
+ */
31
+ export function mergeVersions(held: any, learned: any, replayed?: boolean): any;
32
+ /**
33
+ * Read a failed response's body as parsed JSON, falling back to text.
34
+ */
35
+ export function parseErrorBody(response: any): Promise<any>;
36
+ /**
37
+ * Create an enriched error from a failed HTTP response.
38
+ */
39
+ export function makeHttpError(response: any, errorData: any, url: any, method: any): Error;
40
+ /**
41
+ * Create a network error (status 0). Timeout aborts get a clearer message.
42
+ */
43
+ export function makeNetworkError(originalError: any, url: any, method: any): Error;
44
+ /**
45
+ * Run `attempt`, retrying while it rejects with a 503. Exponential backoff
46
+ * with full jitter, so two clients that collide don't march in lockstep and
47
+ * collide again on every retry.
48
+ *
49
+ * `attempt` must perform the whole request, not just await a prepared one:
50
+ * a fetch body and an AbortSignal.timeout are both single-use.
51
+ * @param {() => Promise<any>} attempt
52
+ * @param {{retries?: number, baseDelayMs?: number, onRetry?: (info: object) => void}} [options]
53
+ */
54
+ export function retryWhileBusy(attempt: () => Promise<any>, { retries, baseDelayMs, onRetry }?: {
55
+ retries?: number;
56
+ baseDelayMs?: number;
57
+ onRetry?: (info: object) => void;
58
+ }): Promise<any>;
59
+ /**
60
+ * Whether `error` leaves the outcome unknown: no response came (status 0),
61
+ * or a proxy answered for the server (502, 504). The write may have landed.
62
+ */
63
+ export function isUnknownOutcome(error: any): boolean;
64
+ /**
65
+ * Whether a create refused with `status` and body `data` was made by an
66
+ * earlier send of the same work: `409 id-taken` for an id in `minted` (a Set
67
+ * of the ids the work minted for its creates), and not for a row deleted
68
+ * since (there is nothing to go on with). The one rule for a lost answer's
69
+ * create. The Python twin is `minted_taken`.
70
+ */
71
+ export function mintedTaken(status: any, data: any, minted: any): boolean;
72
+ /**
73
+ * Run `attempt`, sending it again after each delay in `delaysMs` while it
74
+ * fails with an unknown outcome. Only for a keyed write, whose resend is
75
+ * answered from the first send when that one landed. Not when the browser
76
+ * says it is offline: then the request never left, and the caller waits for
77
+ * the network instead. Full jitter, as retryWhileBusy.
78
+ * @param {() => Promise<any>} attempt
79
+ * @param {{delaysMs?: number[], onRetry?: (info: object) => void}} [options]
80
+ */
81
+ export function retryUnknown(attempt: () => Promise<any>, { delaysMs, onRetry }?: {
82
+ delaysMs?: number[];
83
+ onRetry?: (info: object) => void;
84
+ }): Promise<any>;
85
+ /**
86
+ * The key for the next keyed request, and the strict-mode stamp it must
87
+ * claim when an earlier run of its operation already sent it. `joins` says
88
+ * whether the request joins the open logical operation.
89
+ * Returns `{ key, pin, record }`: `pin` is undefined when nothing is pinned,
90
+ * and `record(version)` pins what this request claimed, the first time.
91
+ */
92
+ export function nextIdempotencyKey(client: any, joins: any): {
93
+ key: string;
94
+ pin: any;
95
+ record: (version: any) => void;
96
+ };
97
+ /**
98
+ * Build a fetch AbortSignal that fires after `timeout` ms, or undefined when
99
+ * timeouts are disabled / unsupported.
100
+ */
101
+ export function timeoutSignal(timeout: any): AbortSignal;
102
+ /**
103
+ * Send a request through XMLHttpRequest so the caller can watch the bytes go
104
+ * up: fetch cannot report upload progress. Resolves to a Response, so the rest
105
+ * of the pipeline is the same either way. The timeout is a STALL timeout,
106
+ * re-armed by every progress event, so a large file on a slow link is never
107
+ * cut off while it is still moving, only once nothing has moved for `timeout`
108
+ * ms. Rejects the way fetch does: a TypeError for a network failure, an error
109
+ * named TimeoutError for a stall.
110
+ * @param {string} url
111
+ * @param {{method: string, headers: Record<string, string>, body: any}} request
112
+ * @param {{onUploadProgress?: (progress: {loaded: number, total: number | null}) => void, timeout?: number | null}} [options]
113
+ */
114
+ export function xhrSend(url: string, { method, headers, body }: {
115
+ method: string;
116
+ headers: Record<string, string>;
117
+ body: any;
118
+ }, { onUploadProgress, timeout }?: {
119
+ onUploadProgress?: (progress: {
120
+ loaded: number;
121
+ total: number | null;
122
+ }) => void;
123
+ timeout?: number | null;
124
+ }): Promise<any>;
125
+ /**
126
+ * Everything a request is before it goes anywhere: the URL with its query
127
+ * params and the stamps strict mode, a per-call audit message and an open
128
+ * logical operation add, plus the transformed body. Shared by the wire path
129
+ * (`makeRequest`) and the batch path (`queueRequest`), so a queued op is
130
+ * exactly the request that would have gone out. Strict mode stamps every
131
+ * write, queued or not, and `stampedDocument` names the document it stamped
132
+ * for (null when it stamped nothing). `stampedGroup` is the open logical
133
+ * operation it joined, if any, for `makeRequest` to mark written once the
134
+ * server has taken the request.
135
+ */
136
+ export function prepareRequest(client: any, method: any, path: any, options?: {}): {
137
+ url: string;
138
+ requestBody: any;
139
+ stampedDocument: any;
140
+ stampedVersion: any;
141
+ stampedGroup: any;
142
+ };
143
+ /**
144
+ * Whether a write joins the open logical operation (see prepareRequest).
145
+ */
146
+ export function joinsOperation(client: any, method: any, options?: {}): boolean;
147
+ /**
148
+ * Whether a call made on the client goes out with an Idempotency-Key.
149
+ */
150
+ export function takesIdempotencyKey(method: any, options?: {}): boolean;
151
+ /**
152
+ * `path` with its strict-mode `document-version` claim replaced by `version`,
153
+ * or dropped when `version` is unknown. For the later requests of a batch
154
+ * split past MAX_BATCH_OPS: each op was stamped with the version the document
155
+ * had when it was queued, and every request before it has since moved the
156
+ * document on.
157
+ */
158
+ export function restampDocumentVersion(path: any, version: any): any;
159
+ /**
160
+ * A call made on a batch (see `PlaidClient#batch`). A write of project data is
161
+ * queued as one operation of the batch and answers `{ batched: true }`; its
162
+ * result is the matching entry of what `submit()` resolves to. A read, and a
163
+ * signal marked `outOfBand`, is the client's to make and goes over the wire
164
+ * now, exactly as if it had been made on the client.
165
+ */
166
+ export function queueRequest(batch: any, method: any, path: any, options?: {}): Promise<any>;
167
+ /**
168
+ * Generic request method handling all fetch logic.
169
+ *
170
+ * Options:
171
+ * body - Object body, run through transformRequest
172
+ * rawBody - Body value passed directly (no transform). Mutually exclusive with body.
173
+ * formData - If true, body is FormData; skip Content-Type header
174
+ * queryParams - Object of query param key/values to append
175
+ * noBatch - If true, throw when made on a batch. Only for calls a
176
+ * batch cannot carry at all (see the note at the
177
+ * top of this file); never for a read.
178
+ * outOfBand - If true, the call is a signal rather than a write of
179
+ * project data: made on a batch it still goes over the
180
+ * wire, and it never joins an open logical operation (see
181
+ * the note at the top of this file).
182
+ * noOperation - If true, the call never joins an open logical operation
183
+ * but still queues on a batch like a write. For a
184
+ * broadcast message, which is never audited.
185
+ * versioned - If false, strict mode does not stamp the call with the
186
+ * document version. For a write that carries its own
187
+ * precondition (a text edit with `base`).
188
+ * skipResponseTransform - Return raw parsed JSON (no transformResponse)
189
+ * noAuth - Skip Authorization header
190
+ * binaryResponse - Return arrayBuffer instead of JSON/text
191
+ * noBusyRetry - If true, a 503 is not retried. For a query: core
192
+ * answers 503 when its queue of large queries stays full
193
+ * until the query's deadline, and asking again at once
194
+ * only joins the same queue.
195
+ * timeout - Per-request timeout in ms overriding client.timeout
196
+ * for this call (0/null disables). Used for known-long
197
+ * ops like project delete.
198
+ * onUploadProgress - Called with `{ loaded, total }` (bytes; total null when
199
+ * unknown) as the request body goes up. In a browser the
200
+ * request then travels by XMLHttpRequest (see xhrSend),
201
+ * where the timeout only fires when the upload stalls;
202
+ * elsewhere the callback is ignored and fetch is used.
203
+ */
204
+ export function makeRequest(client: any, method: any, path: any, options?: {}): Promise<any>;
205
+ export const DEFAULT_TIMEOUT_MS: 30000;
206
+ export const DEFAULT_BATCH_TIMEOUT_MS: 180000;
207
+ export const DEFAULT_QUERY_TIMEOUT_MS: 35000;
208
+ export const BUSY_RETRIES: 4;
209
+ export const BUSY_BACKOFF_MS: 250;
210
+ export const IDEMPOTENCY_HEADER: "Idempotency-Key";
211
+ export const REPLAYED_HEADER: "Idempotent-Replayed";
212
+ export const UNKNOWN_RETRY_DELAYS_MS: number[];
package/src/http.js CHANGED
@@ -239,6 +239,8 @@ export const BUSY_BACKOFF_MS = 250;
239
239
  *
240
240
  * `attempt` must perform the whole request, not just await a prepared one:
241
241
  * a fetch body and an AbortSignal.timeout are both single-use.
242
+ * @param {() => Promise<any>} attempt
243
+ * @param {{retries?: number, baseDelayMs?: number, onRetry?: (info: object) => void}} [options]
242
244
  */
243
245
  export async function retryWhileBusy(
244
246
  attempt,
@@ -315,6 +317,8 @@ export function mintedTaken(status, data, minted) {
315
317
  * answered from the first send when that one landed. Not when the browser
316
318
  * says it is offline: then the request never left, and the caller waits for
317
319
  * the network instead. Full jitter, as retryWhileBusy.
320
+ * @param {() => Promise<any>} attempt
321
+ * @param {{delaysMs?: number[], onRetry?: (info: object) => void}} [options]
318
322
  */
319
323
  export async function retryUnknown(
320
324
  attempt,
@@ -381,6 +385,9 @@ export function timeoutSignal(timeout) {
381
385
  * cut off while it is still moving, only once nothing has moved for `timeout`
382
386
  * ms. Rejects the way fetch does: a TypeError for a network failure, an error
383
387
  * named TimeoutError for a stall.
388
+ * @param {string} url
389
+ * @param {{method: string, headers: Record<string, string>, body: any}} request
390
+ * @param {{onUploadProgress?: (progress: {loaded: number, total: number | null}) => void, timeout?: number | null}} [options]
384
391
  */
385
392
  export function xhrSend(
386
393
  url,
package/src/ids.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * A fresh UUIDv7, later than every one this page minted before it.
3
+ * @returns {string}
4
+ */
5
+ export function uuidv7(): string;
6
+ /**
7
+ * A random UUIDv4, for ids that need no order (an operation group, a lock
8
+ * holder). Built on `crypto.getRandomValues`, since `crypto.randomUUID`
9
+ * exists only in a secure context and a page served over plain HTTP from
10
+ * another host than localhost is not one.
11
+ * @returns {string}
12
+ */
13
+ export function uuidv4(): string;
14
+ /**
15
+ * A user id as the server stores it. A user's id is their email address,
16
+ * trimmed of every whitespace, control and format character at either end
17
+ * and lowercased, in NFC before and after (a capital with no composed form,
18
+ * such as J̌, lowercases to one that has one, ǰ), so `Ana@Example.org` and
19
+ * `ana@example.org` name one account. Compare a typed address with an id
20
+ * through this. The rule is core's (`plaid.sql.user/normalize-id`), checked
21
+ * against its case table.
22
+ * Anything but a string comes back unchanged.
23
+ * @param {string} id
24
+ * @returns {string}
25
+ */
26
+ export function normalizeUserId(id: string): string;
package/src/ids.js CHANGED
@@ -72,3 +72,25 @@ export function uuidv4() {
72
72
  }
73
73
  return s;
74
74
  }
75
+
76
+ // Whitespace, control and format characters at either end of an id (NBSP,
77
+ // the BOM, zero-width and bidi marks as well as ASCII blanks), as core trims.
78
+ const ID_EDGES = /^[\p{Z}\p{Cc}\p{Cf}]+|[\p{Z}\p{Cc}\p{Cf}]+$/gu;
79
+
80
+ /**
81
+ * A user id as the server stores it. A user's id is their email address,
82
+ * trimmed of every whitespace, control and format character at either end
83
+ * and lowercased, in NFC before and after (a capital with no composed form,
84
+ * such as J̌, lowercases to one that has one, ǰ), so `Ana@Example.org` and
85
+ * `ana@example.org` name one account. Compare a typed address with an id
86
+ * through this. The rule is core's (`plaid.sql.user/normalize-id`), checked
87
+ * against its case table.
88
+ * Anything but a string comes back unchanged.
89
+ * @param {string} id
90
+ * @returns {string}
91
+ */
92
+ export function normalizeUserId(id) {
93
+ return typeof id === "string"
94
+ ? id.normalize("NFC").replace(ID_EDGES, "").toLowerCase().normalize("NFC")
95
+ : id;
96
+ }