@abloatai/ablo 0.35.0 → 0.36.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 (215) hide show
  1. package/CHANGELOG.md +713 -629
  2. package/README.md +56 -519
  3. package/bin/ablo.cjs +39 -0
  4. package/dist/BaseSyncedStore.d.ts +24 -4
  5. package/dist/BaseSyncedStore.js +53 -37
  6. package/dist/Database.d.ts +8 -20
  7. package/dist/Database.js +61 -59
  8. package/dist/InstanceCache.d.ts +6 -2
  9. package/dist/InstanceCache.js +18 -16
  10. package/dist/Model.d.ts +17 -7
  11. package/dist/Model.js +17 -7
  12. package/dist/ModelRegistry.d.ts +4 -0
  13. package/dist/ModelRegistry.js +17 -15
  14. package/dist/NetworkMonitor.d.ts +3 -1
  15. package/dist/NetworkMonitor.js +7 -5
  16. package/dist/SyncClient.d.ts +5 -15
  17. package/dist/SyncClient.js +60 -57
  18. package/dist/client/Ablo.js +30 -19
  19. package/dist/client/createInternalComponents.d.ts +4 -0
  20. package/dist/client/createInternalComponents.js +8 -2
  21. package/dist/client/createModelProxy.d.ts +21 -1
  22. package/dist/client/createModelProxy.js +123 -57
  23. package/dist/client/humans.d.ts +30 -9
  24. package/dist/client/humans.js +45 -19
  25. package/dist/client/reactiveEngine.d.ts +16 -11
  26. package/dist/client/reactiveEngine.js +113 -335
  27. package/dist/client/storeCluster.d.ts +47 -0
  28. package/dist/client/storeCluster.js +118 -0
  29. package/dist/client/storeLifecycle.d.ts +61 -0
  30. package/dist/client/storeLifecycle.js +231 -0
  31. package/dist/context.d.ts +13 -0
  32. package/dist/context.js +23 -0
  33. package/dist/core/index.d.ts +1 -1
  34. package/dist/docs/catalog.js +6 -3
  35. package/dist/index.d.ts +4 -2
  36. package/dist/index.js +4 -2
  37. package/dist/query/client.d.ts +3 -0
  38. package/dist/query/client.js +6 -5
  39. package/dist/react/AbloProvider.d.ts +13 -1
  40. package/dist/react/AbloProvider.js +5 -2
  41. package/dist/react/context.d.ts +2 -2
  42. package/dist/react/createAbloReact.d.ts +56 -0
  43. package/dist/react/createAbloReact.js +51 -0
  44. package/dist/react/index.d.ts +1 -0
  45. package/dist/react/index.js +3 -0
  46. package/dist/react/useAblo.d.ts +9 -2
  47. package/dist/react/useAblo.js +25 -7
  48. package/dist/schema/coordination.js +5 -1
  49. package/dist/schema/index.d.ts +1 -0
  50. package/dist/schema/index.js +4 -0
  51. package/dist/schema/select.js +3 -0
  52. package/dist/schema/serialize.js +3 -0
  53. package/dist/source/adapter.d.ts +7 -5
  54. package/dist/source/adapter.js +7 -5
  55. package/dist/source/index.d.ts +1 -1
  56. package/dist/source/index.js +1 -1
  57. package/dist/{core/storeContract.d.ts → storeContract.d.ts} +5 -5
  58. package/dist/{core → stores}/DatabaseManager.d.ts +3 -1
  59. package/dist/{core → stores}/DatabaseManager.js +13 -12
  60. package/dist/{core → stores}/StoreManager.d.ts +5 -3
  61. package/dist/{core → stores}/StoreManager.js +24 -22
  62. package/dist/stores/SyncActionStore.d.ts +3 -1
  63. package/dist/stores/SyncActionStore.js +9 -7
  64. package/dist/sync/BootstrapFetcher.d.ts +4 -0
  65. package/dist/sync/BootstrapFetcher.js +28 -26
  66. package/dist/sync/OnDemandLoader.d.ts +3 -0
  67. package/dist/sync/OnDemandLoader.js +1 -0
  68. package/dist/sync/bootstrapApply.d.ts +3 -0
  69. package/dist/sync/bootstrapApply.js +2 -2
  70. package/dist/sync/deltaPipeline.d.ts +13 -12
  71. package/dist/sync/deltaPipeline.js +21 -4
  72. package/dist/sync/groupChange.d.ts +3 -0
  73. package/dist/sync/groupChange.js +16 -14
  74. package/dist/sync/participants.d.ts +19 -2
  75. package/dist/sync/participants.js +3 -1
  76. package/dist/sync/schemas.d.ts +2 -1
  77. package/dist/sync/schemas.js +3 -3
  78. package/dist/syncLog/contract.d.ts +20 -0
  79. package/dist/syncLog/contract.js +19 -0
  80. package/dist/syncLog/index.d.ts +1 -0
  81. package/dist/syncLog/index.js +1 -0
  82. package/dist/transaction/auth/capability.d.ts +35 -0
  83. package/dist/transaction/auth/capability.js +25 -0
  84. package/dist/transaction/coordination/awaitClaimGrant.d.ts +7 -0
  85. package/dist/transaction/coordination/awaitClaimGrant.js +12 -0
  86. package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +34 -0
  87. package/dist/transaction/coordination/claimHeartbeatLoop.js +20 -0
  88. package/dist/transaction/coordination/index.d.ts +4 -4
  89. package/dist/transaction/coordination/index.js +4 -3
  90. package/dist/transaction/coordination/locator.d.ts +23 -2
  91. package/dist/transaction/coordination/locator.js +22 -2
  92. package/dist/transaction/coordination/schema.d.ts +125 -62
  93. package/dist/transaction/coordination/schema.js +228 -64
  94. package/dist/transaction/coordination/targetConflict.js +32 -28
  95. package/dist/transaction/errorCodes.d.ts +2 -2
  96. package/dist/transaction/errorCodes.js +13 -9
  97. package/dist/transaction/plugin.d.ts +95 -2
  98. package/dist/transaction/plugin.js +21 -2
  99. package/dist/transaction/resources/httpResources.d.ts +57 -2
  100. package/dist/transaction/resources/modelOperations.d.ts +148 -40
  101. package/dist/transaction/resources/where.d.ts +16 -0
  102. package/dist/transaction/resources/where.js +45 -0
  103. package/dist/transaction/schema/field.d.ts +12 -18
  104. package/dist/transaction/schema/fieldRef.d.ts +38 -0
  105. package/dist/transaction/schema/fieldRef.js +11 -0
  106. package/dist/transaction/schema/openapi.d.ts +16 -15
  107. package/dist/transaction/schema/openapi.js +186 -25
  108. package/dist/transaction/schema/relation.d.ts +7 -2
  109. package/dist/transaction/schema/schema.d.ts +27 -0
  110. package/dist/transaction/schema/schema.js +20 -0
  111. package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +1 -1
  112. package/dist/transaction/transactions/settlement/pendingWrite.d.ts +1 -1
  113. package/dist/transaction/transport/httpClient.d.ts +9 -1
  114. package/dist/transaction/transport/httpClient.js +1 -0
  115. package/dist/transaction/transport/httpTransport.js +156 -44
  116. package/dist/transaction/transport/wsTransport.d.ts +2 -4
  117. package/dist/transaction/transport/wsTransport.js +16 -10
  118. package/dist/transaction/types/streams.d.ts +10 -0
  119. package/dist/transaction/utils/duration.d.ts +25 -0
  120. package/dist/transaction/utils/duration.js +32 -0
  121. package/dist/transaction/wire/accountResponses.d.ts +69 -0
  122. package/dist/transaction/wire/accountResponses.js +36 -1
  123. package/dist/transaction/wire/auth.d.ts +9 -2
  124. package/dist/transaction/wire/auth.js +7 -1
  125. package/dist/transaction/wire/claims.d.ts +164 -97
  126. package/dist/transaction/wire/claims.js +126 -28
  127. package/dist/transaction/wire/commit.d.ts +1 -1
  128. package/dist/transaction/wire/feedEvent.d.ts +27 -0
  129. package/dist/transaction/wire/feedEvent.js +27 -1
  130. package/dist/transaction/wire/frames.d.ts +2 -2
  131. package/dist/transaction/wire/inboundFrames.d.ts +12 -2
  132. package/dist/transaction/wire/index.d.ts +10 -6
  133. package/dist/transaction/wire/index.js +12 -3
  134. package/dist/transaction/wire/modelMutations.d.ts +31 -0
  135. package/dist/transaction/wire/modelMutations.js +52 -0
  136. package/dist/transaction/wire/modelShape.d.ts +78 -0
  137. package/dist/transaction/wire/modelShape.js +74 -0
  138. package/dist/transactions/mutations/MutationQueue.d.ts +6 -0
  139. package/dist/transactions/mutations/MutationQueue.js +55 -45
  140. package/dist/transactions/mutations/commitPayload.d.ts +3 -2
  141. package/dist/transactions/mutations/commitPayload.js +5 -5
  142. package/dist/transactions/mutations/deltaConfirmation.d.ts +4 -0
  143. package/dist/transactions/mutations/deltaConfirmation.js +10 -8
  144. package/dist/transactions/mutations/replayValidation.d.ts +2 -1
  145. package/dist/transactions/mutations/replayValidation.js +3 -2
  146. package/dist/{core → views}/QueryView.d.ts +1 -1
  147. package/dist/{core → views}/QueryView.js +1 -1
  148. package/dist/{core → views}/ViewRegistry.d.ts +1 -1
  149. package/dist/{core/queryUtils.d.ts → views/incrementalView.d.ts} +6 -6
  150. package/dist/{core/queryUtils.js → views/incrementalView.js} +6 -6
  151. package/docs/agents.md +1 -1
  152. package/docs/api-keys.md +6 -6
  153. package/docs/api.md +5 -43
  154. package/docs/audit.md +4 -3
  155. package/docs/cli.md +11 -11
  156. package/docs/client-behavior.md +4 -4
  157. package/docs/concurrency-convention.md +28 -42
  158. package/docs/coordination.md +235 -83
  159. package/docs/data-sources.md +4 -4
  160. package/docs/debugging.md +34 -12
  161. package/docs/deployment.md +8 -8
  162. package/docs/examples/scoped-agent.md +3 -3
  163. package/docs/groups.md +57 -3
  164. package/docs/guarantees.md +37 -10
  165. package/docs/how-it-works.md +29 -5
  166. package/docs/idempotency.md +6 -6
  167. package/docs/identity.md +22 -23
  168. package/docs/index.md +8 -8
  169. package/docs/integration-guide.md +14 -3
  170. package/docs/mcp.md +7 -7
  171. package/docs/migration.md +34 -15
  172. package/docs/projects.md +1 -1
  173. package/docs/react.md +19 -8
  174. package/docs/sessions.md +1 -1
  175. package/docs/webhooks.md +9 -9
  176. package/llms.txt +4 -4
  177. package/package.json +13 -20
  178. package/dist/cli.cjs +0 -288600
  179. package/dist/testing/fixtures/bootstrap.d.ts +0 -49
  180. package/dist/testing/fixtures/bootstrap.js +0 -59
  181. package/dist/testing/fixtures/deltas.d.ts +0 -83
  182. package/dist/testing/fixtures/deltas.js +0 -136
  183. package/dist/testing/fixtures/httpResponses.d.ts +0 -70
  184. package/dist/testing/fixtures/httpResponses.js +0 -90
  185. package/dist/testing/fixtures/models.d.ts +0 -83
  186. package/dist/testing/fixtures/models.js +0 -272
  187. package/dist/testing/helpers/reactWrapper.d.ts +0 -69
  188. package/dist/testing/helpers/reactWrapper.js +0 -67
  189. package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
  190. package/dist/testing/helpers/syncEngineHarness.js +0 -73
  191. package/dist/testing/helpers/wait.d.ts +0 -30
  192. package/dist/testing/helpers/wait.js +0 -49
  193. package/dist/testing/index.d.ts +0 -23
  194. package/dist/testing/index.js +0 -33
  195. package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
  196. package/dist/testing/mocks/FakeDatabase.js +0 -10
  197. package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
  198. package/dist/testing/mocks/MockMutationExecutor.js +0 -186
  199. package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
  200. package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
  201. package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
  202. package/dist/testing/mocks/MockSyncContext.js +0 -72
  203. package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
  204. package/dist/testing/mocks/MockSyncStore.js +0 -171
  205. package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
  206. package/dist/testing/mocks/MockWebSocket.js +0 -118
  207. package/docs/interaction-model.md +0 -99
  208. /package/dist/{core → query}/QueryProcessor.d.ts +0 -0
  209. /package/dist/{core → query}/QueryProcessor.js +0 -0
  210. /package/dist/{core/storeContract.js → storeContract.js} +0 -0
  211. /package/dist/{core → stores}/openIDBWithTimeout.d.ts +0 -0
  212. /package/dist/{core → stores}/openIDBWithTimeout.js +0 -0
  213. /package/dist/{source → transaction}/footprint.d.ts +0 -0
  214. /package/dist/{source → transaction}/footprint.js +0 -0
  215. /package/dist/{core → views}/ViewRegistry.js +0 -0
@@ -0,0 +1,118 @@
1
+ /**
2
+ * The store cluster — what `humans()` constructs at `init` now that the
3
+ * plugin context carries the resolved url and the credential source
4
+ * (ADR 0016; docs/plans/package-split.md, sequence step 2): this client's
5
+ * runtime, the internal component graph, the registered model classes, and
6
+ * the `BaseSyncedStore` that orchestrates them. The engine consumes the
7
+ * cluster instead of constructing it; what remains in the composition root
8
+ * (credential lifecycle, ready(), model proxies, resources) migrates in
9
+ * later steps.
10
+ *
11
+ * The cluster rides the humans surface under {@link kStoreCluster}, a
12
+ * symbol: `layerPluginSurface` merges only string-keyed members onto the
13
+ * client, so the handoff never becomes public client API. When the plugin
14
+ * contract adopts context patches (the decoration end-state), this channel
15
+ * dissolves into them.
16
+ */
17
+ import { shouldUseInMemoryPersistence } from '../transaction/persistence.js';
18
+ import { noopObservability, browserOnlineStatus, defaultSessionErrorDetector, noopAnalytics, } from '../RuntimeContext.js';
19
+ import { initRuntime } from '../context.js';
20
+ import { alwaysOnline } from '../adapters/alwaysOnline.js';
21
+ import { BaseSyncedStore } from '../BaseSyncedStore.js';
22
+ import { SyncWebSocket } from '../sync/SyncWebSocket.js';
23
+ import { createInternalComponents, } from './createInternalComponents.js';
24
+ import { registerModelsFromSchema } from './modelRegistration.js';
25
+ import { deriveConfigFromSchema } from './schemaConfig.js';
26
+ import { createDefaultMutationExecutor } from './wsMutationExecutor.js';
27
+ /**
28
+ * The private handoff slot on the humans surface. Symbol-keyed on purpose:
29
+ * surface members merge onto the client by string key, and the cluster is
30
+ * construction machinery, not client API.
31
+ */
32
+ export const kStoreCluster = Symbol('ablo.humans.storeCluster');
33
+ /**
34
+ * The contract types the connection as the core `WsTransport`; on the
35
+ * reactive path the factory constructs the materialising subclass with the
36
+ * default collaboration vocabulary, and this guard is where that knowledge
37
+ * becomes a type. A connection that is not the subclass means the cluster
38
+ * cannot be built here.
39
+ */
40
+ function isReactiveSocket(transport) {
41
+ return transport instanceof SyncWebSocket;
42
+ }
43
+ /**
44
+ * Builds the cluster from what the context carries, or returns `null` when
45
+ * it does not carry enough (no connection, no resolved url, no credential
46
+ * source, or no schema on the options bag) — the same tolerance the
47
+ * presence stream established: `init` constructs what the context can
48
+ * support and no more. The reactive factory always supplies all four and
49
+ * treats a missing cluster as a configuration fault.
50
+ */
51
+ export function buildStoreCluster(context) {
52
+ const options = context.options;
53
+ const { url, auth, transport, logger } = context;
54
+ if (!options?.schema || !url || !auth || !isReactiveSocket(transport)) {
55
+ return null;
56
+ }
57
+ const schema = options.schema;
58
+ // Config derives from the schema; caller-supplied overrides layer on top,
59
+ // caller winning per key.
60
+ const config = {
61
+ ...deriveConfigFromSchema(schema),
62
+ ...options.configOverrides,
63
+ };
64
+ // The default executor sends `{ type: 'commit', ... }` over the context's
65
+ // connection; before it opens, sends reject with the diagnosed not-ready
66
+ // error and the MutationQueue owns the retry. A caller-supplied executor
67
+ // still wins (test mocks, alternative transports).
68
+ const executor = options.mutationExecutor ?? createDefaultMutationExecutor(() => transport);
69
+ // This client's runtime — the instance the whole graph is constructed
70
+ // with, so two clients in one process never read each other's logger,
71
+ // config, or executor. `getModelMetadata` closes over this client's own
72
+ // registry (assigned once the graph exists below), never the
73
+ // active-registry global.
74
+ let registryForMetadata;
75
+ const runtime = {
76
+ logger,
77
+ observability: options.observability ?? noopObservability,
78
+ analytics: options.analytics ?? noopAnalytics,
79
+ sessionErrorDetector: options.sessionErrorDetector ?? defaultSessionErrorDetector,
80
+ onlineStatus: options.onlineStatus ??
81
+ (shouldUseInMemoryPersistence(options) ? alwaysOnline() : browserOnlineStatus),
82
+ config,
83
+ mutationExecutor: executor,
84
+ getModelMetadata: (name) => registryForMetadata?.getMetadata(name),
85
+ };
86
+ // The module-global bridge: code not yet constructed with an instance
87
+ // (model instances, the react hooks, the socket's context ports) still
88
+ // reads it. Last-writer-wins there, exactly as before the runtime became
89
+ // per-client. Retired reference by reference (docs/plans/package-split.md).
90
+ initRuntime(runtime);
91
+ const components = createInternalComponents({
92
+ schema,
93
+ url,
94
+ options,
95
+ auth,
96
+ runtime,
97
+ });
98
+ registryForMetadata = components.modelRegistry;
99
+ registerModelsFromSchema(schema, components.modelRegistry);
100
+ const store = new BaseSyncedStore({
101
+ syncClient: components.syncClient,
102
+ database: components.database,
103
+ objectPool: components.objectPool,
104
+ modelRegistry: components.modelRegistry,
105
+ syncWebSocket: transport,
106
+ schema,
107
+ url,
108
+ auth,
109
+ runtime,
110
+ // The resolved list, so the store's delta pipeline dispatches the
111
+ // declared stage handlers — including this plugin's own `apply`.
112
+ stagePlugins: context.plugins ?? [],
113
+ },
114
+ // Collaboration vocabulary is the application's: the SDK subscribes to
115
+ // the event types the caller declares and to nothing by default.
116
+ { collaborationEvents: options.collaborationEvents ?? [] });
117
+ return { runtime, components, store };
118
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The materialiser's lifecycle: the first credential mint, identity
3
+ * resolution, the credential-refresh machinery, and the idempotent `ready()`
4
+ * that drives the store's initialize generator to completion. Extracted from
5
+ * the composition root along the humans() seam (docs/plans/package-split.md):
6
+ * the engine wires it with the prelude's credential slice and consumes
7
+ * `ready`; resolved identity flows back through one callback, so the
8
+ * engine-side state it seeds (the self locals, the streams) stays with the
9
+ * engine.
10
+ */
11
+ import type { SchemaRecord, Schema } from '../transaction/schema/schema.js';
12
+ import type { ParticipantKind } from '../transaction/types/participant.js';
13
+ import type { Logger } from '../transaction/logger.js';
14
+ import type { AuthCredentialSource } from '../transaction/auth/credentialSource.js';
15
+ import type { ApiKeySetter } from '../transaction/auth/apiKey.js';
16
+ import type { AbloError } from '../transaction/errors.js';
17
+ import type { InternalAbloOptions } from './options.js';
18
+ import type { StoreCluster } from './storeCluster.js';
19
+ /** What identity resolution settled, handed back for the engine to seed. */
20
+ export interface IdentitySeed {
21
+ readonly userId: string;
22
+ readonly participantKind: ParticipantKind;
23
+ /** The resolved account scope; null until known. */
24
+ readonly accountScope: string | null;
25
+ readonly syncGroups: readonly string[];
26
+ }
27
+ export interface StoreLifecycle {
28
+ /**
29
+ * Drives initialization to completion: first mint, identity resolution,
30
+ * seeding, then the store's initialize generator. Idempotent — the first
31
+ * call starts it, later calls share the promise; a failed attempt clears
32
+ * the memo so retry triggers can re-bootstrap.
33
+ */
34
+ ready(): Promise<void>;
35
+ /** Stops the credential refresh scheduler, if identity resolution installed one. */
36
+ dispose(): void;
37
+ }
38
+ export interface StoreLifecycleDeps<S extends SchemaRecord> {
39
+ readonly cluster: StoreCluster;
40
+ readonly schema: Schema<S>;
41
+ readonly internalOptions: InternalAbloOptions<S>;
42
+ readonly authCredentials: AuthCredentialSource;
43
+ readonly credentialResolver: (() => Promise<string | null>) | null;
44
+ readonly configuredApiKey: string | ApiKeySetter | null;
45
+ readonly configuredAuthToken: string | null;
46
+ readonly url: string;
47
+ readonly kind: ParticipantKind;
48
+ readonly logger: Logger;
49
+ /** The engine's up-front options validation; a present error makes `ready()` reject with it. */
50
+ readonly validationError: AbloError | null;
51
+ /** Seeds resolved identity into the engine's own state: the self locals and the streams. */
52
+ readonly onIdentityResolved: (seed: IdentitySeed) => void;
53
+ }
54
+ /**
55
+ * Wires the credential machinery onto the cluster and returns the `ready`
56
+ * the client exposes. Wiring happens now — the refresh lifecycle, the
57
+ * lazy-query auth recovery, the executor binding, and the opt-in auto-start;
58
+ * the first mint waits for `ready()` so the first connection carries a
59
+ * token.
60
+ */
61
+ export declare function startStoreLifecycle<S extends SchemaRecord>(deps: StoreLifecycleDeps<S>): StoreLifecycle;
@@ -0,0 +1,231 @@
1
+ /**
2
+ * The materialiser's lifecycle: the first credential mint, identity
3
+ * resolution, the credential-refresh machinery, and the idempotent `ready()`
4
+ * that drives the store's initialize generator to completion. Extracted from
5
+ * the composition root along the humans() seam (docs/plans/package-split.md):
6
+ * the engine wires it with the prelude's credential slice and consumes
7
+ * `ready`; resolved identity flows back through one callback, so the
8
+ * engine-side state it seeds (the self locals, the streams) stays with the
9
+ * engine.
10
+ */
11
+ import { resolveParticipantIdentity } from '../transaction/auth/identity.js';
12
+ import { AbloAuthenticationError, AbloConnectionError, toAbloError, } from '../transaction/errors.js';
13
+ /**
14
+ * Wires the credential machinery onto the cluster and returns the `ready`
15
+ * the client exposes. Wiring happens now — the refresh lifecycle, the
16
+ * lazy-query auth recovery, the executor binding, and the opt-in auto-start;
17
+ * the first mint waits for `ready()` so the first connection carries a
18
+ * token.
19
+ */
20
+ export function startStoreLifecycle(deps) {
21
+ const { cluster, schema, internalOptions, authCredentials, credentialResolver, configuredApiKey, configuredAuthToken, url, kind, logger, validationError, onIdentityResolved, } = deps;
22
+ const { store, components, runtime } = cluster;
23
+ const { bootstrapHelper, hydration, syncClient } = components;
24
+ // Hand the credential lifecycle to the client (refresher + proactive refresh
25
+ // timer + wake/online/focus re-mint). Installed once here so refresh works for
26
+ // any consumer of `Ablo({ auth })`, not only those who render `<AbloProvider>`.
27
+ // The first mint happens in `ready()` so the first connection carries a token.
28
+ //
29
+ // Long-lived server clients also get the pre-roll timer on windowless hosts
30
+ // (`proactiveInNode`): their socket must renew its `rk_` or `ek_` before the
31
+ // server's keepalive reaper closes it (4001 `credential_expired`). Two signals
32
+ // qualify — an agent or system participant, and an absolute endpoint-string
33
+ // `apiKey` (a relative one can't be fetched in Node, so an absolute URL is
34
+ // unambiguously a deliberate server client). User-kind clients in Node (an
35
+ // SSR/RSC module evaluating scaffolded browser code) stay reactive-only.
36
+ if (credentialResolver) {
37
+ const rawEndpoint = internalOptions.authEndpoint ?? internalOptions.apiKey;
38
+ const absoluteEndpoint = typeof rawEndpoint === 'string' && /^https?:\/\//i.test(rawEndpoint);
39
+ store.startCredentialLifecycle(credentialResolver, {
40
+ /* eslint-disable @typescript-eslint/no-deprecated -- `kind` gates the self-hosted proactive pre-roll; hosted path derives it from the apiKey scope */
41
+ proactiveInNode: internalOptions.kind === 'agent' ||
42
+ internalOptions.kind === 'system' ||
43
+ absoluteEndpoint,
44
+ /* eslint-enable @typescript-eslint/no-deprecated */
45
+ });
46
+ }
47
+ // Put the lazy-query lane on the same auth-recovery path as the WebSocket probe
48
+ // and the proactive pre-roll: a 401 on `/sync/query` re-mints via the store's
49
+ // single-flight lifecycle and replays once, instead of silently returning empty
50
+ // rows against an expired `ek_` until the next proactive tick. Late-bound
51
+ // because the coordinator is constructed before the store exists.
52
+ hydration.setCredentialRecovery((recovery) => store.recoverFromAuthRejection(recovery));
53
+ // Bind this client's executor to its MutationQueue explicitly. The queue
54
+ // already resolves it through this client's own `runtime`, so this is the
55
+ // same executor either way; the explicit binding also covers a queue that
56
+ // was constructed without the instance runtime (subclass and test paths).
57
+ syncClient.getMutationQueue().setMutationExecutor(runtime.mutationExecutor);
58
+ // Status is tracked in store.syncStatus (MobX observable) — the single
59
+ // source of truth. No duplicate closure variables.
60
+ let _readyPromise = null;
61
+ let _refreshScheduler = null;
62
+ async function ready() {
63
+ if (_readyPromise)
64
+ return _readyPromise;
65
+ if (validationError) {
66
+ _readyPromise = Promise.reject(validationError);
67
+ return _readyPromise;
68
+ }
69
+ _readyPromise = (async () => {
70
+ try {
71
+ // Mint the first access credential before we connect, so the initial
72
+ // WebSocket upgrade and bootstrap carry a valid bearer (no tokenless first
73
+ // connect that has to self-heal). Only when a refreshing resolver is wired
74
+ // and no static credential is already present. Follows the `apiKey`
75
+ // resolver contract: `null` means the login is gone (terminal — fail ready
76
+ // so the app shows sign-in); a throw means transient (rethrown; autoStart
77
+ // swallows it and the lifecycle's online/wake triggers retry).
78
+ if (credentialResolver && !authCredentials.getAuthToken()) {
79
+ const token = await credentialResolver();
80
+ if (!token) {
81
+ throw new AbloAuthenticationError('Auth resolver returned null before connect — the user is not signed in.', { code: 'auth_no_credentials' });
82
+ }
83
+ authCredentials.setAuthToken(token);
84
+ }
85
+ // Resolve participant identity + scope. Three branches —
86
+ // hosted-cloud apiKey exchange, self-derived from capability
87
+ // token, or legacy explicit options. See `./identity.ts`.
88
+ const resolved = await resolveParticipantIdentity({
89
+ options: internalOptions,
90
+ internalOptions,
91
+ url,
92
+ kind,
93
+ configuredApiKey,
94
+ // Resolve identity against the live token, not the construction-time
95
+ // `configuredAuthToken`. Consumers using a function `apiKey` never pass
96
+ // `authToken` at construction — the lifecycle mints the first `ek_` or
97
+ // `rk_` and calls `setAuthToken()` before `ready()`, which updates the
98
+ // shared credential source. Reading the frozen `configuredAuthToken`
99
+ // here made `/auth/identity` fire with no bearer (returning
100
+ // `no_matching_provider` / `session_expired`) even though the token was
101
+ // present. This reads the shared credential source, like every other
102
+ // transport.
103
+ configuredAuthToken: authCredentials.getAuthToken() ?? configuredAuthToken,
104
+ bootstrapHelper,
105
+ auth: authCredentials,
106
+ logger,
107
+ });
108
+ const { userId, accountScope, teamIds, capabilityToken, syncGroups, participantKind, } = resolved;
109
+ // Fail-loud guard: detect the degenerate "no real sync groups
110
+ // resolved" state before opening the socket. It is the same class of bug as
111
+ // a sensible-looking default that's functionally broken: the
112
+ // SDK ends up subscribing only to the server-side
113
+ // `['default']` fallback, no
114
+ // delta has that tag, live fan-out silently never delivers.
115
+ // For human users (kind:'user') this is almost certainly a
116
+ // misconfiguration upstream — either the caller didn't pass
117
+ // `syncGroups`, or auth resolution didn't derive them, or
118
+ // both. Warn loudly so the next debugging session starts here
119
+ // instead of with "live updates don't work, hard reload fixes
120
+ // it."
121
+ const resolvedSyncGroups = syncGroups ?? [];
122
+ if (participantKind === 'user' &&
123
+ (resolvedSyncGroups.length === 0 ||
124
+ (resolvedSyncGroups.length === 1 && resolvedSyncGroups[0] === 'default'))) {
125
+ // Actionable and not self-healing (no live updates until fixed):
126
+ // kept at warn level for consumers; the low-level diagnostic
127
+ // fields ride the debug log below.
128
+ logger.warn('This client was started without sync groups, so it will not receive ' +
129
+ 'live updates. Pass `syncGroups` (for example ' +
130
+ '`["org:<id>", "user:<id>"]`) or check that your auth provider supplies them.');
131
+ logger.debug('degenerate syncGroups — details', { participantKind, resolvedSyncGroups });
132
+ }
133
+ // Seed the resolved identity into everything that filters or stamps
134
+ // by participant — the engine's self locals and its streams, through
135
+ // the one callback. This runs before the store connects, so no frame
136
+ // is ever filtered against the construction-time guess.
137
+ onIdentityResolved({
138
+ userId,
139
+ participantKind,
140
+ accountScope,
141
+ syncGroups: resolvedSyncGroups,
142
+ });
143
+ if (resolved.refreshScheduler) {
144
+ _refreshScheduler = resolved.refreshScheduler;
145
+ }
146
+ // Drive the generator to completion. Each yielded promise is awaited
147
+ // then fed back — this is standard generator consumption.
148
+ //
149
+ // The store.initialize() generator updates store.syncStatus as it
150
+ // progresses (syncing → idle on success, error on failure), so the
151
+ // consumer's `sync.syncStatus` observable reflects real-time state.
152
+ // Resolve bootstrap mode: explicit option wins; otherwise
153
+ // agents default to 'none' (transactional participant — see
154
+ // option doc) and everyone else defaults to 'full'.
155
+ const resolvedBootstrapMode = internalOptions.bootstrapMode ?? (participantKind === 'agent' ? 'none' : 'full');
156
+ const gen = store.initialize({
157
+ userId,
158
+ organizationId: accountScope,
159
+ teamIds,
160
+ kind: participantKind,
161
+ capabilityToken,
162
+ syncGroups,
163
+ bootstrapMode: resolvedBootstrapMode,
164
+ });
165
+ let current = gen.next();
166
+ while (!current.done) {
167
+ const yielded = current.value;
168
+ const settled = yielded instanceof Promise ? await yielded : yielded;
169
+ current = gen.next(settled);
170
+ }
171
+ const result = current.value;
172
+ if (!result.success) {
173
+ throw result.error
174
+ ? toAbloError(result.error)
175
+ : new AbloConnectionError('Sync engine initialization failed', {
176
+ code: 'bootstrap_fetch_timeout',
177
+ });
178
+ }
179
+ logger.info('Sync engine ready', { models: Object.keys(schema.models).length });
180
+ }
181
+ catch (err) {
182
+ // Coerce so the rejection a consumer awaiting `ready()` catches is
183
+ // always an AbloError — connection setup is held to the same
184
+ // never-leak-untagged contract as the model operations.
185
+ const error = toAbloError(err);
186
+ // Make sure syncStatus reflects the failure for observer() components
187
+ store.syncStatus.state = 'error';
188
+ store.syncStatus.error = error;
189
+ // Log the typed envelope (type + code + status), not just the bare
190
+ // message — so the console line names it as an Ablo error and carries
191
+ // the code (e.g. AbloAuthenticationError/identity_resolve_failed on a
192
+ // 401) instead of reading like an untagged failure.
193
+ logger.error('Sync engine failed to initialize', {
194
+ type: error.type,
195
+ code: error.code,
196
+ httpStatus: error.httpStatus,
197
+ error: error.message,
198
+ });
199
+ // Clear the memo so a future `ready()` re-attempts bootstrap instead of
200
+ // replaying this rejection forever. Bootstrap failures here are transient
201
+ // by nature — offline, an IndexedDB open timeout, a bootstrap fetch
202
+ // hiccup — and the early `if (_readyPromise) return _readyPromise` guard
203
+ // would otherwise hand every later caller this same dead promise, bricking
204
+ // the engine until a full page reload. Nulling it lets the provider's
205
+ // online/wake/retry triggers drive a clean re-bootstrap. (The terminal
206
+ // `validationError` branch above intentionally stays cached — config
207
+ // can't change without recreating the engine.)
208
+ _readyPromise = null;
209
+ throw error;
210
+ }
211
+ })();
212
+ return _readyPromise;
213
+ }
214
+ // Optional auto-start for convenience. Opt-in because silent background
215
+ // init has historically been the #1 source of "why isn't my data loading"
216
+ // bug reports. Explicit `await sync.ready()` is the default — errors
217
+ // surface immediately instead of being swallowed.
218
+ if (!validationError && internalOptions.autoStart) {
219
+ void ready().catch(() => {
220
+ // Error is captured in store.syncStatus; consumers should check
221
+ // `sync.syncStatus.state === 'error'` to detect failures.
222
+ });
223
+ }
224
+ return {
225
+ ready,
226
+ dispose() {
227
+ _refreshScheduler?.dispose();
228
+ _refreshScheduler = null;
229
+ },
230
+ };
231
+ }
package/dist/context.d.ts CHANGED
@@ -19,6 +19,19 @@ export declare function initRuntime(context: RuntimeContext): void;
19
19
  * so SDK files can import at module load time without crashing.
20
20
  */
21
21
  export declare function getContext(): RuntimeContext;
22
+ /**
23
+ * The module-global runtime as an instance-shaped value — the bridge for code
24
+ * that is not yet constructed with its client's own `RuntimeContext`.
25
+ *
26
+ * Every member reads {@link getContext} at call time, so a context installed
27
+ * after construction is still picked up (tests construct first and
28
+ * `initRuntime` later). Classes take `runtime` as a constructor option with
29
+ * this as the default; a client that threads its own instance is isolated
30
+ * from other clients in the same process, a construction that doesn't is
31
+ * last-writer-wins exactly as before. Retired reference by reference as
32
+ * construction moves behind `humans()` (docs/plans/package-split.md).
33
+ */
34
+ export declare const globalRuntime: RuntimeContext;
22
35
  /**
23
36
  * Check if the sync engine has been initialized.
24
37
  */
package/dist/context.js CHANGED
@@ -27,6 +27,29 @@ export function getContext() {
27
27
  }
28
28
  return _context;
29
29
  }
30
+ /**
31
+ * The module-global runtime as an instance-shaped value — the bridge for code
32
+ * that is not yet constructed with its client's own `RuntimeContext`.
33
+ *
34
+ * Every member reads {@link getContext} at call time, so a context installed
35
+ * after construction is still picked up (tests construct first and
36
+ * `initRuntime` later). Classes take `runtime` as a constructor option with
37
+ * this as the default; a client that threads its own instance is isolated
38
+ * from other clients in the same process, a construction that doesn't is
39
+ * last-writer-wins exactly as before. Retired reference by reference as
40
+ * construction moves behind `humans()` (docs/plans/package-split.md).
41
+ */
42
+ export const globalRuntime = {
43
+ get logger() { return getContext().logger; },
44
+ get observability() { return getContext().observability; },
45
+ get analytics() { return getContext().analytics; },
46
+ get sessionErrorDetector() { return getContext().sessionErrorDetector; },
47
+ get onlineStatus() { return getContext().onlineStatus; },
48
+ get modelDebugLogger() { return getContext().modelDebugLogger; },
49
+ get mutationExecutor() { return getContext().mutationExecutor; },
50
+ get config() { return getContext().config; },
51
+ getModelMetadata: (name) => getContext().getModelMetadata(name),
52
+ };
30
53
  /**
31
54
  * Check if the sync engine has been initialized.
32
55
  */
@@ -22,7 +22,7 @@ export { LazyReferenceCollection, type LazyCollectionOptions, } from '../LazyRef
22
22
  export { ModelRegistry, getActiveRegistry, } from '../ModelRegistry.js';
23
23
  export { postQuery, type PostQueryOptions } from '../query/client.js';
24
24
  export { computeFKDepthPriority } from '../client/schemaConfig.js';
25
- export type { SyncLogger, SyncObservabilityProvider, MutationExecutor, SessionErrorDetector, OnlineStatusProvider, CommitResult, MutationOperation, } from '../interfaces/index.js';
25
+ export type { SyncLogger, SyncObservabilityProvider, MutationExecutor, SessionErrorDetector, OnlineStatusProvider, CommitResult, MutationOperation, MutationOptions, } from '../interfaces/index.js';
26
26
  export { SyncWebSocket, type SyncDelta, type SyncWebSocketOptions, } from '../sync/SyncWebSocket.js';
27
27
  export { BootstrapFetcher } from '../sync/BootstrapFetcher.js';
28
28
  /** @deprecated `BootstrapHelper` was renamed to {@link BootstrapFetcher}. This alias keeps
@@ -137,6 +137,9 @@ async function readDocsDirectory(dir, kind) {
137
137
  }
138
138
  return entries;
139
139
  }
140
+ /** The H1 a page opens with, and the blockquote paragraph that follows it. */
141
+ const H1_LINE = /^#[ \t]+(.+?)[ \t]*$/m;
142
+ const PROMISE_BLOCKQUOTE = /^\s*\n((?:>.*\n)+)/;
140
143
  /**
141
144
  * Pull a page's title and promise line out of its markdown.
142
145
  *
@@ -145,10 +148,10 @@ async function readDocsDirectory(dir, kind) {
145
148
  * catalog never invents prose.
146
149
  */
147
150
  export function parseDocHeader(body) {
148
- const heading = body.match(/^#[ \t]+(.+?)[ \t]*$/m);
151
+ const heading = H1_LINE.exec(body);
149
152
  const title = heading?.[1] ?? null;
150
- const afterHeading = heading?.index === undefined ? body : body.slice(heading.index + heading[0].length);
151
- const promise = afterHeading.match(/^\s*\n((?:>.*\n)+)/);
153
+ const afterHeading = heading === null ? body : body.slice(heading.index + heading[0].length);
154
+ const promise = PROMISE_BLOCKQUOTE.exec(afterHeading);
152
155
  const description = promise?.[1]
153
156
  ?.split('\n')
154
157
  .filter(Boolean)
package/dist/index.d.ts CHANGED
@@ -26,7 +26,6 @@
26
26
  * Related surfaces live on their own import subpaths:
27
27
  * @abloatai/ablo/schema — defineSchema, model, z (Zod)
28
28
  * @abloatai/ablo/react — <AbloProvider>, useQuery, useMutate
29
- * @abloatai/ablo/testing — test harnesses and fixtures
30
29
  *
31
30
  * Reads come in two flavors, distinguished by where the data is fetched from.
32
31
  * `ablo.<model>.retrieve({ id })` and `.list({ where })` are asynchronous reads
@@ -97,12 +96,15 @@ export type { WriteOptionsInput } from './transaction/resources/writeOptionsSche
97
96
  export type { OnStaleMode } from './transaction/coordination/schema.js';
98
97
  export type { WriteOptions, MutationOptions } from './interfaces/index.js';
99
98
  export { staleNotificationSchema, readDependencySchema } from './transaction/coordination/schema.js';
99
+ export { part } from './transaction/coordination/schema.js';
100
+ export type { ClaimPart } from './transaction/coordination/schema.js';
101
+ export type { ClaimField } from './transaction/resources/modelOperations.js';
100
102
  export type { StaleNotification, ReadDependency } from './transaction/coordination/schema.js';
101
103
  export { ClaimLog, formatClaim, formatConflict } from './coordination/ClaimLog.js';
102
104
  export type { ClaimLogEntry } from './coordination/ClaimLog.js';
103
105
  export type { ClaimEvent, ConflictEvent, SyncObservabilityProvider, } from './interfaces/index.js';
104
106
  export { noopObservability } from './RuntimeContext.js';
105
- export { IDBOpenTimeoutError, isStorageOpenTimeout } from './core/openIDBWithTimeout.js';
107
+ export { IDBOpenTimeoutError, isStorageOpenTimeout } from './stores/openIDBWithTimeout.js';
106
108
  export type { CommitLatencySample } from './transactions/mutations/commitLatency.js';
107
109
  export { PUBLIC_MODEL_VERBS, PUBLIC_LIST_OPTION_KEYS, PUBLIC_ABLO_OPTION_KEYS, } from './surface.js';
108
110
  export type { ModelVerb, ListOptionKey, AbloOptionKey } from './surface.js';
package/dist/index.js CHANGED
@@ -26,7 +26,6 @@
26
26
  * Related surfaces live on their own import subpaths:
27
27
  * @abloatai/ablo/schema — defineSchema, model, z (Zod)
28
28
  * @abloatai/ablo/react — <AbloProvider>, useQuery, useMutate
29
- * @abloatai/ablo/testing — test harnesses and fixtures
30
29
  *
31
30
  * Reads come in two flavors, distinguished by where the data is fetched from.
32
31
  * `ablo.<model>.retrieve({ id })` and `.list({ where })` are asynchronous reads
@@ -122,6 +121,9 @@ export { writeOptionsSchema, onStaleModeSchema, assertWriteOptions, } from './tr
122
121
  // returns this notification so the caller can reconcile against the current
123
122
  // value and retry rather than discard its work.
124
123
  export { staleNotificationSchema, readDependencySchema } from './transaction/coordination/schema.js';
124
+ // The claim-part marker: schema field names typecheck plainly on a claim
125
+ // target; an app-defined part (a cell, a section) is named through `part()`.
126
+ export { part } from './transaction/coordination/schema.js';
125
127
  // Collects claim events and stale-write collisions into an ordered list you can
126
128
  // print to inspect coordination, or read through `collisions()` to assert on in
127
129
  // tests. Pass `new ClaimLog()` as `Ablo({ observability })`.
@@ -132,7 +134,7 @@ export { noopObservability } from './RuntimeContext.js';
132
134
  // Detects a stuck local store: use these to recognize when the browser's
133
135
  // IndexedDB backing store fails to open in time, so your app can show a
134
136
  // recovery screen instead of hanging.
135
- export { IDBOpenTimeoutError, isStorageOpenTimeout } from './core/openIDBWithTimeout.js';
137
+ export { IDBOpenTimeoutError, isStorageOpenTimeout } from './stores/openIDBWithTimeout.js';
136
138
  // A machine-readable manifest of the SDK's public verb and option names, bound
137
139
  // at compile time to the real types so the lists can never name a method or
138
140
  // option the API doesn't have. Useful for generating documentation or tooling
@@ -14,6 +14,7 @@
14
14
  import type { QueryBatch, QueryBatchResult } from './types.js';
15
15
  import { type RecoveryClass } from '../transaction/errorCodes.js';
16
16
  import { type AuthTokenGetter } from '../transaction/auth/credentialSource.js';
17
+ import type { RuntimeContext } from '../RuntimeContext.js';
17
18
  export interface PostQueryOptions {
18
19
  /**
19
20
  * Full base URL of the sync server including the `/api` prefix.
@@ -23,6 +24,8 @@ export interface PostQueryOptions {
23
24
  baseUrl: string;
24
25
  /** Timeout in ms for the fetch request. Default: 30000. */
25
26
  fetchTimeout?: number;
27
+ /** The owning client's runtime. Defaults to the module-global bridge. */
28
+ runtime?: RuntimeContext;
26
29
  /**
27
30
  * Live bearer credential getter. Preferred over `capabilityToken` because it
28
31
  * is read per request, so token refreshes propagate without reconstructing
@@ -15,7 +15,7 @@ import { z } from 'zod';
15
15
  import { translateHttpError } from '../transaction/errors.js';
16
16
  import { classifyRecovery } from '../transaction/errorCodes.js';
17
17
  import { withAuthHeaders } from '../transaction/auth/credentialSource.js';
18
- import { getContext } from '../context.js';
18
+ import { globalRuntime } from '../context.js';
19
19
  // ── Response validation ─────────────────────────────────────────────────
20
20
  //
21
21
  // Each result slot is an array of rows, or an object for a bundled response.
@@ -50,6 +50,7 @@ const QueryBatchResultSchema = z
50
50
  export async function postQuery(options, batch) {
51
51
  const url = `${options.baseUrl}/sync/query`;
52
52
  const timeout = options.fetchTimeout ?? 30_000;
53
+ const runtime = options.runtime ?? globalRuntime;
53
54
  // At most two attempts: the original request, plus one replay after a
54
55
  // successful credential recovery (see `recoverCredential`). A second auth
55
56
  // rejection falls through to the log-and-empty path, so a wedged credential
@@ -97,7 +98,7 @@ export async function postQuery(options, batch) {
97
98
  : 'access_credential_expiry';
98
99
  const outcome = await options.recoverCredential(recovery);
99
100
  if (outcome === 'retry') {
100
- getContext().logger.debug('[postQuery] credential recovered — replaying query once', {
101
+ runtime.logger.debug('[postQuery] credential recovered — replaying query once', {
101
102
  code: err.code ?? response.status,
102
103
  });
103
104
  continue;
@@ -109,8 +110,8 @@ export async function postQuery(options, batch) {
109
110
  // `debug` line. The read stays empty until the underlying cause, such
110
111
  // as auth or network, is resolved.
111
112
  const models = batch.queries.map((q) => q.model).join(', ');
112
- getContext().logger.warn(`Could not load ${models} — ${err.message} (code: ${err.code ?? response.status}). No results were returned.`);
113
- getContext().logger.debug('[postQuery.error] query http failure', {
113
+ runtime.logger.warn(`Could not load ${models} — ${err.message} (code: ${err.code ?? response.status}). No results were returned.`);
114
+ runtime.logger.debug('[postQuery.error] query http failure', {
114
115
  type: err.type,
115
116
  code: err.code ?? response.status,
116
117
  models,
@@ -123,7 +124,7 @@ export async function postQuery(options, batch) {
123
124
  if (!parsed.success) {
124
125
  // A malformed server response isn't something the consumer can act on
125
126
  // (server/protocol issue) → debug, gated like everything else.
126
- getContext().logger.debug('[postQuery.error] malformed response', {
127
+ runtime.logger.debug('[postQuery.error] malformed response', {
127
128
  issues: parsed.error.issues,
128
129
  });
129
130
  return { results: batch.queries.map(() => []) };
@@ -1,7 +1,7 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import type { SchemaRecord } from '../transaction/schema/schema.js';
3
3
  import { Ablo } from '../client/Ablo.js';
4
- import type { Claim, Peer } from '../transaction/types/streams.js';
4
+ import type { Claim, Duration, Peer } from '../transaction/types/streams.js';
5
5
  import type { EngineParticipant, ParticipantScope, ParticipantStatus } from '../sync/participants.js';
6
6
  import { type SyncStoreContract } from './context.js';
7
7
  /**
@@ -116,6 +116,18 @@ export type { EngineParticipant, ParticipantScope, ParticipantStatus };
116
116
  */
117
117
  export interface UseJoinOptions {
118
118
  readonly scope?: ParticipantScope;
119
+ /**
120
+ * Lease TTL for the participant claim, as a compact duration (`'5m'`) or a
121
+ * number of seconds. The same dial and the same spelling as
122
+ * `ablo.<model>.join(ids, { ttl })` and every other lease in the SDK.
123
+ */
124
+ readonly ttl?: Duration;
125
+ /**
126
+ * @deprecated Use `ttl`. Removed in 0.37.0.
127
+ *
128
+ * The same rename as on `ParticipantJoinOptions`: one lease, and the seconds
129
+ * spelling was the one that misled, because it accepted duration strings too.
130
+ */
119
131
  readonly ttlSeconds?: number | string | null;
120
132
  /** Tear down + don't re-join while true. */
121
133
  readonly paused?: boolean;