@loams/client 0.1.0 → 0.1.1
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 +78 -2
- package/dist/app.d.ts +149 -0
- package/dist/app.js +317 -0
- package/dist/builders/filter.d.ts +96 -0
- package/dist/builders/filter.js +190 -0
- package/dist/builders/schema.d.ts +50 -0
- package/dist/builders/schema.js +92 -0
- package/dist/builders/search.d.ts +159 -0
- package/dist/builders/search.js +307 -0
- package/dist/gen/facade.d.ts +267 -2
- package/dist/gen/facade.js +194 -0
- package/dist/handle.d.ts +67 -0
- package/dist/handle.js +107 -0
- package/dist/index.d.ts +12 -5
- package/dist/index.js +12 -3
- package/dist/loams.d.ts +77 -2
- package/dist/loams.js +84 -6
- package/dist/runtime/batch.d.ts +29 -0
- package/dist/runtime/batch.js +95 -0
- package/dist/runtime/call.d.ts +63 -9
- package/dist/runtime/call.js +243 -41
- package/dist/runtime/consistency.d.ts +65 -11
- package/dist/runtime/consistency.js +212 -46
- package/dist/runtime/errors.d.ts +29 -2
- package/dist/runtime/errors.js +26 -0
- package/dist/runtime/pagination.js +8 -2
- package/dist/runtime/subscribe.d.ts +37 -0
- package/dist/runtime/subscribe.js +128 -0
- package/dist/vendor/proto/collection-v1.d.ts +3 -0
- package/dist/vendor/proto/collection-v1.js +7 -0
- package/dist/vendor/proto/gen/loams/collection/v1/collection_pb.d.ts +1610 -0
- package/dist/vendor/proto/gen/loams/collection/v1/collection_pb.js +399 -0
- package/dist/vendor/proto/gen/loams/collection/v1/document_pb.d.ts +1054 -0
- package/dist/vendor/proto/gen/loams/collection/v1/document_pb.js +327 -0
- package/dist/vendor/proto/gen/loams/collection/v1/query_pb.d.ts +2101 -0
- package/dist/vendor/proto/gen/loams/collection/v1/query_pb.js +608 -0
- package/dist/vendor/proto/gen/loams/graph/v1/graph_pb.d.ts +2299 -0
- package/dist/vendor/proto/gen/loams/graph/v1/graph_pb.js +596 -0
- package/dist/vendor/proto/gen/loams/operations/v1/operations_pb.d.ts +399 -0
- package/dist/vendor/proto/gen/loams/operations/v1/operations_pb.js +118 -0
- package/dist/vendor/proto/gen/loams/sql/v1/sql_pb.d.ts +182 -0
- package/dist/vendor/proto/gen/loams/sql/v1/sql_pb.js +72 -0
- package/dist/vendor/proto/gen/loams/stream/v1/stream_pb.d.ts +688 -0
- package/dist/vendor/proto/gen/loams/stream/v1/stream_pb.js +180 -0
- package/package.json +2 -2
package/dist/runtime/call.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
// - turns whatever is thrown into the typed `LoamsError` hierarchy, so a
|
|
12
12
|
// caller branches on `reason` and never on a message.
|
|
13
13
|
import { Code, createClient, } from '@connectrpc/connect';
|
|
14
|
-
import { LoamsError, TokenExpiredError, toLoamsError } from './errors.js';
|
|
14
|
+
import { DeadlineExceededError, InvalidArgumentError, LoamsError, TokenExpiredError, toLoamsError, } from './errors.js';
|
|
15
|
+
import { TOKEN_PREFIX } from './consistency.js';
|
|
15
16
|
import { DEFAULT_MAX_RETRIES, backoffMs, shouldRetry, sleep } from './retry.js';
|
|
16
17
|
import { uuidv7 } from './uuidv7.js';
|
|
17
18
|
/**
|
|
@@ -39,7 +40,8 @@ export async function callWithRetry(request, send, plan, rpc) {
|
|
|
39
40
|
attempt -= 1;
|
|
40
41
|
continue;
|
|
41
42
|
}
|
|
42
|
-
if (
|
|
43
|
+
if (plan.expired?.() === true ||
|
|
44
|
+
!shouldRetry(error, plan.retrySafe, attempt, plan.maxRetries)) {
|
|
43
45
|
throw error;
|
|
44
46
|
}
|
|
45
47
|
await sleep(backoffMs(attempt), plan.signal);
|
|
@@ -97,10 +99,61 @@ const IDEMPOTENCY_KEY = 'idempotencyKey';
|
|
|
97
99
|
*/
|
|
98
100
|
function resolveSession(options, clientSession) {
|
|
99
101
|
const session = options?.session;
|
|
100
|
-
if (session ===
|
|
102
|
+
if (session === false) {
|
|
101
103
|
return undefined;
|
|
102
104
|
}
|
|
103
|
-
|
|
105
|
+
if (session === undefined || session === true) {
|
|
106
|
+
return clientSession;
|
|
107
|
+
}
|
|
108
|
+
return session;
|
|
109
|
+
}
|
|
110
|
+
/** Whether a response string is something other than a consistency token. */
|
|
111
|
+
function isForeignToken(value) {
|
|
112
|
+
return !value.startsWith(TOKEN_PREFIX);
|
|
113
|
+
}
|
|
114
|
+
/** The unary deadline when neither the client nor the call sets one (R12). */
|
|
115
|
+
export const DEFAULT_TIMEOUT_MS = 30_000;
|
|
116
|
+
/** The response fields a consistency token is read from (R4, SDKP §2.1). */
|
|
117
|
+
const TOKEN_FIELDS = ['consistencyToken', 'token', 'readToken'];
|
|
118
|
+
/** The response header a consistency token is read from (R4). */
|
|
119
|
+
export const CONSISTENCY_TOKEN_HEADER = 'loams-consistency-token';
|
|
120
|
+
/**
|
|
121
|
+
* The packages whose `namespace` field is a Loams namespace (R11) and whose
|
|
122
|
+
* `consistency` field reads at a consistency token (R4). Other packages have
|
|
123
|
+
* fields of the same name that mean something else: `loams.live.v1`'s
|
|
124
|
+
* `CreateAppRequest.namespace` is free text naming an app's environment, and
|
|
125
|
+
* must never be filled from, or refused for lack of, the client's default.
|
|
126
|
+
*/
|
|
127
|
+
export const APP_PACKAGES = new Set([
|
|
128
|
+
'loams.collection.v1',
|
|
129
|
+
'loams.graph.v1',
|
|
130
|
+
'loams.sql.v1',
|
|
131
|
+
'loams.stream.v1',
|
|
132
|
+
]);
|
|
133
|
+
/**
|
|
134
|
+
* The packages whose reads take the session's token as `at_least` (R4,
|
|
135
|
+
* SDKP-17): collections and SQL. A graph's token names one graph's stream and
|
|
136
|
+
* a Graph read refuses any other, so graph reads never take the session's.
|
|
137
|
+
*/
|
|
138
|
+
export const SESSION_PACKAGES = new Set(['loams.collection.v1', 'loams.sql.v1']);
|
|
139
|
+
/** How a request's `consistency` message spells "at least this token". Both
|
|
140
|
+
* shapes in the protos are covered: a plain `at_least` field
|
|
141
|
+
* (`loams.collection.v1.Consistency`) and an `at_least` arm of a oneof
|
|
142
|
+
* (`loams.graph.v1.Consistency`). */
|
|
143
|
+
function atLeastOf(input) {
|
|
144
|
+
const field = input.field.consistency;
|
|
145
|
+
if (field === undefined || field.fieldKind !== 'message') {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
const atLeast = field.message.field.atLeast;
|
|
149
|
+
if (atLeast === undefined || atLeast.fieldKind !== 'scalar') {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
const oneof = atLeast.oneof;
|
|
153
|
+
if (oneof === undefined) {
|
|
154
|
+
return (token) => ({ atLeast: token });
|
|
155
|
+
}
|
|
156
|
+
return (token) => ({ [oneof.localName]: { case: 'atLeast', value: token } });
|
|
104
157
|
}
|
|
105
158
|
/**
|
|
106
159
|
* The client's call path: a generated `CallBinding` plus a request becomes a
|
|
@@ -113,13 +166,19 @@ export class CallInvoker {
|
|
|
113
166
|
consistency;
|
|
114
167
|
clients = new Map();
|
|
115
168
|
descriptors = new Map();
|
|
116
|
-
/**
|
|
117
|
-
|
|
118
|
-
|
|
169
|
+
/** What each RPC's request schema declares, per RPC. */
|
|
170
|
+
shapes = new Map();
|
|
171
|
+
defaults;
|
|
172
|
+
constructor(transport, tokenSource, maxRetries, consistency, defaults = {}) {
|
|
119
173
|
this.transport = transport;
|
|
120
174
|
this.tokenSource = tokenSource;
|
|
121
175
|
this.maxRetries = maxRetries;
|
|
122
176
|
this.consistency = consistency;
|
|
177
|
+
this.defaults = defaults;
|
|
178
|
+
}
|
|
179
|
+
/** The client's default namespace, if it has one (R11). */
|
|
180
|
+
get namespace() {
|
|
181
|
+
return this.defaults.namespace;
|
|
123
182
|
}
|
|
124
183
|
/**
|
|
125
184
|
* A module's Connect client, built once from the service descriptor
|
|
@@ -137,18 +196,79 @@ export class CallInvoker {
|
|
|
137
196
|
// from the object a caller happened to build. A caller who omits an
|
|
138
197
|
// `optional` field still gets a key, and `MutateRequest` (which has one)
|
|
139
198
|
// is never confused with `DeployRequest` (which does not).
|
|
140
|
-
|
|
199
|
+
const namespace = method.input.field.namespace;
|
|
200
|
+
const pkg = descriptor.file.proto.package ?? '';
|
|
201
|
+
const app = APP_PACKAGES.has(pkg);
|
|
202
|
+
this.shapes.set(`${service}/${method.name}`, {
|
|
203
|
+
keyed: method.input.field[IDEMPOTENCY_KEY] !== undefined,
|
|
204
|
+
namespaced: app &&
|
|
205
|
+
namespace !== undefined &&
|
|
206
|
+
namespace.fieldKind === 'scalar' &&
|
|
207
|
+
namespace.oneof === undefined,
|
|
208
|
+
atLeast: SESSION_PACKAGES.has(pkg) ? atLeastOf(method.input) : undefined,
|
|
209
|
+
});
|
|
141
210
|
}
|
|
142
211
|
this.clients.set(service, createClient(descriptor, this.transport));
|
|
143
212
|
}
|
|
213
|
+
/** What this call's request message declares. */
|
|
214
|
+
shape(binding) {
|
|
215
|
+
return (this.shapes.get(`${binding.service}/${binding.method}`) ?? {
|
|
216
|
+
keyed: false,
|
|
217
|
+
namespaced: false,
|
|
218
|
+
atLeast: undefined,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
144
221
|
/** Whether this call's request message declares an `idempotency_key`. */
|
|
145
222
|
takesIdempotencyKey(binding) {
|
|
146
|
-
return this.
|
|
223
|
+
return this.shape(binding).keyed;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The request as it goes out: the default namespace filled in (R11) and the
|
|
227
|
+
* session's token set as `consistency.at_least` when the caller left
|
|
228
|
+
* `consistency` unset (R4). A request the caller set either on is left as
|
|
229
|
+
* written: an explicit argument always wins.
|
|
230
|
+
*/
|
|
231
|
+
prepare(binding, request, consistency) {
|
|
232
|
+
if (typeof request !== 'object' || request === null) {
|
|
233
|
+
return request;
|
|
234
|
+
}
|
|
235
|
+
const shape = this.shape(binding);
|
|
236
|
+
let message = request;
|
|
237
|
+
if (shape.namespaced) {
|
|
238
|
+
const given = message.namespace;
|
|
239
|
+
if (typeof given !== 'string' || given === '') {
|
|
240
|
+
const fallback = this.defaults.namespace;
|
|
241
|
+
if (fallback === undefined || fallback === '') {
|
|
242
|
+
throw new InvalidArgumentError(`${binding.rpc} needs a namespace: pass one, or give the client a default namespace`, {
|
|
243
|
+
code: Code.InvalidArgument,
|
|
244
|
+
reason: 'namespace_required',
|
|
245
|
+
rpc: binding.rpc,
|
|
246
|
+
metadata: { field: 'namespace' },
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
message = { ...message, namespace: fallback };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (consistency !== undefined && shape.atLeast !== undefined && message.consistency === undefined) {
|
|
253
|
+
message = { ...message, consistency: shape.atLeast(consistency) };
|
|
254
|
+
}
|
|
255
|
+
return message;
|
|
256
|
+
}
|
|
257
|
+
/** The deadline a unary call runs under, in milliseconds, or undefined. */
|
|
258
|
+
unaryTimeout(options) {
|
|
259
|
+
const ms = options.timeoutMs ?? this.defaults.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
260
|
+
return ms > 0 ? ms : undefined;
|
|
147
261
|
}
|
|
148
262
|
/** The connect-es method a binding names, or a clear internal error. */
|
|
149
263
|
method(binding) {
|
|
150
264
|
const client = this.clients.get(binding.service);
|
|
151
|
-
|
|
265
|
+
// The connect-es client is keyed by the RPC's own local name
|
|
266
|
+
// (`createNamespace`), not by the facade name (`create`): the two differ
|
|
267
|
+
// whenever `FacadeOptions.name` renames a call, which every app module does.
|
|
268
|
+
const descriptor = this.descriptors.get(binding.service);
|
|
269
|
+
const local = descriptor?.methods.find((candidate) => candidate.name === binding.method)?.localName ??
|
|
270
|
+
binding.name;
|
|
271
|
+
const method = client?.[local];
|
|
152
272
|
if (typeof method !== 'function') {
|
|
153
273
|
throw new LoamsError(`the generated client for ${binding.service} has no method ${binding.name}`, { code: Code.Internal, rpc: binding.rpc });
|
|
154
274
|
}
|
|
@@ -156,35 +276,97 @@ export class CallInvoker {
|
|
|
156
276
|
}
|
|
157
277
|
/** One unary call: the whole runtime contract, applied to one binding. */
|
|
158
278
|
async unary(binding, request, options = {}) {
|
|
159
|
-
const
|
|
279
|
+
const session = resolveSession(options.consistency, this.consistency);
|
|
280
|
+
// An explicit token wins; otherwise the session's, which is what makes a
|
|
281
|
+
// session read-your-writes rather than record it and never send it (R4).
|
|
282
|
+
// R4 (SDKP-17): one token per proto package, so a collection write's token
|
|
283
|
+
// never becomes a graph read's `at_least`.
|
|
284
|
+
const consistency = options.consistency?.token ?? session?.current(binding.package);
|
|
285
|
+
const prepared = this.prepare(binding, request, consistency);
|
|
286
|
+
const { request: keyed, keyed: hasKey } = withIdempotencyKey(prepared, options.idempotencyKey, this.takesIdempotencyKey(binding));
|
|
160
287
|
const maxRetries = options.maxRetries ?? this.maxRetries;
|
|
161
288
|
// D610: a `safe` call always retries; a mutation retries once it carries an
|
|
162
289
|
// idempotency key, which `withIdempotencyKey` has just decided.
|
|
163
290
|
const retrySafe = options.retrySafe ?? (binding.retry === 'safe' || hasKey);
|
|
164
|
-
const session = resolveSession(options.consistency, this.consistency);
|
|
165
|
-
// An explicit token wins; otherwise the session's, which is what makes
|
|
166
|
-
// `consistency: { session: true }` read-your-writes rather than record it
|
|
167
|
-
// and never send it (R4).
|
|
168
|
-
const consistency = options.consistency?.token ?? session?.current();
|
|
169
291
|
const method = this.method(binding);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
292
|
+
// R12: one budget for the whole call, retries included. The SDK runs its
|
|
293
|
+
// own timer rather than trusting the transport's, so the budget holds over
|
|
294
|
+
// every transport, and so "the client ran out" is told apart from a
|
|
295
|
+
// server's `deadline_exceeded`, which may be retried.
|
|
296
|
+
const timeoutMs = this.unaryTimeout(options);
|
|
297
|
+
const deadline = timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
|
|
298
|
+
const controller = new AbortController();
|
|
299
|
+
let expired = false;
|
|
300
|
+
const onCallerAbort = () => controller.abort(options.signal?.reason);
|
|
301
|
+
if (options.signal?.aborted === true) {
|
|
302
|
+
controller.abort(options.signal.reason);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
options.signal?.addEventListener('abort', onCallerAbort, { once: true });
|
|
306
|
+
}
|
|
307
|
+
const timer = timeoutMs === undefined
|
|
308
|
+
? undefined
|
|
309
|
+
: setTimeout(() => {
|
|
310
|
+
expired = true;
|
|
311
|
+
controller.abort(new Error('client deadline exceeded'));
|
|
312
|
+
}, timeoutMs);
|
|
313
|
+
let headerToken;
|
|
314
|
+
const onHeader = (headers) => {
|
|
315
|
+
// A stub transport may answer without headers at all.
|
|
316
|
+
headerToken = headers?.get?.(CONSISTENCY_TOKEN_HEADER) ?? undefined;
|
|
317
|
+
if (headers !== undefined) {
|
|
318
|
+
options.onHeader?.(headers);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
try {
|
|
322
|
+
const result = (await callWithRetry(keyed, async () => method(keyed, {
|
|
323
|
+
...options,
|
|
324
|
+
signal: controller.signal,
|
|
325
|
+
...(deadline === undefined
|
|
326
|
+
? {}
|
|
327
|
+
: { timeoutMs: Math.max(1, deadline - Date.now()) }),
|
|
328
|
+
onHeader,
|
|
329
|
+
headers: await this.headers(options, consistency, this.shape(binding).atLeast === undefined),
|
|
330
|
+
}), {
|
|
331
|
+
retrySafe,
|
|
332
|
+
maxRetries,
|
|
333
|
+
signal: controller.signal,
|
|
334
|
+
onRefresh: this.tokenSource?.refresh?.bind(this.tokenSource),
|
|
335
|
+
expired: () => expired,
|
|
336
|
+
}, binding.rpc));
|
|
337
|
+
this.recordConsistency(session, result, headerToken, binding.package);
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
catch (thrown) {
|
|
341
|
+
if (expired) {
|
|
342
|
+
throw new DeadlineExceededError(`${binding.rpc} did not answer within the client's ${timeoutMs} ms deadline`, {
|
|
343
|
+
code: Code.DeadlineExceeded,
|
|
344
|
+
reason: 'client_deadline_exceeded',
|
|
345
|
+
rpc: binding.rpc,
|
|
346
|
+
metadata: { timeout_ms: String(timeoutMs) },
|
|
347
|
+
cause: thrown,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
throw toLoamsError(thrown, binding.rpc);
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
if (timer !== undefined) {
|
|
354
|
+
clearTimeout(timer);
|
|
355
|
+
}
|
|
356
|
+
options.signal?.removeEventListener('abort', onCallerAbort);
|
|
357
|
+
}
|
|
178
358
|
}
|
|
179
359
|
/** The headers a call carries: the caller's, plus the bearer and the
|
|
180
360
|
* consistency token the runtime owns. */
|
|
181
|
-
async headers(options, consistency) {
|
|
361
|
+
async headers(options, consistency, asHeader = true) {
|
|
182
362
|
const headers = { ...options.headers };
|
|
183
363
|
const bearer = await this.tokenSource?.token();
|
|
184
364
|
if (bearer !== undefined) {
|
|
185
365
|
headers.authorization = `Bearer ${bearer}`;
|
|
186
366
|
}
|
|
187
|
-
|
|
367
|
+
// A request with a `consistency` field carries the token there (R4); one
|
|
368
|
+
// without carries it as a header, which is all such an RPC can read.
|
|
369
|
+
if (consistency !== undefined && asHeader) {
|
|
188
370
|
headers['loams-consistency'] = consistency;
|
|
189
371
|
}
|
|
190
372
|
return headers;
|
|
@@ -200,7 +382,9 @@ export class CallInvoker {
|
|
|
200
382
|
stream(binding, request, options = {}) {
|
|
201
383
|
const method = this.method(binding);
|
|
202
384
|
const session = resolveSession(options.consistency, this.consistency);
|
|
203
|
-
const consistency = options.consistency?.token ?? session?.current();
|
|
385
|
+
const consistency = options.consistency?.token ?? session?.current(binding.package);
|
|
386
|
+
const prepared = this.prepare(binding, request, consistency);
|
|
387
|
+
const asHeader = this.shape(binding).atLeast === undefined;
|
|
204
388
|
const self = this;
|
|
205
389
|
const refresh = this.tokenSource?.refresh?.bind(this.tokenSource);
|
|
206
390
|
// Synchronous, because `loams.live.watch(...)` has to hand back an
|
|
@@ -211,8 +395,8 @@ export class CallInvoker {
|
|
|
211
395
|
let refreshed = false;
|
|
212
396
|
let yielded = false;
|
|
213
397
|
for (;;) {
|
|
214
|
-
const headers = await self.headers(options, consistency);
|
|
215
|
-
const source = self.mapped(streamOf(method(
|
|
398
|
+
const headers = await self.headers(options, consistency, asHeader);
|
|
399
|
+
const source = self.mapped(streamOf(method(prepared, { ...options, headers })), binding.rpc);
|
|
216
400
|
try {
|
|
217
401
|
for await (const message of source) {
|
|
218
402
|
yielded = true;
|
|
@@ -253,21 +437,39 @@ export class CallInvoker {
|
|
|
253
437
|
};
|
|
254
438
|
return invoke();
|
|
255
439
|
}
|
|
256
|
-
/** Merges
|
|
257
|
-
|
|
258
|
-
|
|
440
|
+
/** Merges every consistency token a response carried into the session:
|
|
441
|
+
* `consistency_token`, `token` and `read_token`, and the
|
|
442
|
+
* `loams-consistency-token` header (R4, SDKP §2.1). */
|
|
443
|
+
recordConsistency(session, result, headerToken, scope) {
|
|
444
|
+
if (session === undefined) {
|
|
259
445
|
return;
|
|
260
446
|
}
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
447
|
+
const tokens = [];
|
|
448
|
+
for (const field of TOKEN_FIELDS) {
|
|
449
|
+
const value = result?.[field];
|
|
450
|
+
if (typeof value === 'string' && value !== '') {
|
|
451
|
+
tokens.push(value);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (headerToken !== undefined && headerToken !== '') {
|
|
455
|
+
tokens.push(headerToken);
|
|
264
456
|
}
|
|
265
|
-
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
457
|
+
for (const token of tokens) {
|
|
458
|
+
// `token` is a common field name; only a `v1:` string is a consistency
|
|
459
|
+
// token, and anything else (a page token, a bearer) is not the
|
|
460
|
+
// session's business.
|
|
461
|
+
if (isForeignToken(token)) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
session.record(token, scope);
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
// The RPC already succeeded. A token the session cannot parse must not
|
|
469
|
+
// turn a committed write into a thrown error, because a caller that
|
|
470
|
+
// retries on that error performs the write twice. The session counts
|
|
471
|
+
// it (`ConsistencySession.rejected`) instead.
|
|
472
|
+
}
|
|
271
473
|
}
|
|
272
474
|
}
|
|
273
475
|
}
|
|
@@ -1,22 +1,76 @@
|
|
|
1
1
|
/** The prefix every token carries (§44 §7.4). */
|
|
2
2
|
export declare const TOKEN_PREFIX = "v1:";
|
|
3
|
-
/** Whether a string looks like a consistency token. */
|
|
3
|
+
/** Whether a string looks like a consistency token (the prefix only). */
|
|
4
4
|
export declare function isConsistencyToken(value: string): boolean;
|
|
5
|
+
/** One item of a token: the next offset after a write, for one partition. */
|
|
6
|
+
export interface TokenPosition {
|
|
7
|
+
readonly stream: bigint;
|
|
8
|
+
readonly partition: number;
|
|
9
|
+
readonly offset: bigint;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A parsed consistency token: `parse`, `toString`, `merge`, `equals`.
|
|
13
|
+
*
|
|
14
|
+
* Immutable. `merge` returns a new token, normalised (sorted by stream and
|
|
15
|
+
* partition, one item per pair, the max offset kept), which is exactly what
|
|
16
|
+
* `ConsistencyToken::merge` on the server produces, so the two print the same.
|
|
17
|
+
*/
|
|
18
|
+
export declare class ConsistencyToken {
|
|
19
|
+
readonly positions: readonly TokenPosition[];
|
|
20
|
+
private constructor();
|
|
21
|
+
/** The token that names nothing: `v1:`. */
|
|
22
|
+
static empty(): ConsistencyToken;
|
|
23
|
+
/** A token from its items, in the order given. */
|
|
24
|
+
static of(positions: readonly TokenPosition[]): ConsistencyToken;
|
|
25
|
+
/** Parses the text form, or throws `InvalidArgumentError`. The items are
|
|
26
|
+
* kept in the order given, as the server's parser keeps them. */
|
|
27
|
+
static parse(text: string): ConsistencyToken;
|
|
28
|
+
/** Whether `text` parses. */
|
|
29
|
+
static isValid(text: string): boolean;
|
|
30
|
+
/** Sorted by `(stream, partition)`, one item per pair, the max offset kept. */
|
|
31
|
+
normalized(): ConsistencyToken;
|
|
32
|
+
/** Both tokens' offsets; a pair both name keeps the larger. Normalised. */
|
|
33
|
+
merge(other: ConsistencyToken | string): ConsistencyToken;
|
|
34
|
+
/** The offset for `(stream, partition)`, the max if listed twice. */
|
|
35
|
+
offset(stream: bigint | number, partition: number): bigint | undefined;
|
|
36
|
+
/** Whether the two tokens name the same positions (after normalising). */
|
|
37
|
+
equals(other: ConsistencyToken | string): boolean;
|
|
38
|
+
/** Whether the token names nothing. */
|
|
39
|
+
get isEmpty(): boolean;
|
|
40
|
+
/** The text form, `v1:` plus the items joined by `,`, in this token's order. */
|
|
41
|
+
toString(): string;
|
|
42
|
+
toJSON(): string;
|
|
43
|
+
}
|
|
5
44
|
/**
|
|
6
|
-
* A session's consistency token
|
|
45
|
+
* A session's consistency tokens: every token a response carried, merged, kept
|
|
46
|
+
* **per proto package** (R4, SDKP-17).
|
|
47
|
+
*
|
|
48
|
+
* A Graph read refuses a token naming another package's stream, so a
|
|
49
|
+
* collection write's token must never become a graph read's `at_least`: the
|
|
50
|
+
* session holds one merged token per scope, and the call path passes the
|
|
51
|
+
* call's package as the scope. A caller using the store directly may leave the
|
|
52
|
+
* scope out, which is one shared scope.
|
|
7
53
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
54
|
+
* `record` folds a token in; `current` is the scope's merge, as text. A token
|
|
55
|
+
* that does not parse is counted in `rejected` and otherwise ignored: the write
|
|
56
|
+
* that answered with it has already happened, and turning a committed write
|
|
57
|
+
* into a thrown error invites a caller to retry it.
|
|
11
58
|
*/
|
|
12
59
|
export declare class ConsistencySession {
|
|
13
60
|
#private;
|
|
14
|
-
/** The token to attach to the next read. */
|
|
15
|
-
current(): string | undefined;
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
/**
|
|
61
|
+
/** The token to attach to the next read in `scope`, or undefined. */
|
|
62
|
+
current(scope?: string): string | undefined;
|
|
63
|
+
/** The merged token of `scope`, parsed. */
|
|
64
|
+
token(scope?: string): ConsistencyToken | undefined;
|
|
65
|
+
/** The scopes the session holds a token for. */
|
|
66
|
+
scopes(): string[];
|
|
67
|
+
/** Folds a token the server returned, in `scope`, in. Throws
|
|
68
|
+
* `InvalidArgumentError` on a malformed token, after counting it. */
|
|
69
|
+
record(token: string | ConsistencyToken | undefined, scope?: string): void;
|
|
70
|
+
/** How many tokens the session could not parse. */
|
|
71
|
+
get rejected(): number;
|
|
72
|
+
/** @deprecated The old name of `rejected`, from before tokens merged. */
|
|
19
73
|
get conflicts(): number;
|
|
20
|
-
/** Forgets
|
|
74
|
+
/** Forgets every token, so the next read is not held to one. */
|
|
21
75
|
clear(): void;
|
|
22
76
|
}
|