@larc-iu/plaid-client 0.2.0-alpha.22 → 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.
package/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # @larc-iu/plaid-client
2
+
3
+ The JavaScript client for the [Plaid](https://larc-iu.github.io/plaid/) annotation API.
4
+ It has no dependencies and runs in browsers and in Node.
5
+
6
+ ## From npm
7
+
8
+ ```sh
9
+ npm install @larc-iu/plaid-client@alpha
10
+ ```
11
+
12
+ ```js
13
+ import { PlaidClient } from "@larc-iu/plaid-client";
14
+
15
+ const client = await PlaidClient.login("http://localhost:8080", "user@example.com", "password");
16
+ console.log(await client.projects.list());
17
+ ```
18
+
19
+ ## From a Plaid server
20
+
21
+ Every Plaid server serves the client that matches its own version at `/client/plaid-client.js`, with its type declarations at `/client/plaid-client.d.ts`.
22
+ A page served by Plaid, for example from the folder set as `static_resources_path` in its `config.toml`, can import it without bundling a copy:
23
+
24
+ ```html
25
+ <script type="module">
26
+ import { PlaidClient } from "/client/plaid-client.js";
27
+ const client = await PlaidClient.login(location.origin, "user@example.com", "password");
28
+ console.log(await client.projects.list());
29
+ </script>
30
+ ```
31
+
32
+ ## Documentation
33
+
34
+ - [Manual](https://larc-iu.github.io/plaid/manual.html), which covers the API and its concepts.
35
+ - [API reference](https://larc-iu.github.io/plaid/api-js.html) for this client.
36
+
37
+ ## Types
38
+
39
+ The type declarations are generated from the JSDoc in `src/` by `npm run types` (TypeScript's `tsc`), which `npm pack` runs. The JSDoc is their only source.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larc-iu/plaid-client",
3
- "version": "0.2.0-alpha.22",
3
+ "version": "0.2.0-alpha.26",
4
4
  "description": "JavaScript client for the Plaid annotation API",
5
5
  "type": "module",
6
6
  "repository": {
@@ -17,10 +17,16 @@
17
17
  ".": "./src/index.js"
18
18
  },
19
19
  "scripts": {
20
- "test": "node --test"
20
+ "test": "node --test",
21
+ "types": "npm run clean-types && tsc -p .",
22
+ "clean-types": "node -e \"const fs = require('fs'); for (const f of fs.readdirSync('src')) if (f.endsWith('.d.ts')) fs.rmSync('src/' + f)\"",
23
+ "prepack": "npm run types",
24
+ "postpack": "npm run clean-types"
21
25
  },
22
26
  "files": [
23
- "src/",
24
- "index.d.ts"
25
- ]
27
+ "src/"
28
+ ],
29
+ "devDependencies": {
30
+ "typescript": "^5.9.3"
31
+ }
26
32
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * A BatchRef for op `opIndex` of `batch` (negative counts from the end, -1
3
+ * is the op queued last), fixed when it is called.
4
+ */
5
+ export function makeBatchRef(batch: any, opIndex: any, index: any): BatchRef;
6
+ /**
7
+ * `body` with each BatchRef in it replaced by null, and the `refs` that say
8
+ * where they were, or `refs` null when it holds none (the body is then
9
+ * returned as it was).
10
+ */
11
+ export function takeRefs(batch: any, body: any): {
12
+ body: any;
13
+ refs: any[];
14
+ };
15
+ /**
16
+ * `refs` counted from `start`, for an op in a batch sent in several requests
17
+ * (the server counts within one request). A ref to an op of an earlier
18
+ * request cannot resolve, so it throws before anything goes.
19
+ */
20
+ export function rebaseRefs(refs: any, start: any): any;
21
+ export class BatchRef {
22
+ constructor(batch: any, op: any, index: any);
23
+ /** The op, counted from 0 in the batch, whose id this stands for. */
24
+ $ref: any;
25
+ index: any;
26
+ belongsTo(batch: any): boolean;
27
+ toJSON(): void;
28
+ [Symbol.toPrimitive](): void;
29
+ #private;
30
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Unicode code-point helpers for working with Plaid text offsets.
3
+ *
4
+ * Plaid token offsets (`begin` / `end`) are 0-based indices in Unicode CODE
5
+ * POINTS (begin inclusive, end exclusive) — NOT UTF-16 code units. JavaScript
6
+ * strings are UTF-16, so `.length`, `.slice`, `.substring`, `s[i]`,
7
+ * `String.prototype.indexOf`, and `Intl.Segmenter`'s `index` all count UTF-16
8
+ * code units, which disagree with code points for astral characters
9
+ * (>= U+10000 — emoji, and SMP scripts such as Gothic, cuneiform, CJK Ext-B).
10
+ *
11
+ * Use these to slice a text body by token offsets, and to compute offsets for
12
+ * new tokens, in code points. The spread/`for…of` string iterator yields code
13
+ * points, which is what makes this work.
14
+ */
15
+ /**
16
+ * Number of Unicode code points in `s` (not `s.length`, which is UTF-16).
17
+ * @param {string} s
18
+ * @returns {number}
19
+ */
20
+ export function cpLength(s: string): number;
21
+ /**
22
+ * Substring of `s` by CODE-POINT indices [begin, end) (end optional = to end).
23
+ * Mirrors `String.prototype.slice` semantics but in code points.
24
+ * @param {string} s
25
+ * @param {number} begin
26
+ * @param {number} [end]
27
+ * @returns {string}
28
+ */
29
+ export function cpSlice(s: string, begin: number, end?: number): string;
30
+ /**
31
+ * Prebuilt slicer for taking MANY code-point slices of the same string:
32
+ * spreads `s` into code points once, then each slice costs O(slice length).
33
+ * `cpSlice` spreads the whole string per call, which turns quadratic when a
34
+ * caller slices every token of a large text. Mirror of the server's
35
+ * `plaid.util.codepoint/cp-slicer`.
36
+ * @param {string} s
37
+ * @returns {(begin: number, end?: number) => string}
38
+ */
39
+ export function cpSlicer(s: string): (begin: number, end?: number) => string;
40
+ /**
41
+ * Convert a UTF-16 index `u` into `s` to a code-point index — i.e. how many
42
+ * code points precede `u`. Inverse of `cpToUtf16`. Useful for converting a
43
+ * DOM/`indexOf`/`Intl.Segmenter` (UTF-16) position into a code-point offset.
44
+ * @param {string} s
45
+ * @param {number} u
46
+ * @returns {number}
47
+ */
48
+ export function utf16ToCp(s: string, u: number): number;
49
+ /**
50
+ * Convert a code-point index `cp` into `s` to a UTF-16 index. Clamps to
51
+ * `s.length` when `cp` is past the end. Inverse of `utf16ToCp`.
52
+ * @param {string} s
53
+ * @param {number} cp
54
+ * @returns {number}
55
+ */
56
+ export function cpToUtf16(s: string, cp: number): number;
57
+ /**
58
+ * Like `String.prototype.indexOf`, but the returned index and `fromCp` are
59
+ * CODE-POINT indices. Returns -1 when `sub` is not found.
60
+ * @param {string} s
61
+ * @param {string} sub
62
+ * @param {number} [fromCp]
63
+ * @returns {number}
64
+ */
65
+ export function cpIndexOf(s: string, sub: string, fromCp?: number): number;
66
+ /**
67
+ * `s` composed (Unicode NFC), as the server stores every text, and where each
68
+ * code-point position of `s` goes in it: `{ text, at }`, `at(p)` for every p
69
+ * in [0, cpLength(s)]. Mirror of the server's `plaid.util.canonical/compose`,
70
+ * for an app that measures tokens on a body it is about to send: the server
71
+ * stores `text`, and a token measured at [b, e) on `s` is at [at(b), at(e)).
72
+ *
73
+ * `s` is cut before each code point that is not a mark, a piece joined to the
74
+ * one before when the two compose together (Hangul jamo, a vowel sign), and
75
+ * each piece composes on its own. A position inside a piece composing changed
76
+ * goes to where cutting the piece there composes the same, else to the next
77
+ * such place, at the latest the piece's composed end, so an edge between a
78
+ * letter and the mark that composes with it moves to after the composed
79
+ * character, and a mark that does not compose stays out of it. `at` never
80
+ * reverses two positions.
81
+ *
82
+ * `cuts` are the code-point positions of `s` where the text's tokens begin
83
+ * and end. A piece with a cut inside it composes in parts, one between each
84
+ * two cuts, so a character whose letter and mark belong to two tokens stays
85
+ * decomposed, each token keeps its own characters, and none is left with no
86
+ * text. The server composes every body with its tokens' edges.
87
+ * @param {string} s
88
+ * @param {Iterable<number> | null} [cuts]
89
+ * @returns {{text: string, at: (p: number) => number}}
90
+ */
91
+ export function composeText(s: string, cuts?: Iterable<number> | null): {
92
+ text: string;
93
+ at: (p: number) => number;
94
+ };
package/src/codepoint.js CHANGED
@@ -13,7 +13,11 @@
13
13
  * points, which is what makes this work.
14
14
  */
15
15
 
16
- /** Number of Unicode code points in `s` (not `s.length`, which is UTF-16). */
16
+ /**
17
+ * Number of Unicode code points in `s` (not `s.length`, which is UTF-16).
18
+ * @param {string} s
19
+ * @returns {number}
20
+ */
17
21
  export function cpLength(s) {
18
22
  return [...s].length;
19
23
  }
@@ -21,6 +25,10 @@ export function cpLength(s) {
21
25
  /**
22
26
  * Substring of `s` by CODE-POINT indices [begin, end) (end optional = to end).
23
27
  * Mirrors `String.prototype.slice` semantics but in code points.
28
+ * @param {string} s
29
+ * @param {number} begin
30
+ * @param {number} [end]
31
+ * @returns {string}
24
32
  */
25
33
  export function cpSlice(s, begin, end) {
26
34
  return [...s].slice(begin, end).join('');
@@ -32,6 +40,8 @@ export function cpSlice(s, begin, end) {
32
40
  * `cpSlice` spreads the whole string per call, which turns quadratic when a
33
41
  * caller slices every token of a large text. Mirror of the server's
34
42
  * `plaid.util.codepoint/cp-slicer`.
43
+ * @param {string} s
44
+ * @returns {(begin: number, end?: number) => string}
35
45
  */
36
46
  export function cpSlicer(s) {
37
47
  const chars = [...(s ?? '')];
@@ -42,6 +52,9 @@ export function cpSlicer(s) {
42
52
  * Convert a UTF-16 index `u` into `s` to a code-point index — i.e. how many
43
53
  * code points precede `u`. Inverse of `cpToUtf16`. Useful for converting a
44
54
  * DOM/`indexOf`/`Intl.Segmenter` (UTF-16) position into a code-point offset.
55
+ * @param {string} s
56
+ * @param {number} u
57
+ * @returns {number}
45
58
  */
46
59
  export function utf16ToCp(s, u) {
47
60
  return [...s.slice(0, u)].length;
@@ -50,6 +63,9 @@ export function utf16ToCp(s, u) {
50
63
  /**
51
64
  * Convert a code-point index `cp` into `s` to a UTF-16 index. Clamps to
52
65
  * `s.length` when `cp` is past the end. Inverse of `utf16ToCp`.
66
+ * @param {string} s
67
+ * @param {number} cp
68
+ * @returns {number}
53
69
  */
54
70
  export function cpToUtf16(s, cp) {
55
71
  if (cp <= 0) return 0;
@@ -66,8 +82,122 @@ export function cpToUtf16(s, cp) {
66
82
  /**
67
83
  * Like `String.prototype.indexOf`, but the returned index and `fromCp` are
68
84
  * CODE-POINT indices. Returns -1 when `sub` is not found.
85
+ * @param {string} s
86
+ * @param {string} sub
87
+ * @param {number} [fromCp]
88
+ * @returns {number}
69
89
  */
70
90
  export function cpIndexOf(s, sub, fromCp = 0) {
71
91
  const u = s.indexOf(sub, cpToUtf16(s, fromCp));
72
92
  return u < 0 ? -1 : utf16ToCp(s, u);
73
93
  }
94
+
95
+ const MARK = /^\p{M}$/u;
96
+
97
+ // The longest piece whose inside positions are each placed where cutting
98
+ // there leaves the composed text as it is (core's `refine-limit`).
99
+ const REFINE_LIMIT = 32;
100
+
101
+ // The positions strictly inside the piece [b, e) of `cps`, which composed to
102
+ // `c` (`len` code points) at `out`: a place where the piece's two halves
103
+ // compose apart to `c` goes to the end of the first half (a tone mark that
104
+ // does not compose with ẹ stays out of it), any other to the next such place,
105
+ // else to the piece's end. Mirror of core's `refine-inside!`.
106
+ function refineInside(at, cps, b, e, out, c, len) {
107
+ const whole = e - b > REFINE_LIMIT;
108
+ let next = out + len;
109
+ for (let i = e - 1; i > b; i -= 1) {
110
+ if (!whole) {
111
+ const pre = cps.slice(b, i).join('').normalize('NFC');
112
+ if (pre + cps.slice(i, e).join('').normalize('NFC') === c) {
113
+ next = Math.min(next, out + [...pre].length);
114
+ }
115
+ }
116
+ at[i] = next;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * `s` composed (Unicode NFC), as the server stores every text, and where each
122
+ * code-point position of `s` goes in it: `{ text, at }`, `at(p)` for every p
123
+ * in [0, cpLength(s)]. Mirror of the server's `plaid.util.canonical/compose`,
124
+ * for an app that measures tokens on a body it is about to send: the server
125
+ * stores `text`, and a token measured at [b, e) on `s` is at [at(b), at(e)).
126
+ *
127
+ * `s` is cut before each code point that is not a mark, a piece joined to the
128
+ * one before when the two compose together (Hangul jamo, a vowel sign), and
129
+ * each piece composes on its own. A position inside a piece composing changed
130
+ * goes to where cutting the piece there composes the same, else to the next
131
+ * such place, at the latest the piece's composed end, so an edge between a
132
+ * letter and the mark that composes with it moves to after the composed
133
+ * character, and a mark that does not compose stays out of it. `at` never
134
+ * reverses two positions.
135
+ *
136
+ * `cuts` are the code-point positions of `s` where the text's tokens begin
137
+ * and end. A piece with a cut inside it composes in parts, one between each
138
+ * two cuts, so a character whose letter and mark belong to two tokens stays
139
+ * decomposed, each token keeps its own characters, and none is left with no
140
+ * text. The server composes every body with its tokens' edges.
141
+ * @param {string} s
142
+ * @param {Iterable<number> | null} [cuts]
143
+ * @returns {{text: string, at: (p: number) => number}}
144
+ */
145
+ export function composeText(s, cuts = null) {
146
+ const text0 = s ?? '';
147
+ if (text0.normalize('NFC') === text0) return { text: text0, at: (p) => p };
148
+ const cps = [...text0];
149
+ const n = cps.length;
150
+ const cutAt = new Set();
151
+ for (const c of cuts ?? []) if (Number.isInteger(c) && c > 0 && c < n) cutAt.add(c);
152
+ const pieces = [];
153
+ let start = 0;
154
+ for (let i = 1; i <= n; i += 1) {
155
+ if (i < n && MARK.test(cps[i])) continue;
156
+ const last = pieces[pieces.length - 1];
157
+ if (last && cps[start].codePointAt(0) >= 0x300) {
158
+ const a = cps.slice(last[0], last[1]).join('');
159
+ const b = cps.slice(start, i).join('');
160
+ if ((a + b).normalize('NFC') !== a.normalize('NFC') + b.normalize('NFC')) {
161
+ last[1] = i;
162
+ start = i;
163
+ continue;
164
+ }
165
+ }
166
+ pieces.push([start, i]);
167
+ start = i;
168
+ }
169
+ // a piece with a cut strictly inside it composes in parts
170
+ const parts = [];
171
+ for (const [b, e] of pieces) {
172
+ let from = b;
173
+ for (let i = b + 1; i < e; i += 1) {
174
+ if (cutAt.has(i)) {
175
+ parts.push([from, i]);
176
+ from = i;
177
+ }
178
+ }
179
+ parts.push([from, e]);
180
+ }
181
+ const at = new Int32Array(n + 1);
182
+ let text = '';
183
+ let out = 0;
184
+ for (const [b, e] of parts) {
185
+ const src = cps.slice(b, e).join('');
186
+ const c = src.normalize('NFC');
187
+ const len = [...c].length;
188
+ text += c;
189
+ if (c === src) {
190
+ for (let i = b; i < e; i += 1) at[i] = out + (i - b);
191
+ } else {
192
+ at[b] = out;
193
+ refineInside(at, cps, b, e, out, c, len);
194
+ }
195
+ out += len;
196
+ }
197
+ at[n] = out;
198
+ const expected = text0.normalize('NFC');
199
+ if ((cutAt.size ? text.normalize('NFC') : text) !== expected) {
200
+ throw new Error('The text could not be composed.');
201
+ }
202
+ return { text, at: (p) => at[p] };
203
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * A constraint an app declares on a layer, e.g. `{type: "value-set", values: ["N"]}`.
3
+ * @typedef {{type: "max-in-degree" | "acyclic" | "same-ancestor" | "single-span"
4
+ * | "value-set" | "coextensive" | "single-link"} & Record<string, any>} LayerConstraint
5
+ */
6
+ /**
7
+ * `expected`: the list read for the namespace, null when it was absent.
8
+ * @typedef {{expected?: LayerConstraint[] | null}} ConstraintWriteOptions
9
+ */
10
+ /**
11
+ * One violation a 422 lists.
12
+ * @typedef {object} ConstraintViolation
13
+ * @property {string} constraint
14
+ * @property {string | null} namespace
15
+ * @property {string} layer
16
+ * @property {string} layerName
17
+ * @property {string | null} document
18
+ * @property {string | null} at
19
+ * @property {string[]} ids
20
+ * @property {any} [value]
21
+ * @property {string[]} [parts]
22
+ */
23
+ /**
24
+ * The violations of a write refused by a layer constraint, camelCased
25
+ * ({constraint, namespace, layer, layerName, document, at, ids, value?,
26
+ * parts?}), or null for any other error. The answer lists at most 100, and
27
+ * `err.responseData["violation-count"]` is the total.
28
+ * @param {any} err
29
+ * @returns {ConstraintViolation[] | null}
30
+ */
31
+ export function violationsOf(err: any): ConstraintViolation[] | null;
32
+ /** The request body of a constraint write: the list, and `expected` when given. */
33
+ export function constraintsBody(constraints: any, options: any): {
34
+ constraints?: undefined;
35
+ } | {
36
+ constraints: any;
37
+ };
38
+ /**
39
+ * Whether a value-set constraint ({values, delimiters?, parts?}) allows
40
+ * `value` as the server reads it: null and blank values pass, a non-string
41
+ * does not, and every part split on a delimiter (or only the first, with
42
+ * `parts: "first"`), trimmed, must be listed.
43
+ * @param {{values: string[], delimiters?: string, parts?: "all"|"first"}} constraint
44
+ * @param {any} value
45
+ * @returns {boolean}
46
+ */
47
+ export function valueSetAllows(constraint: {
48
+ values: string[];
49
+ delimiters?: string;
50
+ parts?: "all" | "first";
51
+ }, value: any): boolean;
52
+ /**
53
+ * A constraint an app declares on a layer, e.g. `{type: "value-set", values: ["N"]}`.
54
+ */
55
+ export type LayerConstraint = {
56
+ type: "max-in-degree" | "acyclic" | "same-ancestor" | "single-span" | "value-set" | "coextensive" | "single-link";
57
+ } & Record<string, any>;
58
+ /**
59
+ * `expected`: the list read for the namespace, null when it was absent.
60
+ */
61
+ export type ConstraintWriteOptions = {
62
+ expected?: LayerConstraint[] | null;
63
+ };
64
+ /**
65
+ * One violation a 422 lists.
66
+ */
67
+ export type ConstraintViolation = {
68
+ constraint: string;
69
+ namespace: string | null;
70
+ layer: string;
71
+ layerName: string;
72
+ document: string | null;
73
+ at: string | null;
74
+ ids: string[];
75
+ value?: any;
76
+ parts?: string[];
77
+ };
@@ -7,13 +7,38 @@
7
7
 
8
8
  import { transformResponse } from "./transforms.js";
9
9
 
10
+ /**
11
+ * A constraint an app declares on a layer, e.g. `{type: "value-set", values: ["N"]}`.
12
+ * @typedef {{type: "max-in-degree" | "acyclic" | "same-ancestor" | "single-span"
13
+ * | "value-set" | "coextensive" | "single-link"} & Record<string, any>} LayerConstraint
14
+ */
15
+
16
+ /**
17
+ * `expected`: the list read for the namespace, null when it was absent.
18
+ * @typedef {{expected?: LayerConstraint[] | null}} ConstraintWriteOptions
19
+ */
20
+
21
+ /**
22
+ * One violation a 422 lists.
23
+ * @typedef {object} ConstraintViolation
24
+ * @property {string} constraint
25
+ * @property {string | null} namespace
26
+ * @property {string} layer
27
+ * @property {string} layerName
28
+ * @property {string | null} document
29
+ * @property {string | null} at
30
+ * @property {string[]} ids
31
+ * @property {any} [value]
32
+ * @property {string[]} [parts]
33
+ */
34
+
10
35
  /**
11
36
  * The violations of a write refused by a layer constraint, camelCased
12
37
  * ({constraint, namespace, layer, layerName, document, at, ids, value?,
13
38
  * parts?}), or null for any other error. The answer lists at most 100, and
14
39
  * `err.responseData["violation-count"]` is the total.
15
40
  * @param {any} err
16
- * @returns {Array<object>|null}
41
+ * @returns {ConstraintViolation[] | null}
17
42
  */
18
43
  export function violationsOf(err) {
19
44
  const vs = err?.status === 422 ? err?.responseData?.violations : undefined;
@@ -0,0 +1,2 @@
1
+ export function createdId(result: any | null | undefined): string | undefined;
2
+ export function createdIds(result: any | null | undefined): string[];
@@ -0,0 +1,158 @@
1
+ /**
2
+ * How long a freshly taken lock lasts, from the server's own answer.
3
+ *
4
+ * `expiresAt` is the epoch-millisecond moment the lock endpoints return, on
5
+ * the server's clock, so it is read against the server's clock too
6
+ * (`client.serverNow()`): this machine's clock being off cannot stretch or
7
+ * shrink the window. It is how a window an operator has retuned
8
+ * (`:plaid.server.locks/config :expiration-ms`) reaches the keeper.
9
+ *
10
+ * An answer that cannot be a window (none, already over, past an hour) is
11
+ * core's default, 60 s.
12
+ *
13
+ * @param {number} expiresAt
14
+ * @param {number} serverNowMs
15
+ * @returns {number}
16
+ */
17
+ export function lockTtlMs(expiresAt: number, serverNowMs: number): number;
18
+ /**
19
+ * Hold `documentId`'s server-enforced lock for the length of `fn`.
20
+ * `client.documents.locked` is the entry point; see its doc comment.
21
+ *
22
+ * @param {object} client
23
+ * @param {string} documentId
24
+ * @param {(lock: DocumentLock) => any} fn
25
+ * @param {{ keepAlive?: boolean, acquireRetryMs?: number }} [options]
26
+ */
27
+ export function withDocumentLock(client: object, documentId: string, fn: (lock: DocumentLock) => any, { keepAlive, acquireRetryMs }?: {
28
+ keepAlive?: boolean;
29
+ acquireRetryMs?: number;
30
+ }): Promise<any>;
31
+ /**
32
+ * Keeping a document lock alive for as long as a `locked()` block runs.
33
+ *
34
+ * plaid-core expires a document lock about a minute after it was taken, and the
35
+ * holder's own writes are what renew it (`plaid.sql.operation` calls
36
+ * `refresh-locks!` on every write it accepts). That is the right rule for an
37
+ * editor, which writes as the person types. It is the wrong one for a service:
38
+ * a parser loads a model, reads the document, spends minutes in inference and
39
+ * only then writes, so for most of its run it holds nothing. The lock lapses in
40
+ * silence, a person's edit lands between the read the work was planned from and
41
+ * the write about to go out, and the write clobbers it.
42
+ *
43
+ * A `locked()` block therefore renews on a timer of its own. The renewal is an
44
+ * acquire that names the holder's `lockId`: the server refreshes the lock that
45
+ * id holds, and refuses one somebody else holds (the same user included).
46
+ *
47
+ * The beat is unconditional rather than "only when no write has gone out
48
+ * recently". The request layer knows the method of a call but not which
49
+ * document it touched, so a write to some other document would count as a
50
+ * renewal here and would not be one. Two extra requests a minute is the whole
51
+ * cost.
52
+ *
53
+ * A renewal that fails means the block is no longer holding what it asked for,
54
+ * so it stops the run: the loss is recorded on the client and every later write
55
+ * throws `DocumentLockLost`. The guard sits in the request layer because the
56
+ * callers are services, and a check they have to remember to call is a check
57
+ * that is missing somewhere.
58
+ */
59
+ /**
60
+ * The lock a `documents.locked()` block was holding is no longer held.
61
+ *
62
+ * Thrown by the keep-alive when it cannot renew the lock, and then by the
63
+ * request layer on every write this client attempts, so work that has been
64
+ * running for minutes stops rather than writing over an edit that may have
65
+ * landed while the lock was gone.
66
+ */
67
+ export class DocumentLockLost extends Error {
68
+ /**
69
+ * @param {string} message
70
+ * @param {string} documentId
71
+ * @param {unknown} [cause]
72
+ */
73
+ constructor(message: string, documentId: string, cause?: unknown);
74
+ /** @type {string} */
75
+ documentId: string;
76
+ }
77
+ /**
78
+ * Renews one document's lock until the block holding it exits.
79
+ *
80
+ * `refresh` is called with the document id and must reject on failure.
81
+ * `onLost` is called once, with the `DocumentLockLost`, when the lock can no
82
+ * longer be assumed.
83
+ *
84
+ * `clock` and `sleep` exist so the whole policy can be run on a fake clock:
85
+ * `sleep(delayMs)` resolves to true once the block has exited.
86
+ */
87
+ export class LockKeeper {
88
+ constructor(refresh: any, documentId: any, ttlMs: any, { onLost, clock, sleep }?: {
89
+ onLost?: any;
90
+ clock?: () => number;
91
+ sleep?: any;
92
+ });
93
+ _refresh: any;
94
+ _documentId: any;
95
+ _ttlMs: any;
96
+ _onLost: any;
97
+ _clock: () => number;
98
+ _sleep: any;
99
+ _stopped: boolean;
100
+ _wake: any;
101
+ _running: Promise<void>;
102
+ /**
103
+ * The `DocumentLockLost` this keeper raised, or null.
104
+ * @type {DocumentLockLost | null}
105
+ */
106
+ lost: DocumentLockLost | null;
107
+ /**
108
+ * Time between renewals: half the window, so a renewal that fails has a
109
+ * second chance before the lock it is renewing expires.
110
+ */
111
+ get intervalMs(): number;
112
+ /**
113
+ * Time before retrying a renewal that failed. A blip should not end a run
114
+ * that the server still considers the holder of.
115
+ */
116
+ get retryMs(): number;
117
+ _defaultSleep(ms: any): Promise<any>;
118
+ /** The beat. Resolves when the block exits or the lock is lost. */
119
+ run(): Promise<void>;
120
+ start(): Promise<void>;
121
+ stop(): void;
122
+ _fail(error: any): void;
123
+ }
124
+ /**
125
+ * What a `client.documents.locked(id, (lock) => ...)` block gets.
126
+ *
127
+ * The block does not have to consult it: a lost lock stops the next write on
128
+ * its own. Read `lock.lost` (or call `lock.raiseIfLost()`) to give up earlier,
129
+ * between steps of work that has not written anything yet.
130
+ */
131
+ export class DocumentLock {
132
+ /**
133
+ * @param {string} documentId
134
+ * @param {LockKeeper | null} keeper
135
+ * @param {string | null} [lockId]
136
+ */
137
+ constructor(documentId: string, keeper: LockKeeper | null, lockId?: string | null);
138
+ /** @type {string} */
139
+ documentId: string;
140
+ /**
141
+ * The holder id the acquire answered with.
142
+ * @type {string | null}
143
+ */
144
+ lockId: string | null;
145
+ _keeper: LockKeeper;
146
+ /**
147
+ * The `DocumentLockLost` if the lock lapsed, else null.
148
+ * @returns {DocumentLockLost | null}
149
+ */
150
+ get lost(): DocumentLockLost | null;
151
+ /** Throw if the lock lapsed. Call it between steps that have not written. */
152
+ raiseIfLost(): void;
153
+ }
154
+ /**
155
+ * The pause before sending again an acquire whose outcome is unknown, which
156
+ * grows by this much each time (three sends in all).
157
+ */
158
+ export const LOCK_ACQUIRE_RETRY_MS: 500;