@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.
- package/README.md +39 -0
- package/package.json +13 -6
- package/src/apiAnnotations.d.ts +508 -0
- package/src/apiAnnotations.js +543 -0
- package/src/apiDocuments.d.ts +569 -0
- package/src/apiDocuments.js +551 -0
- package/src/apiLayers.d.ts +369 -0
- package/src/apiLayers.js +222 -0
- package/src/apiMessages.d.ts +293 -0
- package/src/apiMessages.js +265 -0
- package/src/apiPeople.d.ts +626 -0
- package/src/apiPeople.js +698 -0
- package/src/apiProject.d.ts +761 -0
- package/src/apiProject.js +774 -0
- package/src/apiServer.d.ts +455 -0
- package/src/apiServer.js +422 -0
- package/src/apiVocab.d.ts +456 -0
- package/src/apiVocab.js +425 -0
- package/src/batch.d.ts +54 -0
- package/src/batch.js +323 -0
- package/src/batchRef.d.ts +30 -0
- package/src/batchRef.js +128 -0
- package/src/client.d.ts +1321 -0
- package/src/client.js +743 -0
- package/src/codepoint.d.ts +94 -0
- package/src/codepoint.js +131 -1
- package/src/constraints.d.ts +85 -0
- package/src/constraints.js +112 -0
- package/src/created.d.ts +2 -0
- package/src/created.js +29 -0
- package/src/documentLock.d.ts +172 -0
- package/src/documentLock.js +384 -0
- package/src/entryForms.d.ts +63 -0
- package/src/entryForms.js +152 -0
- package/src/events.d.ts +60 -0
- package/src/events.js +271 -0
- package/src/families.d.ts +349 -0
- package/src/families.js +396 -0
- package/src/http.d.ts +233 -0
- package/src/http.js +809 -93
- package/src/ids.d.ts +26 -0
- package/src/ids.js +96 -0
- package/src/index.d.ts +101 -0
- package/src/index.js +183 -2105
- package/src/metadataOps.d.ts +15 -0
- package/src/metadataOps.js +124 -0
- package/src/operations.d.ts +36 -0
- package/src/operations.js +164 -0
- package/src/pagination.d.ts +49 -0
- package/src/pagination.js +20 -53
- package/src/provenance.d.ts +243 -0
- package/src/provenance.js +458 -36
- package/src/replayed.d.ts +3 -0
- package/src/replayed.js +31 -0
- package/src/requester.d.ts +60 -0
- package/src/requester.js +74 -0
- package/src/roles.d.ts +104 -0
- package/src/roles.js +64 -2
- package/src/serviceSchema.d.ts +337 -0
- package/src/serviceSchema.js +177 -60
- package/src/services.d.ts +319 -0
- package/src/services.js +742 -58
- package/src/sse.d.ts +30 -0
- package/src/sse.js +80 -3
- package/src/testing.d.ts +128 -0
- package/src/testing.js +379 -0
- package/src/textEdits.d.ts +59 -0
- package/src/textEdits.js +234 -0
- package/src/transforms.d.ts +25 -0
- package/src/transforms.js +11 -1
- package/src/types.d.ts +867 -0
- package/src/types.js +663 -0
- 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
|
-
/**
|
|
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
|
+
}
|
package/src/created.d.ts
ADDED
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;
|