@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
@@ -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,85 @@
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
+ /**
33
+ * How many violations a write refused by a layer constraint has in all, or
34
+ * null for any other error. Read from the raw refusal's "violation-count",
35
+ * since the answer lists at most 100 of them.
36
+ * @param {any} err
37
+ * @returns {number | null}
38
+ */
39
+ export function violationCountOf(err: any): number | null;
40
+ /** The request body of a constraint write: the list, and `expected` when given. */
41
+ export function constraintsBody(constraints: any, options: any): {
42
+ constraints?: undefined;
43
+ } | {
44
+ constraints: any;
45
+ };
46
+ /**
47
+ * Whether a value-set constraint ({values, delimiters?, parts?}) allows
48
+ * `value` as the server reads it: null and blank values pass, a non-string
49
+ * does not, and every part split on a delimiter (or only the first, with
50
+ * `parts: "first"`), trimmed, must be listed.
51
+ * @param {{values: string[], delimiters?: string, parts?: "all"|"first"}} constraint
52
+ * @param {any} value
53
+ * @returns {boolean}
54
+ */
55
+ export function valueSetAllows(constraint: {
56
+ values: string[];
57
+ delimiters?: string;
58
+ parts?: "all" | "first";
59
+ }, value: any): boolean;
60
+ /**
61
+ * A constraint an app declares on a layer, e.g. `{type: "value-set", values: ["N"]}`.
62
+ */
63
+ export type LayerConstraint = {
64
+ type: "max-in-degree" | "acyclic" | "same-ancestor" | "single-span" | "value-set" | "coextensive" | "single-link";
65
+ } & Record<string, any>;
66
+ /**
67
+ * `expected`: the list read for the namespace, null when it was absent.
68
+ */
69
+ export type ConstraintWriteOptions = {
70
+ expected?: LayerConstraint[] | null;
71
+ };
72
+ /**
73
+ * One violation a 422 lists.
74
+ */
75
+ export type ConstraintViolation = {
76
+ constraint: string;
77
+ namespace: string | null;
78
+ layer: string;
79
+ layerName: string;
80
+ document: string | null;
81
+ at: string | null;
82
+ ids: string[];
83
+ value?: any;
84
+ parts?: string[];
85
+ };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Layer constraints: rules an app declares on the token, span and relation
3
+ * layers it owns, which the server enforces inside every write. A write that
4
+ * breaks one on a row it writes is refused with 422 and the violations. See
5
+ * the core manual, "Layer constraints".
6
+ */
7
+
8
+ import { transformResponse } from "./transforms.js";
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
+
35
+ /**
36
+ * The violations of a write refused by a layer constraint, camelCased
37
+ * ({constraint, namespace, layer, layerName, document, at, ids, value?,
38
+ * parts?}), or null for any other error. The answer lists at most 100, and
39
+ * `err.responseData["violation-count"]` is the total.
40
+ * @param {any} err
41
+ * @returns {ConstraintViolation[] | null}
42
+ */
43
+ export function violationsOf(err) {
44
+ const vs = err?.status === 422 ? err?.responseData?.violations : undefined;
45
+ return Array.isArray(vs) ? transformResponse(vs) : null;
46
+ }
47
+
48
+ /**
49
+ * How many violations a write refused by a layer constraint has in all, or
50
+ * null for any other error. Read from the raw refusal's "violation-count",
51
+ * since the answer lists at most 100 of them.
52
+ * @param {any} err
53
+ * @returns {number | null}
54
+ */
55
+ export function violationCountOf(err) {
56
+ const vs = violationsOf(err);
57
+ if (!vs) return null;
58
+ const n = err.responseData["violation-count"];
59
+ return typeof n === "number" ? n : vs.length;
60
+ }
61
+
62
+ /** The request body of a constraint write: the list, and `expected` when given. */
63
+ export function constraintsBody(constraints, options) {
64
+ const body = constraints === undefined ? {} : { constraints };
65
+ if (options && Object.hasOwn(options, "expected")) {
66
+ body.expected = options.expected === undefined ? null : options.expected;
67
+ }
68
+ return body;
69
+ }
70
+
71
+ // What String.prototype.trim trims. The server trims a value-set's parts the
72
+ // same way, so a value read as listed here is read so there.
73
+ const splitParts = (value, delimiters) => {
74
+ const set = new Set([...(delimiters || "")]);
75
+ if (!set.size) return [value];
76
+ const out = [];
77
+ let part = "";
78
+ for (const ch of value) {
79
+ if (set.has(ch)) {
80
+ out.push(part);
81
+ part = "";
82
+ } else {
83
+ part += ch;
84
+ }
85
+ }
86
+ out.push(part);
87
+ return out;
88
+ };
89
+
90
+ /**
91
+ * Whether a value-set constraint ({values, delimiters?, parts?}) allows
92
+ * `value` as the server reads it: null and blank values pass, a non-string
93
+ * does not, and every part split on a delimiter (or only the first, with
94
+ * `parts: "first"`), trimmed, must be listed.
95
+ * @param {{values: string[], delimiters?: string, parts?: "all"|"first"}} constraint
96
+ * @param {any} value
97
+ * @returns {boolean}
98
+ */
99
+ export function valueSetAllows(constraint, value) {
100
+ if (value == null) return true;
101
+ if (typeof value !== "string") return false;
102
+ if (value.trim() === "") return true;
103
+ const delimiters = constraint?.delimiters || "";
104
+ const values = constraint?.values || [];
105
+ const parts = splitParts(value, delimiters);
106
+ if (constraint?.parts === "first") {
107
+ const firsts = new Set(values.map((v) => splitParts(v, delimiters)[0].trim()));
108
+ return firsts.has(parts[0].trim());
109
+ }
110
+ const allowed = new Set(values);
111
+ return parts.every((p) => p.trim() !== "" && allowed.has(p.trim()));
112
+ }
@@ -0,0 +1,2 @@
1
+ export function createdId(result: any | null | undefined): string | undefined;
2
+ export function createdIds(result: any | null | undefined): string[];
package/src/created.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The ids a create answered with. A create's response is `{id}` and a bulk
3
+ * create's is `{ids}`, read straight off the call or off its result in a
4
+ * batch (`{status, body}`). Every reader of a create response goes through
5
+ * these, so none guesses at the shape.
6
+ */
7
+
8
+ const isId = (v) => typeof v === "string";
9
+
10
+ /**
11
+ * The id a single create answered with, or undefined when it gave none.
12
+ * @param {Object|null|undefined} result - the create's response, or its batch result
13
+ * @returns {string|undefined}
14
+ */
15
+ export const createdId = (result) => {
16
+ if (isId(result?.id)) return result.id;
17
+ return isId(result?.body?.id) ? result.body.id : undefined;
18
+ };
19
+
20
+ /**
21
+ * The ids a bulk create answered with, in input order, or an empty array
22
+ * when it gave none.
23
+ * @param {Object|null|undefined} result - the bulk create's response, or its batch result
24
+ * @returns {string[]}
25
+ */
26
+ export const createdIds = (result) => {
27
+ if (Array.isArray(result?.ids)) return result.ids;
28
+ return Array.isArray(result?.body?.ids) ? result.body.ids : [];
29
+ };
@@ -0,0 +1,172 @@
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". Two extra requests a minute is the whole cost. A write the server
49
+ * took that moved the locked document's version renewed the lock as well, and
50
+ * the request layer tells the keeper (`writesStarted`, `writesLanded` in
51
+ * http.js), so renewals that fail with something other than a 423 while the
52
+ * run's writes land do not end the run as a lost lock.
53
+ *
54
+ * A renewal that fails means the block is no longer holding what it asked for,
55
+ * so it stops the run: the loss is recorded on the client and every later write
56
+ * throws `DocumentLockLost`. The guard sits in the request layer because the
57
+ * callers are services, and a check they have to remember to call is a check
58
+ * that is missing somewhere.
59
+ */
60
+ /**
61
+ * The lock a `documents.locked()` block was holding is no longer held.
62
+ *
63
+ * Thrown by the keep-alive when it cannot renew the lock, and then by the
64
+ * request layer on every write this client attempts, so work that has been
65
+ * running for minutes stops rather than writing over an edit that may have
66
+ * landed while the lock was gone.
67
+ */
68
+ export class DocumentLockLost extends Error {
69
+ /**
70
+ * @param {string} message
71
+ * @param {string} documentId
72
+ * @param {unknown} [cause]
73
+ */
74
+ constructor(message: string, documentId: string, cause?: unknown);
75
+ /** @type {string} */
76
+ documentId: string;
77
+ }
78
+ /**
79
+ * Renews one document's lock until the block holding it exits.
80
+ *
81
+ * `refresh` is called with the document id and must reject on failure.
82
+ * `onLost` is called once, with the `DocumentLockLost`, when the lock can no
83
+ * longer be assumed.
84
+ *
85
+ * `clock` and `sleep` exist so the whole policy can be run on a fake clock:
86
+ * `sleep(delayMs)` resolves to true once the block has exited.
87
+ */
88
+ export class LockKeeper {
89
+ constructor(refresh: any, documentId: any, ttlMs: any, { onLost, clock, sleep }?: {
90
+ onLost?: any;
91
+ clock?: () => number;
92
+ sleep?: any;
93
+ });
94
+ _refresh: any;
95
+ _documentId: any;
96
+ _ttlMs: any;
97
+ _onLost: any;
98
+ _clock: () => number;
99
+ _sleep: any;
100
+ _stopped: boolean;
101
+ _wake: any;
102
+ _running: Promise<void>;
103
+ _deadline: any;
104
+ /**
105
+ * The `DocumentLockLost` this keeper raised, or null.
106
+ * @type {DocumentLockLost | null}
107
+ */
108
+ lost: DocumentLockLost | null;
109
+ get documentId(): any;
110
+ /** This keeper's clock, which a write's send time is read on. */
111
+ now(): number;
112
+ /**
113
+ * A write of the locked document, sent at `sentAt`, was taken. The server
114
+ * renews a live lock on every write its holder makes to the document, from
115
+ * when it took the write, which is no earlier than `sentAt`. It counts only
116
+ * when its answer came back before the window this keeper counts ran out:
117
+ * the server then still held the lock when it took the write.
118
+ * @param {number} sentAt
119
+ */
120
+ noteWrite(sentAt: number): void;
121
+ /**
122
+ * Time between renewals: half the window, so a renewal that fails has a
123
+ * second chance before the lock it is renewing expires.
124
+ */
125
+ get intervalMs(): number;
126
+ /**
127
+ * Time before retrying a renewal that failed. A blip should not end a run
128
+ * that the server still considers the holder of.
129
+ */
130
+ get retryMs(): number;
131
+ _defaultSleep(ms: any): Promise<any>;
132
+ /** The beat. Resolves when the block exits or the lock is lost. */
133
+ run(): Promise<void>;
134
+ start(): Promise<void>;
135
+ stop(): void;
136
+ _fail(error: any): void;
137
+ }
138
+ /**
139
+ * What a `client.documents.locked(id, (lock) => ...)` block gets.
140
+ *
141
+ * The block does not have to consult it: a lost lock stops the next write on
142
+ * its own. Read `lock.lost` (or call `lock.raiseIfLost()`) to give up earlier,
143
+ * between steps of work that has not written anything yet.
144
+ */
145
+ export class DocumentLock {
146
+ /**
147
+ * @param {string} documentId
148
+ * @param {LockKeeper | null} keeper
149
+ * @param {string | null} [lockId]
150
+ */
151
+ constructor(documentId: string, keeper: LockKeeper | null, lockId?: string | null);
152
+ /** @type {string} */
153
+ documentId: string;
154
+ /**
155
+ * The holder id the acquire answered with.
156
+ * @type {string | null}
157
+ */
158
+ lockId: string | null;
159
+ _keeper: LockKeeper;
160
+ /**
161
+ * The `DocumentLockLost` if the lock lapsed, else null.
162
+ * @returns {DocumentLockLost | null}
163
+ */
164
+ get lost(): DocumentLockLost | null;
165
+ /** Throw if the lock lapsed. Call it between steps that have not written. */
166
+ raiseIfLost(): void;
167
+ }
168
+ /**
169
+ * The pause before sending again an acquire whose outcome is unknown, which
170
+ * grows by this much each time (three sends in all).
171
+ */
172
+ export const LOCK_ACQUIRE_RETRY_MS: 500;