@cortexkit/claustrum-client 0.12.0 → 0.13.0

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 (3) hide show
  1. package/README.md +15 -0
  2. package/dist/wire.js +59 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -23,6 +23,21 @@ credential caching, refresh, account scheduling, and retry policy.
23
23
  as a version older than every real one.
24
24
  • Unknown server error classes are bounded to `transient`; callers must retry them
25
25
  rather than treating a forward-compatible class as permanent.
26
+ • **Changed in 0.13.0:** a transport `Error` frame whose answer no retry or reconnect
27
+ can change is reported as `permanent` with action `gone`, and the client no longer
28
+ reconnects for it: `unknown_method`, `invalid_params`, `invalid_request`,
29
+ `flow_scopes_unsupported`, `invalid_control_body`, `unknown_module`, and the SDK's
30
+ pre-send `missing_identity`. These are not verdicts about a credential: do not mark
31
+ anything gone or needing re-authentication on them; stop retrying and surface the
32
+ code. `unknown_method` from a vault means the vault predates that operation (for
33
+ example an enrollment op against a vault older than the enrollment ceremony):
34
+ update the vault. `watchScoped` is the exception: there `unknown_method` resolves
35
+ `closed` as `unsupported`, the signal to fall back to polling `listScoped`. Before 0.13.0 these were `transient`/`retry`. Every other
36
+ transport failure is still `transient`/`retry`.
37
+ • Since 0.13.0, when a terminal transport failure makes the client reconnect and the
38
+ reconnect or the retried call then fails, the error carries the ORIGINAL call's code
39
+ and class. Earlier versions threw the reconnect's error instead, usually
40
+ `transport_error`, hiding the original call's code.
26
41
  • `listScoped` rows always carry `providerIds` (a required field since 0.6.0, so code
27
42
  that builds rows by hand, such as test fixtures, must set it). An empty array means
28
43
  no provider ids are set. Since 0.6.2 a daemon too old to send the field decodes the
package/dist/wire.js CHANGED
@@ -5,6 +5,37 @@ import { storeIdentity } from './identity.js';
5
5
  import { openScopedWatch } from './watch.js';
6
6
  const CLAUSTRUM_MODULE_ID = 'claustrum';
7
7
  const RECONNECT_BACKOFF_MS = 60_000;
8
+ /**
9
+ * Transport `Error` frame codes whose answer neither a retry nor a fresh connection can
10
+ * change. A call failing with one of these is reported as `permanent`/`gone` and is never
11
+ * reconnected; every other transport failure stays `transient`/`retry`.
12
+ *
13
+ * This is the ONLY list of such codes: the error classification and the reconnect
14
+ * decision both read it, so the two cannot disagree about a code.
15
+ *
16
+ * - `unknown_method`: the vault does not implement the operation. From a released vault
17
+ * this means the vault predates the operation, and the remedy is to update the vault.
18
+ * - `invalid_params`, `invalid_request`: the vault could not decode the request body or
19
+ * its params. The same bytes are refused the same way on any connection.
20
+ * - `flow_scopes_unsupported`: the vault refuses routes opened under a flow scope.
21
+ * - `invalid_control_body`: the vault build cannot decode the daemon's route bind.
22
+ * - `unknown_module`: the daemon has no module with this id registered or supervised at
23
+ * all. A vault that is restarting or warming up is reported as `module_warming` or
24
+ * `target_unavailable` instead, which the SDK already retries in place. A fresh
25
+ * connection to the same daemon gets the same answer, and the wasted reconnect would
26
+ * also start the reconnect backoff, leaving a genuinely wedged connection unable to
27
+ * recover for a minute.
28
+ * - `missing_identity`: the SDK refused before sending because no bind identity was set.
29
+ */
30
+ const DEFINITIVE_TRANSPORT_CODES = new Set([
31
+ 'unknown_method',
32
+ 'invalid_params',
33
+ 'invalid_request',
34
+ 'flow_scopes_unsupported',
35
+ 'invalid_control_body',
36
+ 'unknown_module',
37
+ 'missing_identity',
38
+ ]);
8
39
  function isRecord(value) {
9
40
  return typeof value === 'object' && value !== null && !Array.isArray(value);
10
41
  }
@@ -284,46 +315,45 @@ export class ClaustrumClient {
284
315
  });
285
316
  }
286
317
  catch (error) {
287
- if (this.#shouldReconnect(error)) {
288
- try {
289
- if (this.#client === sentClient)
290
- await this.#reconnect();
291
- }
292
- catch (reconnectError) {
293
- throw this.#asTransportError(reconnectError);
294
- }
295
- try {
296
- return await this.#client.call(CLAUSTRUM_MODULE_ID, method, params, {
297
- identity: this.#identity,
298
- consumerIdentity: null,
299
- });
300
- }
301
- catch (retryError) {
302
- throw this.#asTransportError(retryError);
303
- }
318
+ if (!this.#shouldReconnect(error))
319
+ throw this.#asTransportError(error);
320
+ try {
321
+ if (this.#client === sentClient)
322
+ await this.#reconnect();
323
+ return await this.#client.call(CLAUSTRUM_MODULE_ID, method, params, {
324
+ identity: this.#identity,
325
+ consumerIdentity: null,
326
+ });
327
+ }
328
+ catch (recoveryError) {
329
+ // Report the call's own failure, not the recovery's. A failed reconnect would
330
+ // otherwise replace the original call's code with `transport_error`, so
331
+ // the one actionable code would appear once and every later diagnostic would
332
+ // name a transport failure that never happened. Only a failure that carried no
333
+ // code at all has nothing to preserve.
334
+ throw this.#transportErrorCode(error) === undefined
335
+ ? this.#asTransportError(recoveryError)
336
+ : this.#asTransportError(error);
304
337
  }
305
- throw this.#asTransportError(error);
306
338
  }
307
339
  }
340
+ #transportErrorCode(error) {
341
+ return error instanceof SubcCallError && error.code ? error.code : undefined;
342
+ }
308
343
  #asTransportError(error) {
309
344
  if (error instanceof ClaustrumCredentialError)
310
345
  return error;
311
- const code = error instanceof SubcCallError && error.code ? error.code : 'transport_error';
312
- return new ClaustrumCredentialError(code, 'transient', 'retry');
346
+ const code = this.#transportErrorCode(error);
347
+ if (code !== undefined && DEFINITIVE_TRANSPORT_CODES.has(code)) {
348
+ return new ClaustrumCredentialError(code, 'permanent', 'gone');
349
+ }
350
+ return new ClaustrumCredentialError(code ?? 'transport_error', 'transient', 'retry');
313
351
  }
314
352
  #shouldReconnect(error) {
315
353
  return (!this.#closed &&
316
354
  error instanceof SubcCallError &&
317
355
  error.kind === 'terminal' &&
318
- error.code !== 'missing_identity' &&
319
- error.code !== 'invalid_control_body' &&
320
- // The daemon answers unknown_module only when no module with this id is
321
- // registered or supervised at all; a vault that is restarting or warming up
322
- // is reported as module_warming or target_unavailable, which the SDK already
323
- // retries in place. A fresh connection to the same daemon gets the same
324
- // answer, and the wasted reconnect would also start the reconnect backoff,
325
- // leaving a genuinely wedged connection unable to recover for a minute.
326
- error.code !== 'unknown_module');
356
+ !DEFINITIVE_TRANSPORT_CODES.has(error.code ?? ''));
327
357
  }
328
358
  async #reconnect() {
329
359
  if (this.#closed)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/claustrum-client",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",