@larc-iu/plaid-client 0.2.0-alpha.23 → 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 +4 -0
- package/package.json +11 -5
- package/src/batchRef.d.ts +30 -0
- package/src/codepoint.d.ts +94 -0
- package/src/codepoint.js +131 -1
- package/src/constraints.d.ts +77 -0
- package/src/constraints.js +26 -1
- package/src/created.d.ts +2 -0
- package/src/documentLock.d.ts +158 -0
- package/src/documentLock.js +27 -3
- package/src/events.d.ts +60 -0
- package/src/events.js +7 -3
- package/src/http.d.ts +212 -0
- package/src/http.js +7 -0
- package/src/ids.d.ts +26 -0
- package/src/ids.js +22 -0
- package/src/index.d.ts +3814 -0
- package/src/index.js +939 -699
- package/src/metadataOps.d.ts +15 -0
- package/src/metadataOps.js +13 -8
- package/src/pagination.d.ts +49 -0
- package/src/pagination.js +2 -19
- package/src/provenance.d.ts +200 -0
- package/src/provenance.js +34 -6
- package/src/replayed.d.ts +3 -0
- package/src/replayed.js +5 -1
- package/src/requester.d.ts +60 -0
- package/src/requester.js +21 -3
- package/src/roles.d.ts +65 -0
- package/src/roles.js +3 -2
- package/src/serviceSchema.d.ts +331 -0
- package/src/serviceSchema.js +75 -9
- package/src/services.d.ts +302 -0
- package/src/services.js +67 -10
- package/src/sse.d.ts +30 -0
- package/src/sse.js +11 -0
- package/src/textEdits.d.ts +59 -0
- package/src/textEdits.js +26 -1
- package/src/transforms.d.ts +25 -0
- package/index.d.ts +0 -2026
package/README.md
CHANGED
|
@@ -33,3 +33,7 @@ A page served by Plaid, for example from the folder set as `static_resources_pat
|
|
|
33
33
|
|
|
34
34
|
- [Manual](https://larc-iu.github.io/plaid/manual.html), which covers the API and its concepts.
|
|
35
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.
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
+
};
|
package/src/constraints.js
CHANGED
|
@@ -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 {
|
|
41
|
+
* @returns {ConstraintViolation[] | null}
|
|
17
42
|
*/
|
|
18
43
|
export function violationsOf(err) {
|
|
19
44
|
const vs = err?.status === 422 ? err?.responseData?.violations : undefined;
|
package/src/created.d.ts
ADDED
|
@@ -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;
|
package/src/documentLock.js
CHANGED
|
@@ -39,10 +39,18 @@ import { uuidv4 } from "./ids.js";
|
|
|
39
39
|
* landed while the lock was gone.
|
|
40
40
|
*/
|
|
41
41
|
export class DocumentLockLost extends Error {
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} message
|
|
44
|
+
* @param {string} documentId
|
|
45
|
+
* @param {unknown} [cause]
|
|
46
|
+
*/
|
|
42
47
|
constructor(message, documentId, cause) {
|
|
43
48
|
super(message);
|
|
49
|
+
/** @type {"DocumentLockLost"} */
|
|
44
50
|
this.name = "DocumentLockLost";
|
|
51
|
+
/** @type {string} */
|
|
45
52
|
this.documentId = documentId;
|
|
53
|
+
/** @type {unknown} */
|
|
46
54
|
this.cause = cause;
|
|
47
55
|
}
|
|
48
56
|
}
|
|
@@ -101,7 +109,10 @@ export class LockKeeper {
|
|
|
101
109
|
this._stopped = false;
|
|
102
110
|
this._wake = null;
|
|
103
111
|
this._running = null;
|
|
104
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* The `DocumentLockLost` this keeper raised, or null.
|
|
114
|
+
* @type {DocumentLockLost | null}
|
|
115
|
+
*/
|
|
105
116
|
this.lost = null;
|
|
106
117
|
}
|
|
107
118
|
|
|
@@ -193,18 +204,31 @@ export class LockKeeper {
|
|
|
193
204
|
* between steps of work that has not written anything yet.
|
|
194
205
|
*/
|
|
195
206
|
export class DocumentLock {
|
|
207
|
+
/**
|
|
208
|
+
* @param {string} documentId
|
|
209
|
+
* @param {LockKeeper | null} keeper
|
|
210
|
+
* @param {string | null} [lockId]
|
|
211
|
+
*/
|
|
196
212
|
constructor(documentId, keeper, lockId = null) {
|
|
213
|
+
/** @type {string} */
|
|
197
214
|
this.documentId = documentId;
|
|
198
|
-
/**
|
|
215
|
+
/**
|
|
216
|
+
* The holder id the acquire answered with.
|
|
217
|
+
* @type {string | null}
|
|
218
|
+
*/
|
|
199
219
|
this.lockId = lockId;
|
|
200
220
|
this._keeper = keeper;
|
|
201
221
|
}
|
|
202
222
|
|
|
203
|
-
/**
|
|
223
|
+
/**
|
|
224
|
+
* The `DocumentLockLost` if the lock lapsed, else null.
|
|
225
|
+
* @returns {DocumentLockLost | null}
|
|
226
|
+
*/
|
|
204
227
|
get lost() {
|
|
205
228
|
return this._keeper ? this._keeper.lost : null;
|
|
206
229
|
}
|
|
207
230
|
|
|
231
|
+
/** Throw if the lock lapsed. Call it between steps that have not written. */
|
|
208
232
|
raiseIfLost() {
|
|
209
233
|
if (this.lost) throw this.lost;
|
|
210
234
|
}
|