@loams/client 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +104 -4
  2. package/dist/app.d.ts +149 -0
  3. package/dist/app.js +317 -0
  4. package/dist/builders/filter.d.ts +96 -0
  5. package/dist/builders/filter.js +190 -0
  6. package/dist/builders/schema.d.ts +50 -0
  7. package/dist/builders/schema.js +92 -0
  8. package/dist/builders/search.d.ts +159 -0
  9. package/dist/builders/search.js +307 -0
  10. package/dist/gen/facade.d.ts +267 -2
  11. package/dist/gen/facade.js +194 -0
  12. package/dist/handle.d.ts +67 -0
  13. package/dist/handle.js +107 -0
  14. package/dist/index.d.ts +13 -5
  15. package/dist/index.js +13 -3
  16. package/dist/loams.d.ts +78 -3
  17. package/dist/loams.js +84 -6
  18. package/dist/node.js +1 -1
  19. package/dist/runtime/batch.d.ts +29 -0
  20. package/dist/runtime/batch.js +95 -0
  21. package/dist/runtime/call.d.ts +63 -9
  22. package/dist/runtime/call.js +243 -41
  23. package/dist/runtime/consistency.d.ts +65 -11
  24. package/dist/runtime/consistency.js +212 -46
  25. package/dist/runtime/errors.d.ts +29 -2
  26. package/dist/runtime/errors.js +26 -0
  27. package/dist/runtime/machine-key.d.ts +33 -0
  28. package/dist/runtime/machine-key.js +174 -0
  29. package/dist/runtime/pagination.js +8 -2
  30. package/dist/runtime/subscribe.d.ts +37 -0
  31. package/dist/runtime/subscribe.js +128 -0
  32. package/dist/runtime/token-source.d.ts +1 -1
  33. package/dist/runtime/token-source.js +1 -1
  34. package/dist/runtime/transports.d.ts +22 -1
  35. package/dist/runtime/transports.js +28 -2
  36. package/dist/vendor/proto/collection-v1.d.ts +3 -0
  37. package/dist/vendor/proto/collection-v1.js +7 -0
  38. package/dist/vendor/proto/gen/loams/auth/v1/auth_pb.d.ts +7 -2
  39. package/dist/vendor/proto/gen/loams/auth/v1/auth_pb.js +5 -1
  40. package/dist/vendor/proto/gen/loams/collection/v1/collection_pb.d.ts +1610 -0
  41. package/dist/vendor/proto/gen/loams/collection/v1/collection_pb.js +399 -0
  42. package/dist/vendor/proto/gen/loams/collection/v1/document_pb.d.ts +1054 -0
  43. package/dist/vendor/proto/gen/loams/collection/v1/document_pb.js +327 -0
  44. package/dist/vendor/proto/gen/loams/collection/v1/query_pb.d.ts +2101 -0
  45. package/dist/vendor/proto/gen/loams/collection/v1/query_pb.js +608 -0
  46. package/dist/vendor/proto/gen/loams/graph/v1/graph_pb.d.ts +2299 -0
  47. package/dist/vendor/proto/gen/loams/graph/v1/graph_pb.js +596 -0
  48. package/dist/vendor/proto/gen/loams/instance/v1/instance_pb.d.ts +23 -7
  49. package/dist/vendor/proto/gen/loams/instance/v1/instance_pb.js +20 -5
  50. package/dist/vendor/proto/gen/loams/operations/v1/operations_pb.d.ts +399 -0
  51. package/dist/vendor/proto/gen/loams/operations/v1/operations_pb.js +118 -0
  52. package/dist/vendor/proto/gen/loams/sql/v1/sql_pb.d.ts +182 -0
  53. package/dist/vendor/proto/gen/loams/sql/v1/sql_pb.js +72 -0
  54. package/dist/vendor/proto/gen/loams/stream/v1/stream_pb.d.ts +688 -0
  55. package/dist/vendor/proto/gen/loams/stream/v1/stream_pb.js +180 -0
  56. package/package.json +2 -2
@@ -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 (!shouldRetry(error, plan.retrySafe, attempt, plan.maxRetries)) {
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 === undefined || session === false) {
102
+ if (session === false) {
101
103
  return undefined;
102
104
  }
103
- return session === true ? clientSession : session;
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
- /** Whether a call's request schema declares `idempotency_key`, per RPC. */
117
- keyed = new Map();
118
- constructor(transport, tokenSource, maxRetries, consistency) {
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
- this.keyed.set(`${service}/${method.name}`, method.input.field[IDEMPOTENCY_KEY] !== undefined);
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.keyed.get(`${binding.service}/${binding.method}`) ?? false;
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
- const method = client?.[binding.name];
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 { request: keyed, keyed: hasKey } = withIdempotencyKey(request, options.idempotencyKey, this.takesIdempotencyKey(binding));
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
- const result = (await callWithRetry(keyed, async () => method(keyed, { ...options, headers: await this.headers(options, consistency) }), {
171
- retrySafe,
172
- maxRetries,
173
- signal: options.signal,
174
- onRefresh: this.tokenSource?.refresh?.bind(this.tokenSource),
175
- }, binding.rpc));
176
- this.recordConsistency(session, result);
177
- return result;
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
- if (consistency !== undefined) {
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(request, { ...options, headers })), binding.rpc);
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 the consistency token a write answered with into the session. */
257
- recordConsistency(session, result) {
258
- if (session === undefined || result === undefined) {
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 token = result.consistencyToken;
262
- try {
263
- session.record(typeof token === 'string' && token !== '' ? token : undefined);
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
- catch {
266
- // The RPC already succeeded. A token the session cannot merge — the
267
- // encoding is still opaque, see `ConsistencySession` — must not turn a
268
- // committed write into a thrown error, because a caller that retries on
269
- // that error performs the write twice. `ConsistencySession.conflicts`
270
- // counts it instead.
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
- * Merging is deliberately conservative while the encoding is opaque: the store
9
- * keeps what it has, and a second, different token is a conflict the caller has
10
- * to resolve, not something to merge blindly.
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
- /** Folds a token the server returned in. */
17
- record(token: string | undefined): void;
18
- /** How many unmergeable pairs the session has seen. */
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 the token, so the next read is not held to it. */
74
+ /** Forgets every token, so the next read is not held to one. */
21
75
  clear(): void;
22
76
  }