@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.
- package/CHANGELOG.md +713 -629
- package/README.md +56 -519
- package/bin/ablo.cjs +39 -0
- package/dist/BaseSyncedStore.d.ts +24 -4
- package/dist/BaseSyncedStore.js +53 -37
- package/dist/Database.d.ts +8 -20
- package/dist/Database.js +61 -59
- package/dist/InstanceCache.d.ts +6 -2
- package/dist/InstanceCache.js +18 -16
- package/dist/Model.d.ts +17 -7
- package/dist/Model.js +17 -7
- package/dist/ModelRegistry.d.ts +4 -0
- package/dist/ModelRegistry.js +17 -15
- package/dist/NetworkMonitor.d.ts +3 -1
- package/dist/NetworkMonitor.js +7 -5
- package/dist/SyncClient.d.ts +5 -15
- package/dist/SyncClient.js +60 -57
- package/dist/client/Ablo.js +30 -19
- package/dist/client/createInternalComponents.d.ts +4 -0
- package/dist/client/createInternalComponents.js +8 -2
- package/dist/client/createModelProxy.d.ts +21 -1
- package/dist/client/createModelProxy.js +123 -57
- package/dist/client/humans.d.ts +30 -9
- package/dist/client/humans.js +45 -19
- package/dist/client/reactiveEngine.d.ts +16 -11
- package/dist/client/reactiveEngine.js +113 -335
- package/dist/client/storeCluster.d.ts +47 -0
- package/dist/client/storeCluster.js +118 -0
- package/dist/client/storeLifecycle.d.ts +61 -0
- package/dist/client/storeLifecycle.js +231 -0
- package/dist/context.d.ts +13 -0
- package/dist/context.js +23 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/docs/catalog.js +6 -3
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/query/client.d.ts +3 -0
- package/dist/query/client.js +6 -5
- package/dist/react/AbloProvider.d.ts +13 -1
- package/dist/react/AbloProvider.js +5 -2
- package/dist/react/context.d.ts +2 -2
- package/dist/react/createAbloReact.d.ts +56 -0
- package/dist/react/createAbloReact.js +51 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +3 -0
- package/dist/react/useAblo.d.ts +9 -2
- package/dist/react/useAblo.js +25 -7
- package/dist/schema/coordination.js +5 -1
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +4 -0
- package/dist/schema/select.js +3 -0
- package/dist/schema/serialize.js +3 -0
- package/dist/source/adapter.d.ts +7 -5
- package/dist/source/adapter.js +7 -5
- package/dist/source/index.d.ts +1 -1
- package/dist/source/index.js +1 -1
- package/dist/{core/storeContract.d.ts → storeContract.d.ts} +5 -5
- package/dist/{core → stores}/DatabaseManager.d.ts +3 -1
- package/dist/{core → stores}/DatabaseManager.js +13 -12
- package/dist/{core → stores}/StoreManager.d.ts +5 -3
- package/dist/{core → stores}/StoreManager.js +24 -22
- package/dist/stores/SyncActionStore.d.ts +3 -1
- package/dist/stores/SyncActionStore.js +9 -7
- package/dist/sync/BootstrapFetcher.d.ts +4 -0
- package/dist/sync/BootstrapFetcher.js +28 -26
- package/dist/sync/OnDemandLoader.d.ts +3 -0
- package/dist/sync/OnDemandLoader.js +1 -0
- package/dist/sync/bootstrapApply.d.ts +3 -0
- package/dist/sync/bootstrapApply.js +2 -2
- package/dist/sync/deltaPipeline.d.ts +13 -12
- package/dist/sync/deltaPipeline.js +21 -4
- package/dist/sync/groupChange.d.ts +3 -0
- package/dist/sync/groupChange.js +16 -14
- package/dist/sync/participants.d.ts +19 -2
- package/dist/sync/participants.js +3 -1
- package/dist/sync/schemas.d.ts +2 -1
- package/dist/sync/schemas.js +3 -3
- package/dist/syncLog/contract.d.ts +20 -0
- package/dist/syncLog/contract.js +19 -0
- package/dist/syncLog/index.d.ts +1 -0
- package/dist/syncLog/index.js +1 -0
- package/dist/transaction/auth/capability.d.ts +35 -0
- package/dist/transaction/auth/capability.js +25 -0
- package/dist/transaction/coordination/awaitClaimGrant.d.ts +7 -0
- package/dist/transaction/coordination/awaitClaimGrant.js +12 -0
- package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +34 -0
- package/dist/transaction/coordination/claimHeartbeatLoop.js +20 -0
- package/dist/transaction/coordination/index.d.ts +4 -4
- package/dist/transaction/coordination/index.js +4 -3
- package/dist/transaction/coordination/locator.d.ts +23 -2
- package/dist/transaction/coordination/locator.js +22 -2
- package/dist/transaction/coordination/schema.d.ts +125 -62
- package/dist/transaction/coordination/schema.js +228 -64
- package/dist/transaction/coordination/targetConflict.js +32 -28
- package/dist/transaction/errorCodes.d.ts +2 -2
- package/dist/transaction/errorCodes.js +13 -9
- package/dist/transaction/plugin.d.ts +95 -2
- package/dist/transaction/plugin.js +21 -2
- package/dist/transaction/resources/httpResources.d.ts +57 -2
- package/dist/transaction/resources/modelOperations.d.ts +148 -40
- package/dist/transaction/resources/where.d.ts +16 -0
- package/dist/transaction/resources/where.js +45 -0
- package/dist/transaction/schema/field.d.ts +12 -18
- package/dist/transaction/schema/fieldRef.d.ts +38 -0
- package/dist/transaction/schema/fieldRef.js +11 -0
- package/dist/transaction/schema/openapi.d.ts +16 -15
- package/dist/transaction/schema/openapi.js +186 -25
- package/dist/transaction/schema/relation.d.ts +7 -2
- package/dist/transaction/schema/schema.d.ts +27 -0
- package/dist/transaction/schema/schema.js +20 -0
- package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +1 -1
- package/dist/transaction/transactions/settlement/pendingWrite.d.ts +1 -1
- package/dist/transaction/transport/httpClient.d.ts +9 -1
- package/dist/transaction/transport/httpClient.js +1 -0
- package/dist/transaction/transport/httpTransport.js +156 -44
- package/dist/transaction/transport/wsTransport.d.ts +2 -4
- package/dist/transaction/transport/wsTransport.js +16 -10
- package/dist/transaction/types/streams.d.ts +10 -0
- package/dist/transaction/utils/duration.d.ts +25 -0
- package/dist/transaction/utils/duration.js +32 -0
- package/dist/transaction/wire/accountResponses.d.ts +69 -0
- package/dist/transaction/wire/accountResponses.js +36 -1
- package/dist/transaction/wire/auth.d.ts +9 -2
- package/dist/transaction/wire/auth.js +7 -1
- package/dist/transaction/wire/claims.d.ts +164 -97
- package/dist/transaction/wire/claims.js +126 -28
- package/dist/transaction/wire/commit.d.ts +1 -1
- package/dist/transaction/wire/feedEvent.d.ts +27 -0
- package/dist/transaction/wire/feedEvent.js +27 -1
- package/dist/transaction/wire/frames.d.ts +2 -2
- package/dist/transaction/wire/inboundFrames.d.ts +12 -2
- package/dist/transaction/wire/index.d.ts +10 -6
- package/dist/transaction/wire/index.js +12 -3
- package/dist/transaction/wire/modelMutations.d.ts +31 -0
- package/dist/transaction/wire/modelMutations.js +52 -0
- package/dist/transaction/wire/modelShape.d.ts +78 -0
- package/dist/transaction/wire/modelShape.js +74 -0
- package/dist/transactions/mutations/MutationQueue.d.ts +6 -0
- package/dist/transactions/mutations/MutationQueue.js +55 -45
- package/dist/transactions/mutations/commitPayload.d.ts +3 -2
- package/dist/transactions/mutations/commitPayload.js +5 -5
- package/dist/transactions/mutations/deltaConfirmation.d.ts +4 -0
- package/dist/transactions/mutations/deltaConfirmation.js +10 -8
- package/dist/transactions/mutations/replayValidation.d.ts +2 -1
- package/dist/transactions/mutations/replayValidation.js +3 -2
- package/dist/{core → views}/QueryView.d.ts +1 -1
- package/dist/{core → views}/QueryView.js +1 -1
- package/dist/{core → views}/ViewRegistry.d.ts +1 -1
- package/dist/{core/queryUtils.d.ts → views/incrementalView.d.ts} +6 -6
- package/dist/{core/queryUtils.js → views/incrementalView.js} +6 -6
- package/docs/agents.md +1 -1
- package/docs/api-keys.md +6 -6
- package/docs/api.md +5 -43
- package/docs/audit.md +4 -3
- package/docs/cli.md +11 -11
- package/docs/client-behavior.md +4 -4
- package/docs/concurrency-convention.md +28 -42
- package/docs/coordination.md +235 -83
- package/docs/data-sources.md +4 -4
- package/docs/debugging.md +34 -12
- package/docs/deployment.md +8 -8
- package/docs/examples/scoped-agent.md +3 -3
- package/docs/groups.md +57 -3
- package/docs/guarantees.md +37 -10
- package/docs/how-it-works.md +29 -5
- package/docs/idempotency.md +6 -6
- package/docs/identity.md +22 -23
- package/docs/index.md +8 -8
- package/docs/integration-guide.md +14 -3
- package/docs/mcp.md +7 -7
- package/docs/migration.md +34 -15
- package/docs/projects.md +1 -1
- package/docs/react.md +19 -8
- package/docs/sessions.md +1 -1
- package/docs/webhooks.md +9 -9
- package/llms.txt +4 -4
- package/package.json +13 -20
- package/dist/cli.cjs +0 -288600
- package/dist/testing/fixtures/bootstrap.d.ts +0 -49
- package/dist/testing/fixtures/bootstrap.js +0 -59
- package/dist/testing/fixtures/deltas.d.ts +0 -83
- package/dist/testing/fixtures/deltas.js +0 -136
- package/dist/testing/fixtures/httpResponses.d.ts +0 -70
- package/dist/testing/fixtures/httpResponses.js +0 -90
- package/dist/testing/fixtures/models.d.ts +0 -83
- package/dist/testing/fixtures/models.js +0 -272
- package/dist/testing/helpers/reactWrapper.d.ts +0 -69
- package/dist/testing/helpers/reactWrapper.js +0 -67
- package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
- package/dist/testing/helpers/syncEngineHarness.js +0 -73
- package/dist/testing/helpers/wait.d.ts +0 -30
- package/dist/testing/helpers/wait.js +0 -49
- package/dist/testing/index.d.ts +0 -23
- package/dist/testing/index.js +0 -33
- package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
- package/dist/testing/mocks/FakeDatabase.js +0 -10
- package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
- package/dist/testing/mocks/MockMutationExecutor.js +0 -186
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
- package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
- package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
- package/dist/testing/mocks/MockSyncContext.js +0 -72
- package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
- package/dist/testing/mocks/MockSyncStore.js +0 -171
- package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
- package/dist/testing/mocks/MockWebSocket.js +0 -118
- package/docs/interaction-model.md +0 -99
- /package/dist/{core → query}/QueryProcessor.d.ts +0 -0
- /package/dist/{core → query}/QueryProcessor.js +0 -0
- /package/dist/{core/storeContract.js → storeContract.js} +0 -0
- /package/dist/{core → stores}/openIDBWithTimeout.d.ts +0 -0
- /package/dist/{core → stores}/openIDBWithTimeout.js +0 -0
- /package/dist/{source → transaction}/footprint.d.ts +0 -0
- /package/dist/{source → transaction}/footprint.js +0 -0
- /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
|
*/
|
package/dist/core/index.d.ts
CHANGED
|
@@ -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
|
package/dist/docs/catalog.js
CHANGED
|
@@ -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 =
|
|
151
|
+
const heading = H1_LINE.exec(body);
|
|
149
152
|
const title = heading?.[1] ?? null;
|
|
150
|
-
const afterHeading = heading
|
|
151
|
-
const promise =
|
|
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 './
|
|
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 './
|
|
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
|
package/dist/query/client.d.ts
CHANGED
|
@@ -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
|
package/dist/query/client.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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;
|