@larc-iu/plaid-client 0.2.0-alpha.21 → 0.2.0-alpha.23
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 +35 -0
- package/index.d.ts +27 -5
- package/package.json +1 -1
- package/src/documentLock.js +2 -1
- package/src/http.js +25 -2
- package/src/ids.js +20 -0
- package/src/index.js +146 -30
- package/src/services.js +13 -2
- package/src/textEdits.js +18 -2
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @larc-iu/plaid-client
|
|
2
|
+
|
|
3
|
+
The JavaScript client for the [Plaid](https://larc-iu.github.io/plaid/) annotation API.
|
|
4
|
+
It has no dependencies and runs in browsers and in Node.
|
|
5
|
+
|
|
6
|
+
## From npm
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install @larc-iu/plaid-client@alpha
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
import { PlaidClient } from "@larc-iu/plaid-client";
|
|
14
|
+
|
|
15
|
+
const client = await PlaidClient.login("http://localhost:8080", "user@example.com", "password");
|
|
16
|
+
console.log(await client.projects.list());
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## From a Plaid server
|
|
20
|
+
|
|
21
|
+
Every Plaid server serves the client that matches its own version at `/client/plaid-client.js`, with its type declarations at `/client/plaid-client.d.ts`.
|
|
22
|
+
A page served by Plaid, for example from the folder set as `static_resources_path` in its `config.toml`, can import it without bundling a copy:
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<script type="module">
|
|
26
|
+
import { PlaidClient } from "/client/plaid-client.js";
|
|
27
|
+
const client = await PlaidClient.login(location.origin, "user@example.com", "password");
|
|
28
|
+
console.log(await client.projects.list());
|
|
29
|
+
</script>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Documentation
|
|
33
|
+
|
|
34
|
+
- [Manual](https://larc-iu.github.io/plaid/manual.html), which covers the API and its concepts.
|
|
35
|
+
- [API reference](https://larc-iu.github.io/plaid/api-js.html) for this client.
|
package/index.d.ts
CHANGED
|
@@ -422,6 +422,8 @@ interface UsersBundle {
|
|
|
422
422
|
interface UserDataEntry {
|
|
423
423
|
key: string;
|
|
424
424
|
updatedAt: string;
|
|
425
|
+
/** Raised by one on every write of the entry. A new entry is 1. */
|
|
426
|
+
version: number;
|
|
425
427
|
value?: any;
|
|
426
428
|
}
|
|
427
429
|
|
|
@@ -446,11 +448,18 @@ interface UserDataBundle {
|
|
|
446
448
|
opts?: UserDataOptions & { pageSize?: number },
|
|
447
449
|
): AsyncGenerator<UserDataEntry[]>;
|
|
448
450
|
get(userId: string, key: string): Promise<UserDataEntry & { value: any }>;
|
|
451
|
+
/**
|
|
452
|
+
* `version`: write only when the entry is still at this version (0: only
|
|
453
|
+
* when there is none). Refused with 409, `error: "version-mismatch"` and the
|
|
454
|
+
* stored `version` and `updatedAt` on the error's `responseData`, when
|
|
455
|
+
* another write landed since.
|
|
456
|
+
*/
|
|
449
457
|
put(
|
|
450
458
|
userId: string,
|
|
451
459
|
key: string,
|
|
452
460
|
value: any,
|
|
453
|
-
|
|
461
|
+
opts?: { version?: number },
|
|
462
|
+
): Promise<{ key: string; updatedAt: string; version: number }>;
|
|
454
463
|
delete(userId: string, key: string): Promise<any>;
|
|
455
464
|
}
|
|
456
465
|
|
|
@@ -923,6 +932,10 @@ interface AuditPageOptions {
|
|
|
923
932
|
order?: "asc" | "desc";
|
|
924
933
|
limit?: number;
|
|
925
934
|
cursor?: string;
|
|
935
|
+
/** Keep only each entry's oldest N operations in `ops` (1..1000). Every entry has `opCount`. */
|
|
936
|
+
opsLimit?: number;
|
|
937
|
+
/** Read only this entry rather than a page. */
|
|
938
|
+
entryId?: string;
|
|
926
939
|
}
|
|
927
940
|
|
|
928
941
|
/** One user's activity over a scope and window. */
|
|
@@ -1115,7 +1128,14 @@ interface DocumentsBundle {
|
|
|
1115
1128
|
onProgress?: (progress: { loaded: number; total: number | null }) => void;
|
|
1116
1129
|
},
|
|
1117
1130
|
): Promise<any>;
|
|
1118
|
-
deleteMedia(
|
|
1131
|
+
deleteMedia(
|
|
1132
|
+
documentId: string,
|
|
1133
|
+
auditMessage?: string,
|
|
1134
|
+
options?: {
|
|
1135
|
+
/** The recording meant, the `?v=` of the document's `mediaUrl`. A 409 when the stored one is another. */
|
|
1136
|
+
mediaVersion?: string;
|
|
1137
|
+
},
|
|
1138
|
+
): Promise<any>;
|
|
1119
1139
|
setMetadata(
|
|
1120
1140
|
documentId: string,
|
|
1121
1141
|
body: any,
|
|
@@ -1458,7 +1478,7 @@ interface TokensBundle {
|
|
|
1458
1478
|
tokenId: string,
|
|
1459
1479
|
position: number,
|
|
1460
1480
|
auditMessage?: string,
|
|
1461
|
-
options?: { id?: string },
|
|
1481
|
+
options?: { id?: string; keep?: "left" | "right" },
|
|
1462
1482
|
): Promise<any>;
|
|
1463
1483
|
merge(
|
|
1464
1484
|
tokenId: string,
|
|
@@ -1652,7 +1672,7 @@ export declare class PlaidClient {
|
|
|
1652
1672
|
} | null;
|
|
1653
1673
|
beginOperation(
|
|
1654
1674
|
message: string,
|
|
1655
|
-
opts?: {
|
|
1675
|
+
opts?: { groupId?: string; kind?: OperationKind; ref?: string; keys?: KeySeed; minted?: Set<string> },
|
|
1656
1676
|
): string;
|
|
1657
1677
|
/** A seed for the Idempotency-Keys of an operation that may be run again (see beginOperation's `keys`). */
|
|
1658
1678
|
keySeed(): KeySeed;
|
|
@@ -1660,7 +1680,7 @@ export declare class PlaidClient {
|
|
|
1660
1680
|
withOperation<T>(
|
|
1661
1681
|
message: string,
|
|
1662
1682
|
fn: (setMessage: (msg: string) => void) => Promise<T> | T,
|
|
1663
|
-
opts?: { kind?: OperationKind; ref?: string;
|
|
1683
|
+
opts?: { kind?: OperationKind; ref?: string; groupId?: string; keys?: KeySeed; minted?: Set<string> },
|
|
1664
1684
|
): Promise<T>;
|
|
1665
1685
|
operationGroups: OperationGroupsBundle;
|
|
1666
1686
|
|
|
@@ -1893,6 +1913,8 @@ export interface KeySeed {
|
|
|
1893
1913
|
}
|
|
1894
1914
|
/** A fresh UUIDv7, for the id of something a client creates. */
|
|
1895
1915
|
export function uuidv7(): string;
|
|
1916
|
+
/** A fresh random UUID (version 4), from crypto.getRandomValues, so it works on any page. */
|
|
1917
|
+
export function uuidv4(): string;
|
|
1896
1918
|
/** Whether a write's answer was replayed from its Idempotency-Key's first send (`Idempotent-Replayed: true`), so it wrote nothing new. Marked on an object or list answer as a non-enumerable `replayed: true`. */
|
|
1897
1919
|
export function wasReplayed(answer: unknown): boolean;
|
|
1898
1920
|
export function createdId(result: any): string | undefined;
|
package/package.json
CHANGED
package/src/documentLock.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isUnknownOutcome, retryUnknown } from "./http.js";
|
|
2
|
+
import { uuidv4 } from "./ids.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Keeping a document lock alive for as long as a `locked()` block runs.
|
|
@@ -216,7 +217,7 @@ export class DocumentLock {
|
|
|
216
217
|
export const LOCK_ACQUIRE_RETRY_MS = 500;
|
|
217
218
|
|
|
218
219
|
function mintLockId() {
|
|
219
|
-
return
|
|
220
|
+
return uuidv4();
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
/**
|
package/src/http.js
CHANGED
|
@@ -69,6 +69,23 @@ export const DEFAULT_TIMEOUT_MS = 30000;
|
|
|
69
69
|
// nothing and costs a retry stacked on a write that is still running.
|
|
70
70
|
export const DEFAULT_BATCH_TIMEOUT_MS = 180000;
|
|
71
71
|
|
|
72
|
+
// A query may run for core's own query limit (30 s, `*query-timeout-ms*` in
|
|
73
|
+
// plaid.sql.query.exec), and core then answers 408. The client waits a little
|
|
74
|
+
// longer than that, so the caller gets core's 408 for a query too broad and
|
|
75
|
+
// not a timeout of its own that reads like a lost connection.
|
|
76
|
+
export const DEFAULT_QUERY_TIMEOUT_MS = 35000;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The timeout for a query: the client's own, raised to
|
|
80
|
+
* DEFAULT_QUERY_TIMEOUT_MS when it is shorter. A disabled timeout (0, null)
|
|
81
|
+
* stays disabled.
|
|
82
|
+
*/
|
|
83
|
+
export function queryTimeout(client) {
|
|
84
|
+
const t = client.timeout;
|
|
85
|
+
if (!t || t <= 0) return t;
|
|
86
|
+
return Math.max(t, DEFAULT_QUERY_TIMEOUT_MS);
|
|
87
|
+
}
|
|
88
|
+
|
|
72
89
|
/**
|
|
73
90
|
* Note how far the server's clock is from this machine's, from a response's
|
|
74
91
|
* Date header, for `client.serverNow()`. A time the server stamped (an audit
|
|
@@ -738,6 +755,10 @@ export async function queueRequest(batch, method, path, options = {}) {
|
|
|
738
755
|
* skipResponseTransform - Return raw parsed JSON (no transformResponse)
|
|
739
756
|
* noAuth - Skip Authorization header
|
|
740
757
|
* binaryResponse - Return arrayBuffer instead of JSON/text
|
|
758
|
+
* noBusyRetry - If true, a 503 is not retried. For a query: core
|
|
759
|
+
* answers 503 when its queue of large queries stays full
|
|
760
|
+
* until the query's deadline, and asking again at once
|
|
761
|
+
* only joins the same queue.
|
|
741
762
|
* timeout - Per-request timeout in ms overriding client.timeout
|
|
742
763
|
* for this call (0/null disables). Used for known-long
|
|
743
764
|
* ops like project delete.
|
|
@@ -756,6 +777,7 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
756
777
|
timeout,
|
|
757
778
|
onUploadProgress,
|
|
758
779
|
learningOmittedVersion,
|
|
780
|
+
noBusyRetry,
|
|
759
781
|
} = options;
|
|
760
782
|
if (method !== "GET" && omittedStrictDocument(client)) {
|
|
761
783
|
await learnOmittedVersion(client);
|
|
@@ -825,12 +847,13 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
825
847
|
|
|
826
848
|
try {
|
|
827
849
|
const onRetry = (info) => client._noteRetry?.(info);
|
|
850
|
+
const busy = noBusyRetry ? { onRetry, retries: 0 } : { onRetry };
|
|
828
851
|
const response = keyed
|
|
829
|
-
? await retryUnknown(() => retryWhileBusy(attempt,
|
|
852
|
+
? await retryUnknown(() => retryWhileBusy(attempt, busy), {
|
|
830
853
|
delaysMs: client.retryDelaysMs,
|
|
831
854
|
onRetry,
|
|
832
855
|
})
|
|
833
|
-
: await retryWhileBusy(attempt,
|
|
856
|
+
: await retryWhileBusy(attempt, busy);
|
|
834
857
|
noteServerClock(client, response.headers);
|
|
835
858
|
|
|
836
859
|
if (!response.ok) {
|
package/src/ids.js
CHANGED
|
@@ -52,3 +52,23 @@ export function uuidv7() {
|
|
|
52
52
|
}
|
|
53
53
|
return s;
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A random UUIDv4, for ids that need no order (an operation group, a lock
|
|
58
|
+
* holder). Built on `crypto.getRandomValues`, since `crypto.randomUUID`
|
|
59
|
+
* exists only in a secure context and a page served over plain HTTP from
|
|
60
|
+
* another host than localhost is not one.
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
export function uuidv4() {
|
|
64
|
+
const b = new Uint8Array(16);
|
|
65
|
+
crypto.getRandomValues(b);
|
|
66
|
+
b[6] = (b[6] & 0x0f) | 0x40;
|
|
67
|
+
b[8] = (b[8] & 0x3f) | 0x80;
|
|
68
|
+
let s = "";
|
|
69
|
+
for (let i = 0; i < 16; i++) {
|
|
70
|
+
s += hex[b[i]];
|
|
71
|
+
if (i === 3 || i === 5 || i === 7 || i === 9) s += "-";
|
|
72
|
+
}
|
|
73
|
+
return s;
|
|
74
|
+
}
|
package/src/index.js
CHANGED
|
@@ -20,8 +20,9 @@ import {
|
|
|
20
20
|
REPLAYED_HEADER,
|
|
21
21
|
DEFAULT_TIMEOUT_MS,
|
|
22
22
|
DEFAULT_BATCH_TIMEOUT_MS,
|
|
23
|
+
queryTimeout,
|
|
23
24
|
} from "./http.js";
|
|
24
|
-
import { uuidv7 } from "./ids.js";
|
|
25
|
+
import { uuidv4, uuidv7 } from "./ids.js";
|
|
25
26
|
import { constraintsBody } from "./constraints.js";
|
|
26
27
|
import { listAll, listPage, iterPages } from "./pagination.js";
|
|
27
28
|
import { recorderFor } from "./events.js";
|
|
@@ -133,6 +134,21 @@ async function anonymousGet(baseUrl, path, options = {}) {
|
|
|
133
134
|
*/
|
|
134
135
|
const MAX_BATCH_OPS = 1000;
|
|
135
136
|
|
|
137
|
+
/**
|
|
138
|
+
* The kinds an operation may say it is (`?group-kind=`), the server's closed
|
|
139
|
+
* list (plaid.sql.operation-group/kinds). An operation of any other kind is
|
|
140
|
+
* refused when it begins, before it writes anything.
|
|
141
|
+
*/
|
|
142
|
+
const OPERATION_KINDS = [
|
|
143
|
+
"assistant-plan",
|
|
144
|
+
"service-run",
|
|
145
|
+
"import",
|
|
146
|
+
"bulk-edit",
|
|
147
|
+
"guess-adoption",
|
|
148
|
+
"repair",
|
|
149
|
+
"review",
|
|
150
|
+
];
|
|
151
|
+
|
|
136
152
|
/**
|
|
137
153
|
* How long `admin.backup()` waits for the server to finish writing a backup:
|
|
138
154
|
* minutes on a large database, past the usual per-request timeout.
|
|
@@ -528,13 +544,17 @@ class PlaidClient {
|
|
|
528
544
|
* @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
|
|
529
545
|
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
530
546
|
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
547
|
+
* @param {number} [opts.opsLimit] - Keep only each entry's oldest N
|
|
548
|
+
* operations in `ops` (1..1000). `opCount` on every entry says how
|
|
549
|
+
* many it has
|
|
550
|
+
* @param {string} [opts.entryId] - Read only this entry, not a page
|
|
531
551
|
* @param {string} [opts.itemId] - Only the changes that wrote this one
|
|
532
552
|
* entry, each with only its operations that did
|
|
533
553
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
534
554
|
*/
|
|
535
555
|
auditPage: (
|
|
536
556
|
id,
|
|
537
|
-
{ startTime, endTime, opTypes, kinds, order, limit, cursor, itemId } = {},
|
|
557
|
+
{ startTime, endTime, opTypes, kinds, order, limit, cursor, opsLimit, entryId, itemId } = {},
|
|
538
558
|
) =>
|
|
539
559
|
listPage(this, `/api/v1/vocab-layers/${id}/audit`, {
|
|
540
560
|
limit,
|
|
@@ -545,6 +565,8 @@ class PlaidClient {
|
|
|
545
565
|
"op-types": opTypesParam(opTypes),
|
|
546
566
|
kinds: kindsParam(kinds),
|
|
547
567
|
order,
|
|
568
|
+
"ops-limit": opsLimit,
|
|
569
|
+
"entry-id": entryId,
|
|
548
570
|
"item-id": itemId,
|
|
549
571
|
},
|
|
550
572
|
}),
|
|
@@ -1173,14 +1195,14 @@ class PlaidClient {
|
|
|
1173
1195
|
* List (or search) users. Transparently follows pagination cursors and
|
|
1174
1196
|
* returns the full flat array. Admin-or-maintainer only.
|
|
1175
1197
|
* @param {object} [opts]
|
|
1176
|
-
* @param {string} [opts.q] - Filter to users whose display name or email contains
|
|
1198
|
+
* @param {string} [opts.q] - Filter to users whose display name contains this text, or whose email starts with it (contains it, when the text has an @), case-insensitive
|
|
1177
1199
|
*/
|
|
1178
1200
|
list: ({ q } = {}) =>
|
|
1179
1201
|
listAll(this, "/api/v1/users", { query: { q } }),
|
|
1180
1202
|
/**
|
|
1181
1203
|
* Fetch a single page of users (optionally filtered by `q`).
|
|
1182
1204
|
* @param {object} [opts]
|
|
1183
|
-
* @param {string} [opts.q] - Filter to users whose display name or email contains
|
|
1205
|
+
* @param {string} [opts.q] - Filter to users whose display name contains this text, or whose email starts with it (contains it, when the text has an @), case-insensitive
|
|
1184
1206
|
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
1185
1207
|
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
1186
1208
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
@@ -1194,7 +1216,7 @@ class PlaidClient {
|
|
|
1194
1216
|
/**
|
|
1195
1217
|
* Async-iterate users page by page; yields each page's entries array.
|
|
1196
1218
|
* @param {object} [opts]
|
|
1197
|
-
* @param {string} [opts.q] - Filter to users whose display name or email contains
|
|
1219
|
+
* @param {string} [opts.q] - Filter to users whose display name contains this text, or whose email starts with it (contains it, when the text has an @), case-insensitive
|
|
1198
1220
|
* @param {number} [opts.pageSize] - Per-request page size
|
|
1199
1221
|
* @returns {AsyncGenerator<Array>}
|
|
1200
1222
|
*/
|
|
@@ -1256,11 +1278,15 @@ class PlaidClient {
|
|
|
1256
1278
|
* @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
|
|
1257
1279
|
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
1258
1280
|
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
1281
|
+
* @param {number} [opts.opsLimit] - Keep only each entry's oldest N
|
|
1282
|
+
* operations in `ops` (1..1000). `opCount` on every entry says how
|
|
1283
|
+
* many it has
|
|
1284
|
+
* @param {string} [opts.entryId] - Read only this entry, not a page
|
|
1259
1285
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
1260
1286
|
*/
|
|
1261
1287
|
auditPage: (
|
|
1262
1288
|
userId,
|
|
1263
|
-
{ startTime, endTime, opTypes, kinds, order, limit, cursor } = {},
|
|
1289
|
+
{ startTime, endTime, opTypes, kinds, order, limit, cursor, opsLimit, entryId } = {},
|
|
1264
1290
|
) =>
|
|
1265
1291
|
listPage(this, `/api/v1/users/${userId}/audit`, {
|
|
1266
1292
|
limit,
|
|
@@ -1271,6 +1297,8 @@ class PlaidClient {
|
|
|
1271
1297
|
"op-types": opTypesParam(opTypes),
|
|
1272
1298
|
kinds: kindsParam(kinds),
|
|
1273
1299
|
order,
|
|
1300
|
+
"ops-limit": opsLimit,
|
|
1301
|
+
"entry-id": entryId,
|
|
1274
1302
|
},
|
|
1275
1303
|
}),
|
|
1276
1304
|
/**
|
|
@@ -1446,7 +1474,7 @@ class PlaidClient {
|
|
|
1446
1474
|
query: { prefix, pattern, "include-values": includeValues },
|
|
1447
1475
|
}),
|
|
1448
1476
|
/**
|
|
1449
|
-
* Read one private data entry ({key, updatedAt, value}); 404 if absent.
|
|
1477
|
+
* Read one private data entry ({key, updatedAt, version, value}); 404 if absent.
|
|
1450
1478
|
* @param {string} userId
|
|
1451
1479
|
* @param {string} key
|
|
1452
1480
|
*/
|
|
@@ -1464,17 +1492,28 @@ class PlaidClient {
|
|
|
1464
1492
|
* value whose keys are camelCase round-trips unchanged while one keyed
|
|
1465
1493
|
* by arbitrary strings does not. Put such a map under a `metadata` key,
|
|
1466
1494
|
* which both clients pass through untouched.
|
|
1495
|
+
*
|
|
1496
|
+
* Every write raises the entry's `version` by one, and reads answer it.
|
|
1497
|
+
* With `version`, the write lands only when the entry is still at that
|
|
1498
|
+
* version (0: only when there is no entry), and is otherwise refused with
|
|
1499
|
+
* 409, `error: "version-mismatch"`, and the stored `version` and
|
|
1500
|
+
* `updatedAt` on the error's `responseData`: read it again and make the
|
|
1501
|
+
* change on what is there.
|
|
1467
1502
|
* @param {string} userId
|
|
1468
1503
|
* @param {string} key
|
|
1469
1504
|
* @param {*} value
|
|
1505
|
+
* @param {object} [opts]
|
|
1506
|
+
* @param {number} [opts.version] - The version this write was made from
|
|
1507
|
+
* @returns {Promise<{key: string, updatedAt: string, version: number}>}
|
|
1470
1508
|
*/
|
|
1471
|
-
put: (userId, key, value) =>
|
|
1509
|
+
put: (userId, key, value, { version } = {}) =>
|
|
1472
1510
|
this._request(
|
|
1473
1511
|
"PUT",
|
|
1474
1512
|
`/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
|
|
1475
1513
|
{
|
|
1476
1514
|
body: value,
|
|
1477
1515
|
noBatch: true,
|
|
1516
|
+
queryParams: { version },
|
|
1478
1517
|
},
|
|
1479
1518
|
),
|
|
1480
1519
|
/**
|
|
@@ -1839,9 +1878,13 @@ class PlaidClient {
|
|
|
1839
1878
|
* is what a feed wants. A cursor belongs to the direction that made it.
|
|
1840
1879
|
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
1841
1880
|
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
1881
|
+
* @param {number} [opts.opsLimit] - Keep only each entry's oldest N
|
|
1882
|
+
* operations in `ops` (1..1000). `opCount` on every entry says how
|
|
1883
|
+
* many it has
|
|
1884
|
+
* @param {string} [opts.entryId] - Read only this entry, not a page
|
|
1842
1885
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
1843
1886
|
*/
|
|
1844
|
-
listPage: ({ startTime, endTime, opTypes, kinds, order, limit, cursor } = {}) =>
|
|
1887
|
+
listPage: ({ startTime, endTime, opTypes, kinds, order, limit, cursor, opsLimit, entryId } = {}) =>
|
|
1845
1888
|
listPage(this, "/api/v1/audit", {
|
|
1846
1889
|
limit,
|
|
1847
1890
|
cursor,
|
|
@@ -1851,6 +1894,8 @@ class PlaidClient {
|
|
|
1851
1894
|
"op-types": opTypesParam(opTypes),
|
|
1852
1895
|
kinds: kindsParam(kinds),
|
|
1853
1896
|
order,
|
|
1897
|
+
"ops-limit": opsLimit,
|
|
1898
|
+
"entry-id": entryId,
|
|
1854
1899
|
},
|
|
1855
1900
|
}),
|
|
1856
1901
|
/**
|
|
@@ -2112,6 +2157,11 @@ class PlaidClient {
|
|
|
2112
2157
|
}),
|
|
2113
2158
|
/**
|
|
2114
2159
|
* Upload a media file for a document. Uses Apache Tika for content validation.
|
|
2160
|
+
* A document that already has a recording refuses it with a 409
|
|
2161
|
+
* (`media-exists`, and the current `media-url`): delete that one first.
|
|
2162
|
+
* A WAV whose samples are not PCM, 32-bit float, A-law or mu-law (IMA or
|
|
2163
|
+
* MS ADPCM, GSM, 64-bit float and the rest), which browsers cannot play,
|
|
2164
|
+
* is refused with a 415 whose `error` names its coding.
|
|
2115
2165
|
* @param {string} documentId - The document ID
|
|
2116
2166
|
* @param {File} file - The file to upload
|
|
2117
2167
|
* @param {string} [auditMessage] - Custom audit-log message for this write
|
|
@@ -2131,15 +2181,21 @@ class PlaidClient {
|
|
|
2131
2181
|
/**
|
|
2132
2182
|
* Delete media file for a document
|
|
2133
2183
|
* @param {string} documentId - The document ID
|
|
2184
|
+
* @param {string} [auditMessage] - Audit message for this write
|
|
2185
|
+
* @param {{mediaVersion?: string}} [options] - `mediaVersion`: the
|
|
2186
|
+
* recording meant, the `?v=` of the document's `mediaUrl`. The delete
|
|
2187
|
+
* is refused with a 409 (`media-changed`, and the current `media-url`)
|
|
2188
|
+
* when the stored recording is another one.
|
|
2134
2189
|
*/
|
|
2135
2190
|
// The upload above is multipart and cannot be batched, but a DELETE
|
|
2136
2191
|
// carries no blob, so the batch transport takes it. It is a write
|
|
2137
2192
|
// of the document's own data and queues like any other. Note that the
|
|
2138
2193
|
// file removal happens outside the server's transaction, so a batch that
|
|
2139
2194
|
// aborts after this op does not bring the file back.
|
|
2140
|
-
deleteMedia: (documentId, auditMessage) =>
|
|
2195
|
+
deleteMedia: (documentId, auditMessage, { mediaVersion } = {}) =>
|
|
2141
2196
|
this._request("DELETE", `/api/v1/documents/${documentId}/media`, {
|
|
2142
2197
|
auditMessage,
|
|
2198
|
+
queryParams: { "media-version": mediaVersion },
|
|
2143
2199
|
}),
|
|
2144
2200
|
/**
|
|
2145
2201
|
* Replace all metadata for a document.
|
|
@@ -2171,7 +2227,10 @@ class PlaidClient {
|
|
|
2171
2227
|
}),
|
|
2172
2228
|
/**
|
|
2173
2229
|
* Get audit log for a document. Transparently follows pagination cursors
|
|
2174
|
-
* and returns the full flat array.
|
|
2230
|
+
* and returns the full flat array. Each entry, and each of its `ops`,
|
|
2231
|
+
* says what kind of credential made it as `credential`: `login`,
|
|
2232
|
+
* `named-token`, `service` or `delegated` (absent on older operations),
|
|
2233
|
+
* beside `apiToken` (the named token's id and name) when there was one.
|
|
2175
2234
|
* @param {string} documentId - The document ID
|
|
2176
2235
|
* @param {string} [startTime] - Start of time range
|
|
2177
2236
|
* @param {string} [endTime] - End of time range
|
|
@@ -2200,11 +2259,15 @@ class PlaidClient {
|
|
|
2200
2259
|
* @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
|
|
2201
2260
|
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
2202
2261
|
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
2262
|
+
* @param {number} [opts.opsLimit] - Keep only each entry's oldest N
|
|
2263
|
+
* operations in `ops` (1..1000). `opCount` on every entry says how
|
|
2264
|
+
* many it has
|
|
2265
|
+
* @param {string} [opts.entryId] - Read only this entry, not a page
|
|
2203
2266
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
2204
2267
|
*/
|
|
2205
2268
|
auditPage: (
|
|
2206
2269
|
documentId,
|
|
2207
|
-
{ startTime, endTime, opTypes, kinds, order, limit, cursor } = {},
|
|
2270
|
+
{ startTime, endTime, opTypes, kinds, order, limit, cursor, opsLimit, entryId } = {},
|
|
2208
2271
|
) =>
|
|
2209
2272
|
listPage(this, `/api/v1/documents/${documentId}/audit`, {
|
|
2210
2273
|
limit,
|
|
@@ -2215,6 +2278,8 @@ class PlaidClient {
|
|
|
2215
2278
|
"op-types": opTypesParam(opTypes),
|
|
2216
2279
|
kinds: kindsParam(kinds),
|
|
2217
2280
|
order,
|
|
2281
|
+
"ops-limit": opsLimit,
|
|
2282
|
+
"entry-id": entryId,
|
|
2218
2283
|
},
|
|
2219
2284
|
}),
|
|
2220
2285
|
/**
|
|
@@ -2401,7 +2466,10 @@ class PlaidClient {
|
|
|
2401
2466
|
),
|
|
2402
2467
|
/**
|
|
2403
2468
|
* Get audit log for a project. Transparently follows pagination cursors
|
|
2404
|
-
* and returns the full flat array.
|
|
2469
|
+
* and returns the full flat array. Each entry, and each of its `ops`,
|
|
2470
|
+
* says what kind of credential made it as `credential`: `login`,
|
|
2471
|
+
* `named-token`, `service` or `delegated` (absent on older operations),
|
|
2472
|
+
* beside `apiToken` (the named token's id and name) when there was one.
|
|
2405
2473
|
* @param {string} projectId - The project ID
|
|
2406
2474
|
* @param {string} [startTime] - Start of time range
|
|
2407
2475
|
* @param {string} [endTime] - End of time range
|
|
@@ -2430,11 +2498,15 @@ class PlaidClient {
|
|
|
2430
2498
|
* @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
|
|
2431
2499
|
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
2432
2500
|
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
2501
|
+
* @param {number} [opts.opsLimit] - Keep only each entry's oldest N
|
|
2502
|
+
* operations in `ops` (1..1000). `opCount` on every entry says how
|
|
2503
|
+
* many it has
|
|
2504
|
+
* @param {string} [opts.entryId] - Read only this entry, not a page
|
|
2433
2505
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
2434
2506
|
*/
|
|
2435
2507
|
auditPage: (
|
|
2436
2508
|
projectId,
|
|
2437
|
-
{ startTime, endTime, opTypes, kinds, order, limit, cursor } = {},
|
|
2509
|
+
{ startTime, endTime, opTypes, kinds, order, limit, cursor, opsLimit, entryId } = {},
|
|
2438
2510
|
) =>
|
|
2439
2511
|
listPage(this, `/api/v1/projects/${projectId}/audit`, {
|
|
2440
2512
|
limit,
|
|
@@ -2445,6 +2517,8 @@ class PlaidClient {
|
|
|
2445
2517
|
"op-types": opTypesParam(opTypes),
|
|
2446
2518
|
kinds: kindsParam(kinds),
|
|
2447
2519
|
order,
|
|
2520
|
+
"ops-limit": opsLimit,
|
|
2521
|
+
"entry-id": entryId,
|
|
2448
2522
|
},
|
|
2449
2523
|
}),
|
|
2450
2524
|
/**
|
|
@@ -2762,8 +2836,11 @@ class PlaidClient {
|
|
|
2762
2836
|
* losers are deleted. Links are read when the merge runs, so one made
|
|
2763
2837
|
* after the caller looked moves too. Every loser must be in the
|
|
2764
2838
|
* survivor's vocabulary, and a loser that is already gone is skipped, so
|
|
2765
|
-
* a repeated merge changes nothing.
|
|
2766
|
-
*
|
|
2839
|
+
* a repeated merge changes nothing. Metadata naming a loser by its id,
|
|
2840
|
+
* in the vocabulary's other entries and in the documents of every
|
|
2841
|
+
* project it is linked to, is rewritten to name the survivor. A
|
|
2842
|
+
* reference to a loser in the survivor's own metadata is the caller's to
|
|
2843
|
+
* rewrite, in the same batch.
|
|
2767
2844
|
* Needs maintainer rights on the vocabulary.
|
|
2768
2845
|
* @param {string} survivorId - The entry that stays
|
|
2769
2846
|
* @param {string[]} loserIds - The entries merged into it
|
|
@@ -2966,8 +3043,11 @@ class PlaidClient {
|
|
|
2966
3043
|
* @param {string} tokenId - The token ID
|
|
2967
3044
|
* @param {number} position - Code-point offset to split at (strictly between begin and end)
|
|
2968
3045
|
* @param {string} [auditMessage] - Audit message for this write
|
|
2969
|
-
* @param {{id?: string}} [options] - `id`: the
|
|
2970
|
-
*
|
|
3046
|
+
* @param {{id?: string, keep?: "left"|"right"}} [options] - `id`: the new
|
|
3047
|
+
* token's id, a UUIDv7 this client minted. `keep`: the half the original token
|
|
3048
|
+
* (its id, spans, vocab-links, comments and metadata) stays on, `"left"` by
|
|
3049
|
+
* default. With `"right"` the new token, whose id is answered, is the left
|
|
3050
|
+
* half. A relation layer whose relations must stay inside one token
|
|
2971
3051
|
* of this layer declares a same-ancestor constraint, and the server deletes
|
|
2972
3052
|
* the relations the split leaves crossing in the same transaction.
|
|
2973
3053
|
*/
|
|
@@ -2975,11 +3055,11 @@ class PlaidClient {
|
|
|
2975
3055
|
tokenId,
|
|
2976
3056
|
position,
|
|
2977
3057
|
auditMessage,
|
|
2978
|
-
{ id } = {},
|
|
3058
|
+
{ id, keep } = {},
|
|
2979
3059
|
) =>
|
|
2980
3060
|
this._request("POST", `/api/v1/tokens/${tokenId}/split`, {
|
|
2981
3061
|
auditMessage,
|
|
2982
|
-
body: bodyOf({ id, position }),
|
|
3062
|
+
body: bodyOf({ id, keep, position }),
|
|
2983
3063
|
}),
|
|
2984
3064
|
/**
|
|
2985
3065
|
* Merge two tokens. The left token (smaller begin) survives with the combined
|
|
@@ -3228,9 +3308,16 @@ class PlaidClient {
|
|
|
3228
3308
|
*/
|
|
3229
3309
|
// outOfBand: a query is a read that travels as a POST (see the note at
|
|
3230
3310
|
// the top of http.js). Made on a batch it goes over the wire like any
|
|
3231
|
-
// read, and it never joins a logical operation.
|
|
3311
|
+
// read, and it never joins a logical operation. It waits past core's own
|
|
3312
|
+
// query limit, so a query too broad answers with core's 408, and a 503
|
|
3313
|
+
// from core's full queue of large queries is not retried.
|
|
3232
3314
|
this.query = (body) =>
|
|
3233
|
-
this._request("POST", "/api/v1/query", {
|
|
3315
|
+
this._request("POST", "/api/v1/query", {
|
|
3316
|
+
body,
|
|
3317
|
+
outOfBand: true,
|
|
3318
|
+
timeout: queryTimeout(this),
|
|
3319
|
+
noBusyRetry: true,
|
|
3320
|
+
});
|
|
3234
3321
|
|
|
3235
3322
|
// Logical-operation groups (audit-log grouping). There is no create: a
|
|
3236
3323
|
// group row is made lazily by the first write carrying `?group-id=`
|
|
@@ -3691,6 +3778,7 @@ class PlaidClient {
|
|
|
3691
3778
|
* `kind` says what kind of operation this is, for a program reading the
|
|
3692
3779
|
* log: one of `assistant-plan`, `service-run`, `import`, `bulk-edit`,
|
|
3693
3780
|
* `guess-adoption`, `repair` or `review` (the server refuses any other).
|
|
3781
|
+
* Any other kind throws here, before anything is written.
|
|
3694
3782
|
* `ref` is a short string naming what the operation came from, in the
|
|
3695
3783
|
* shape its kind documents (the core manual, "Kinds of operation"). Both
|
|
3696
3784
|
* are recorded from the first write like the label, and a nested operation
|
|
@@ -3706,10 +3794,15 @@ class PlaidClient {
|
|
|
3706
3794
|
* matching end, and the outer numbering resumes after it.
|
|
3707
3795
|
*
|
|
3708
3796
|
* @param {string} message - Human label for the operation.
|
|
3709
|
-
* @param {object} [opts] - Optional `{
|
|
3797
|
+
* @param {object} [opts] - Optional `{ groupId, kind, ref, keys, minted }`. `groupId` adopts an existing group id instead of minting one (a service joining the requester's operation; `requestService` propagates an open operation to the service automatically). Python's is `group_id`. `kind`, `ref` and `keys` are described above. `minted` is the ids the operation mints for what it creates: a create refused 409 id-taken for one of them was made by an earlier send of the operation, and answers as made.
|
|
3710
3798
|
* @returns {string} The operation's group id.
|
|
3711
3799
|
*/
|
|
3712
|
-
beginOperation(message, {
|
|
3800
|
+
beginOperation(message, { groupId, kind, ref, keys, minted } = {}) {
|
|
3801
|
+
if (kind != null && !OPERATION_KINDS.includes(String(kind))) {
|
|
3802
|
+
throw new Error(
|
|
3803
|
+
`Unknown operation kind "${kind}". It is one of: ${OPERATION_KINDS.join(", ")}.`,
|
|
3804
|
+
);
|
|
3805
|
+
}
|
|
3713
3806
|
const frame = {
|
|
3714
3807
|
keys: keys || null,
|
|
3715
3808
|
count: 0,
|
|
@@ -3733,7 +3826,7 @@ class PlaidClient {
|
|
|
3733
3826
|
return open.id;
|
|
3734
3827
|
}
|
|
3735
3828
|
this.operationGroup = {
|
|
3736
|
-
id:
|
|
3829
|
+
id: groupId ? String(groupId) : uuidv4(),
|
|
3737
3830
|
message: message == null ? null : String(message),
|
|
3738
3831
|
kind: kind == null ? null : String(kind),
|
|
3739
3832
|
ref: ref == null ? null : String(ref),
|
|
@@ -3807,11 +3900,11 @@ class PlaidClient {
|
|
|
3807
3900
|
*
|
|
3808
3901
|
* @param {string} message - Human label for the operation.
|
|
3809
3902
|
* @param {function} fn - The work to run; receives `setMessage(msg)` to refine the label once the outcome is known.
|
|
3810
|
-
* @param {object} [opts] - Optional `{ kind, ref,
|
|
3903
|
+
* @param {object} [opts] - Optional `{ kind, ref, groupId, keys, minted }`, as for beginOperation. `kind` is one of `assistant-plan`, `service-run`, `import`, `bulk-edit`, `guess-adoption`, `repair` or `review`, and any other throws before `fn` runs.
|
|
3811
3904
|
* @returns {Promise<any>} Whatever `fn` resolves to.
|
|
3812
3905
|
*/
|
|
3813
|
-
async withOperation(message, fn, { kind, ref,
|
|
3814
|
-
this.beginOperation(message, { kind, ref,
|
|
3906
|
+
async withOperation(message, fn, { kind, ref, groupId, keys, minted } = {}) {
|
|
3907
|
+
this.beginOperation(message, { kind, ref, groupId, keys, minted });
|
|
3815
3908
|
const group = this.operationGroup;
|
|
3816
3909
|
// The key frame this call opened is ended by this call, whatever else
|
|
3817
3910
|
// began or ended meanwhile, not by whichever end comes at its depth.
|
|
@@ -3957,6 +4050,28 @@ class PlaidClient {
|
|
|
3957
4050
|
}
|
|
3958
4051
|
}
|
|
3959
4052
|
}
|
|
4053
|
+
// The batch's own header holds each document's version after the whole
|
|
4054
|
+
// batch, the layer rules' remedies at its end included, which no single
|
|
4055
|
+
// operation's answer does: a split that deletes a relation crossing
|
|
4056
|
+
// the new boundary moves the version once more.
|
|
4057
|
+
const outerVersions = response.headers?.get?.("X-Document-Versions");
|
|
4058
|
+
if (outerVersions) {
|
|
4059
|
+
try {
|
|
4060
|
+
const versionsMap = JSON.parse(outerVersions);
|
|
4061
|
+
if (typeof versionsMap === "object" && versionsMap !== null) {
|
|
4062
|
+
this.documentVersions = mergeVersions(
|
|
4063
|
+
this.documentVersions,
|
|
4064
|
+
versionsMap,
|
|
4065
|
+
replayed,
|
|
4066
|
+
);
|
|
4067
|
+
}
|
|
4068
|
+
} catch (e) {
|
|
4069
|
+
console.warn(
|
|
4070
|
+
"Failed to parse document versions header from batch response:",
|
|
4071
|
+
e,
|
|
4072
|
+
);
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
3960
4075
|
|
|
3961
4076
|
const out = results.map((result) => ({
|
|
3962
4077
|
...result,
|
|
@@ -4314,7 +4429,8 @@ export {
|
|
|
4314
4429
|
// result. See ./created.js.
|
|
4315
4430
|
export { createdId, createdIds } from "./created.js";
|
|
4316
4431
|
// UUIDv7 ids for what a client creates. See ./ids.js and the manual,
|
|
4317
|
-
// "Retrying a write".
|
|
4318
|
-
|
|
4432
|
+
// "Retrying a write". uuidv4 works where crypto.randomUUID does not (a page
|
|
4433
|
+
// served over plain HTTP from any host but localhost).
|
|
4434
|
+
export { uuidv4, uuidv7 } from "./ids.js";
|
|
4319
4435
|
export { wasReplayed } from "./replayed.js";
|
|
4320
4436
|
export { MAX_BATCH_OPS };
|
package/src/services.js
CHANGED
|
@@ -444,7 +444,8 @@ export function serve(client, projectId, serviceInfo, onServiceRequest, extras =
|
|
|
444
444
|
* @param {string} serviceId - Service ID to request
|
|
445
445
|
* @param {any} data - Request payload
|
|
446
446
|
* @param {number} [timeout=10000] - How long the service may say NOTHING, in ms.
|
|
447
|
-
* Every event it sends starts the clock again, so this does not cap a long run
|
|
447
|
+
* Every event it sends starts the clock again, so this does not cap a long run.
|
|
448
|
+
* 0, a negative number or null: no limit (wait until `signal` stops it)
|
|
448
449
|
* @param {function} [onProgress] - Called with each progress payload {percent, message}
|
|
449
450
|
* @param {AbortSignal} [signal] - Abort to stop waiting; rejects with an AbortError
|
|
450
451
|
* @param {Object} [opts]
|
|
@@ -494,8 +495,15 @@ export function requestService(client, projectId, serviceId, data, timeout = 100
|
|
|
494
495
|
url: `${client.baseUrl}/api/v1/projects/${projectId}/services/${encodeURIComponent(serviceId)}/requests${qs}`,
|
|
495
496
|
method: 'POST',
|
|
496
497
|
body: JSON.stringify(payload === undefined ? null : transformRequest(payload)),
|
|
498
|
+
// `notLive` (with `serviceId`) lets an app say it in the service's own
|
|
499
|
+
// name rather than its id.
|
|
497
500
|
onStatus: (response) =>
|
|
498
|
-
response.status === 503
|
|
501
|
+
response.status === 503
|
|
502
|
+
? Object.assign(new Error(`No live service '${serviceId}' on this project`), {
|
|
503
|
+
notLive: true,
|
|
504
|
+
serviceId,
|
|
505
|
+
})
|
|
506
|
+
: null,
|
|
499
507
|
what: 'Service request',
|
|
500
508
|
},
|
|
501
509
|
timeout,
|
|
@@ -526,6 +534,7 @@ export function requestService(client, projectId, serviceId, data, timeout = 100
|
|
|
526
534
|
* @param {string} projectId - Project UUID
|
|
527
535
|
* @param {string} requestId - The request id (from `onAccepted` or your own)
|
|
528
536
|
* @param {number} [timeout=10000] - How long the service may say NOTHING, in ms
|
|
537
|
+
* (0, a negative number or null: no limit)
|
|
529
538
|
* @param {function} [onProgress] - Called with each progress payload {percent, message}
|
|
530
539
|
* @param {AbortSignal} [signal] - Abort to stop waiting; rejects with an AbortError
|
|
531
540
|
* @returns {Promise<any>} The service's result
|
|
@@ -702,9 +711,11 @@ function streamServiceRequest(client, { url, method, body, onStatus, what }, tim
|
|
|
702
711
|
// deadline on the whole run it killed working transcriptions at the
|
|
703
712
|
// five-minute default and left their writes to land on a document the page
|
|
704
713
|
// had already handed back to the user as editable.
|
|
714
|
+
// 0, a negative number or null is no limit, as for every other timeout.
|
|
705
715
|
let timer;
|
|
706
716
|
const waitAgain = () => {
|
|
707
717
|
clearTimeout(timer);
|
|
718
|
+
if (!(timeout > 0)) return;
|
|
708
719
|
timer = setTimeout(
|
|
709
720
|
() =>
|
|
710
721
|
finish(reject, stillRunning(new Error(`${what} timed out after ${timeout}ms of silence`))),
|
package/src/textEdits.js
CHANGED
|
@@ -20,6 +20,22 @@
|
|
|
20
20
|
|
|
21
21
|
const codePoints = (s) => [...s];
|
|
22
22
|
|
|
23
|
+
// The last body split into code points, kept: an edit log composes the same
|
|
24
|
+
// long body once for each of its kept states, and splitting it each time made
|
|
25
|
+
// a save on a long text block the page (H31-TEXT-1). Frozen, as every caller
|
|
26
|
+
// shares it.
|
|
27
|
+
let lastBody = null;
|
|
28
|
+
let lastPoints = Object.freeze([]);
|
|
29
|
+
|
|
30
|
+
/** The code points of `body`, as a frozen array shared with other callers. */
|
|
31
|
+
export function bodyCodePoints(body) {
|
|
32
|
+
if (body !== lastBody) {
|
|
33
|
+
lastPoints = Object.freeze(codePoints(body));
|
|
34
|
+
lastBody = body;
|
|
35
|
+
}
|
|
36
|
+
return lastPoints;
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
function opName(op) {
|
|
24
40
|
return JSON.stringify(op);
|
|
25
41
|
}
|
|
@@ -98,7 +114,7 @@ function cutAt(segs, at) {
|
|
|
98
114
|
* malformed or out-of-bounds op.
|
|
99
115
|
*/
|
|
100
116
|
export function composeTextEdits(body, ops) {
|
|
101
|
-
const old =
|
|
117
|
+
const old = bodyCodePoints(body);
|
|
102
118
|
let segs = old.length ? [{ old: true, start: 0, end: old.length }] : [];
|
|
103
119
|
let len = old.length;
|
|
104
120
|
for (const op of ops) {
|
|
@@ -128,7 +144,7 @@ export function composeTextEdits(body, ops) {
|
|
|
128
144
|
close(seg.start);
|
|
129
145
|
kept = seg.end;
|
|
130
146
|
} else {
|
|
131
|
-
|
|
147
|
+
for (const c of seg.chars) typed.push(c);
|
|
132
148
|
}
|
|
133
149
|
}
|
|
134
150
|
close(old.length);
|