@abloatai/ablo 0.25.0 → 0.26.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/AGENTS.md +5 -3
- package/CHANGELOG.md +20 -0
- package/README.md +3 -3
- package/dist/BaseSyncedStore.d.ts +72 -195
- package/dist/BaseSyncedStore.js +240 -625
- package/dist/Database.d.ts +8 -9
- package/dist/Database.js +9 -14
- package/dist/LazyReferenceCollection.js +5 -0
- package/dist/Model.js +6 -8
- package/dist/ModelRegistry.d.ts +25 -6
- package/dist/ModelRegistry.js +12 -6
- package/dist/NetworkMonitor.js +1 -1
- package/dist/ObjectPool.js +9 -3
- package/dist/SyncClient.d.ts +15 -5
- package/dist/SyncClient.js +152 -66
- package/dist/SyncEngineContext.d.ts +1 -3
- package/dist/SyncEngineContext.js +1 -2
- package/dist/agent/Agent.d.ts +21 -8
- package/dist/agent/Agent.js +26 -13
- package/dist/ai-sdk/coordination-context.js +9 -3
- package/dist/ai-sdk/wrap.d.ts +1 -1
- package/dist/ai-sdk/wrap.js +1 -1
- package/dist/auth/credentialPolicy.d.ts +10 -2
- package/dist/auth/credentialSource.d.ts +1 -13
- package/dist/auth/credentialSource.js +6 -13
- package/dist/auth/index.js +3 -3
- package/dist/cli.cjs +328 -262
- package/dist/client/Ablo.d.ts +35 -764
- package/dist/client/Ablo.js +64 -688
- package/dist/client/ApiClient.d.ts +14 -1
- package/dist/client/ApiClient.js +63 -9
- package/dist/client/auth.d.ts +8 -17
- package/dist/client/auth.js +42 -6
- package/dist/client/consoleLogger.d.ts +36 -0
- package/dist/client/consoleLogger.js +45 -0
- package/dist/client/createInternalComponents.js +1 -1
- package/dist/client/createModelProxy.js +10 -6
- package/dist/client/credentialEndpoint.d.ts +63 -0
- package/dist/client/credentialEndpoint.js +87 -0
- package/dist/client/hostedEndpoints.d.ts +24 -0
- package/dist/client/hostedEndpoints.js +24 -0
- package/dist/client/httpClient.d.ts +1 -1
- package/dist/client/identity.js +2 -1
- package/dist/client/modelRegistration.d.ts +14 -0
- package/dist/client/modelRegistration.js +321 -0
- package/dist/client/options.d.ts +396 -0
- package/dist/client/options.js +10 -0
- package/dist/client/resourceTypes.d.ts +344 -0
- package/dist/client/resourceTypes.js +13 -0
- package/dist/client/schemaConfig.d.ts +56 -0
- package/dist/client/schemaConfig.js +188 -0
- package/dist/client/sessionMint.d.ts +1 -1
- package/dist/client/validateAbloOptions.js +1 -1
- package/dist/client/wsMutationExecutor.d.ts +33 -0
- package/dist/client/wsMutationExecutor.js +77 -0
- package/dist/context.js +0 -3
- package/dist/coordination/index.d.ts +8 -1
- package/dist/coordination/index.js +24 -1
- package/dist/coordination/trace.d.ts +0 -1
- package/dist/coordination/trace.js +0 -1
- package/dist/core/DatabaseManager.d.ts +0 -1
- package/dist/core/DatabaseManager.js +26 -24
- package/dist/core/QueryProcessor.js +7 -8
- package/dist/core/QueryView.d.ts +14 -2
- package/dist/core/QueryView.js +4 -5
- package/dist/core/StoreManager.d.ts +2 -2
- package/dist/core/StoreManager.js +5 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/openIDBWithTimeout.js +15 -11
- package/dist/core/query-utils.js +1 -1
- package/dist/core/storeContract.d.ts +143 -0
- package/dist/core/storeContract.js +16 -0
- package/dist/errorCodes.d.ts +11 -2
- package/dist/errorCodes.js +155 -137
- package/dist/errors.d.ts +18 -7
- package/dist/errors.js +14 -1
- package/dist/index.js +5 -0
- package/dist/interfaces/index.d.ts +6 -20
- package/dist/keys/index.js +18 -9
- package/dist/mutators/UndoManager.js +6 -1
- package/dist/mutators/defineMutators.d.ts +1 -3
- package/dist/policy/types.d.ts +1 -1
- package/dist/query/client.d.ts +11 -0
- package/dist/query/client.js +81 -52
- package/dist/react/AbloProvider.d.ts +6 -10
- package/dist/react/AbloProvider.js +22 -15
- package/dist/react/context.d.ts +2 -118
- package/dist/react/useAblo.js +1 -1
- package/dist/react/useErrorListener.js +1 -1
- package/dist/react/useMutationFailureListener.js +1 -1
- package/dist/react/useMutators.js +7 -1
- package/dist/react/useReactive.js +0 -1
- package/dist/react/useUndoScope.js +2 -2
- package/dist/schema/ddlLock.d.ts +39 -0
- package/dist/schema/ddlLock.js +51 -0
- package/dist/schema/field.js +1 -1
- package/dist/schema/index.d.ts +4 -3
- package/dist/schema/index.js +15 -5
- package/dist/schema/model.d.ts +3 -3
- package/dist/schema/model.js +2 -2
- package/dist/schema/openapi.js +5 -1
- package/dist/schema/queries.d.ts +1 -1
- package/dist/schema/queries.js +1 -0
- package/dist/schema/residency.d.ts +35 -0
- package/dist/schema/residency.js +27 -0
- package/dist/schema/roles.d.ts +25 -0
- package/dist/schema/roles.js +37 -0
- package/dist/schema/schema.d.ts +3 -3
- package/dist/schema/schema.js +11 -7
- package/dist/schema/serialize.d.ts +8 -8
- package/dist/schema/sync-delta-row.d.ts +2 -2
- package/dist/schema/sync-delta-row.js +2 -2
- package/dist/server/storage-mode.d.ts +8 -1
- package/dist/server/storage-mode.js +14 -4
- package/dist/source/adapter.d.ts +1 -1
- package/dist/source/adapters/drizzle.js +4 -3
- package/dist/source/adapters/kysely.js +4 -3
- package/dist/source/adapters/memory.js +1 -1
- package/dist/source/adapters/prisma.js +5 -4
- package/dist/source/conformance.js +10 -8
- package/dist/source/connector.js +4 -2
- package/dist/source/factory.d.ts +106 -0
- package/dist/source/factory.js +273 -0
- package/dist/source/index.d.ts +11 -461
- package/dist/source/index.js +14 -416
- package/dist/source/next.d.ts +1 -1
- package/dist/source/next.js +1 -1
- package/dist/source/pushQueue.d.ts +1 -1
- package/dist/source/pushQueue.js +6 -3
- package/dist/source/signing.d.ts +59 -0
- package/dist/source/signing.js +142 -0
- package/dist/source/types.d.ts +334 -0
- package/dist/source/types.js +44 -0
- package/dist/stores/ObjectStore.d.ts +1 -1
- package/dist/stores/ObjectStore.js +23 -23
- package/dist/stores/SyncActionStore.d.ts +1 -1
- package/dist/stores/SyncActionStore.js +70 -35
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +1 -0
- package/dist/sync/AreaOfInterestManager.d.ts +2 -2
- package/dist/sync/AreaOfInterestManager.js +1 -1
- package/dist/sync/BootstrapHelper.d.ts +1 -3
- package/dist/sync/BootstrapHelper.js +29 -8
- package/dist/sync/ConnectionManager.d.ts +8 -0
- package/dist/sync/ConnectionManager.js +19 -4
- package/dist/sync/HydrationCoordinator.d.ts +12 -0
- package/dist/sync/HydrationCoordinator.js +25 -5
- package/dist/sync/NetworkProbe.d.ts +3 -2
- package/dist/sync/NetworkProbe.js +7 -6
- package/dist/sync/SyncWebSocket.d.ts +71 -120
- package/dist/sync/SyncWebSocket.js +324 -630
- package/dist/sync/awaitClaimGrant.js +27 -19
- package/dist/sync/bootstrapApply.d.ts +60 -0
- package/dist/sync/bootstrapApply.js +65 -0
- package/dist/sync/commitFrames.d.ts +43 -0
- package/dist/sync/commitFrames.js +94 -0
- package/dist/sync/createClaimStream.js +3 -2
- package/dist/sync/createSnapshot.d.ts +1 -3
- package/dist/sync/credentialLifecycle.d.ts +175 -0
- package/dist/sync/credentialLifecycle.js +329 -0
- package/dist/sync/deltaPipeline.d.ts +111 -0
- package/dist/sync/deltaPipeline.js +262 -0
- package/dist/sync/groupChange.d.ts +111 -0
- package/dist/sync/groupChange.js +246 -0
- package/dist/sync/heartbeat.d.ts +62 -0
- package/dist/sync/heartbeat.js +91 -0
- package/dist/sync/participants.d.ts +8 -8
- package/dist/sync/syncCursor.d.ts +44 -0
- package/dist/sync/syncCursor.js +59 -0
- package/dist/sync/syncPlan.d.ts +62 -0
- package/dist/sync/syncPlan.js +51 -0
- package/dist/sync/wsFrameHandlers.d.ts +117 -0
- package/dist/sync/wsFrameHandlers.js +378 -0
- package/dist/testing/fixtures/bootstrap.d.ts +9 -11
- package/dist/testing/fixtures/deltas.d.ts +1 -1
- package/dist/testing/fixtures/models.js +1 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +0 -3
- package/dist/testing/helpers/sync-engine-harness.js +2 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/mocks/MockSyncContext.d.ts +1 -17
- package/dist/testing/mocks/MockSyncContext.js +1 -32
- package/dist/testing/mocks/MockSyncStore.d.ts +4 -4
- package/dist/testing/mocks/MockWebSocket.d.ts +2 -2
- package/dist/transactions/TransactionQueue.d.ts +20 -56
- package/dist/transactions/TransactionQueue.js +101 -487
- package/dist/transactions/TransactionStore.d.ts +18 -0
- package/dist/transactions/TransactionStore.js +51 -0
- package/dist/transactions/coalesceRules.d.ts +34 -0
- package/dist/transactions/coalesceRules.js +117 -0
- package/dist/transactions/commitPayload.d.ts +134 -0
- package/dist/transactions/commitPayload.js +152 -0
- package/dist/transactions/deltaConfirmation.d.ts +60 -0
- package/dist/transactions/deltaConfirmation.js +223 -0
- package/dist/transactions/optimistic.d.ts +24 -0
- package/dist/transactions/optimistic.js +45 -0
- package/dist/transactions/persistedReplay.d.ts +93 -0
- package/dist/transactions/persistedReplay.js +105 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.js +0 -5
- package/dist/types/modelData.d.ts +12 -0
- package/dist/types/modelData.js +11 -0
- package/dist/types/participant.d.ts +21 -0
- package/dist/types/participant.js +12 -0
- package/dist/types/streams.d.ts +8 -14
- package/dist/utils/mobx-setup.d.ts +1 -1
- package/dist/utils/mobx-setup.js +2 -2
- package/dist/wire/errorEnvelope.d.ts +11 -2
- package/dist/wire/errorEnvelope.js +14 -2
- package/dist/wire/frames.d.ts +90 -0
- package/dist/wire/frames.js +55 -1
- package/dist/wire/index.d.ts +4 -1
- package/dist/wire/index.js +20 -1
- package/dist/wire/protocol.d.ts +45 -0
- package/dist/wire/protocol.js +45 -0
- package/dist/wire/protocolVersion.d.ts +56 -0
- package/dist/wire/protocolVersion.js +63 -0
- package/docs/api-keys.md +4 -3
- package/docs/examples/existing-python-backend.md +3 -3
- package/docs/identity.md +4 -4
- package/docs/integration-guide.md +1 -1
- package/docs/react.md +1 -1
- package/docs/sessions.md +5 -7
- package/package.json +15 -13
- package/dist/client/index.d.ts +0 -36
- package/dist/client/index.js +0 -33
- package/dist/config/index.d.ts +0 -10
- package/dist/config/index.js +0 -12
- package/dist/interfaces/headless.d.ts +0 -95
- package/dist/interfaces/headless.js +0 -41
- package/dist/query/index.d.ts +0 -6
- package/dist/query/index.js +0 -5
- package/dist/realtime/index.d.ts +0 -10
- package/dist/realtime/index.js +0 -9
- package/dist/schema/plane.d.ts +0 -23
- package/dist/schema/plane.js +0 -19
- package/dist/server/next.d.ts +0 -51
- package/dist/server/next.js +0 -47
- package/dist/sync/OfflineFlush.d.ts +0 -9
- package/dist/sync/OfflineFlush.js +0 -22
- package/dist/sync/OfflineTransactionStore.d.ts +0 -37
- package/dist/sync/OfflineTransactionStore.js +0 -263
- package/dist/transactions/index.d.ts +0 -16
- package/dist/transactions/index.js +0 -7
|
@@ -31,7 +31,7 @@ export function awaitClaimGrant(transport, claimId, options) {
|
|
|
31
31
|
unsubs.push(transport.subscribe('claim_acquired', (p) => {
|
|
32
32
|
if (p?.claimId === claimId) {
|
|
33
33
|
getContext().logger.debug(`claim: acquired ${claimId} (target was free)`);
|
|
34
|
-
settle(() => resolve({ waited: false }));
|
|
34
|
+
settle(() => { resolve({ waited: false }); });
|
|
35
35
|
}
|
|
36
36
|
}));
|
|
37
37
|
unsubs.push(transport.subscribe('claim_granted', (p) => {
|
|
@@ -39,7 +39,7 @@ export function awaitClaimGrant(transport, claimId, options) {
|
|
|
39
39
|
// Promoted to the head of the line — the creator's "it's the agent's
|
|
40
40
|
// turn now" moment after waiting behind a holder.
|
|
41
41
|
getContext().logger.info(`claim: granted ${claimId} — your turn (waited in queue)`);
|
|
42
|
-
settle(() => resolve({ waited: true }));
|
|
42
|
+
settle(() => { resolve({ waited: true }); });
|
|
43
43
|
}
|
|
44
44
|
}));
|
|
45
45
|
if (options?.maxQueueDepth !== undefined) {
|
|
@@ -49,7 +49,9 @@ export function awaitClaimGrant(transport, claimId, options) {
|
|
|
49
49
|
return;
|
|
50
50
|
const position = typeof p.position === 'number' ? p.position : 0;
|
|
51
51
|
if (position >= max) {
|
|
52
|
-
settle(() =>
|
|
52
|
+
settle(() => {
|
|
53
|
+
reject(new AbloClaimedError(`Claim queue for ${claimId} is ${position} deep (max ${max}).`, { code: 'queue_too_deep' }));
|
|
54
|
+
});
|
|
53
55
|
}
|
|
54
56
|
}));
|
|
55
57
|
}
|
|
@@ -64,29 +66,35 @@ export function awaitClaimGrant(transport, claimId, options) {
|
|
|
64
66
|
field: rejection.target.field,
|
|
65
67
|
})
|
|
66
68
|
: claimId;
|
|
67
|
-
settle(() =>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
settle(() => {
|
|
70
|
+
reject(new AbloClaimedError(formatClaimedErrorMessage({
|
|
71
|
+
targetLabel: target,
|
|
72
|
+
heldBy: rejection.heldBy,
|
|
73
|
+
claim: rejection.heldByClaim,
|
|
74
|
+
policyReason: rejection.policyReason,
|
|
75
|
+
fallback: `Claim rejected for ${target}.`,
|
|
76
|
+
}), {
|
|
77
|
+
code: rejection.reason === 'conflict'
|
|
78
|
+
? 'claim_conflict'
|
|
79
|
+
: 'claim_lease_unavailable',
|
|
80
|
+
claims: rejection.heldByClaim ? [rejection.heldByClaim] : undefined,
|
|
81
|
+
}));
|
|
82
|
+
});
|
|
79
83
|
}));
|
|
80
84
|
unsubs.push(transport.subscribe('claim_lost', (p) => {
|
|
81
85
|
if (p?.claimId === claimId) {
|
|
82
|
-
settle(() =>
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
settle(() => {
|
|
87
|
+
reject(new AbloClaimedError(`Claim lost while queued for ${claimId}.`, {
|
|
88
|
+
code: 'claim_lost',
|
|
89
|
+
}));
|
|
90
|
+
});
|
|
85
91
|
}
|
|
86
92
|
}));
|
|
87
93
|
if (options?.timeoutMs && options.timeoutMs > 0) {
|
|
88
94
|
timer = setTimeout(() => {
|
|
89
|
-
settle(() =>
|
|
95
|
+
settle(() => {
|
|
96
|
+
reject(new AbloClaimedError(`Timed out waiting for the queue grant on claim ${claimId}.`, { code: 'grant_timeout' }));
|
|
97
|
+
});
|
|
90
98
|
}, options.timeoutMs);
|
|
91
99
|
}
|
|
92
100
|
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bootstrapApply — applying bootstrap results to the in-memory pool.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from BaseSyncedStore.ts as a cohesive leaf: routing a
|
|
5
|
+
* full/partial bootstrap result through the pool-write facade, collecting
|
|
6
|
+
* the delta-protected ids that must survive ghost removal, and replaying
|
|
7
|
+
* the deltas queued during an active bootstrap. The store keeps thin
|
|
8
|
+
* protected delegates with unchanged signatures and talks back through the
|
|
9
|
+
* minimal {@link PoolContext} — never the store's class type — so no
|
|
10
|
+
* module cycle forms. The heavy lifting (model creation, healing, upsert,
|
|
11
|
+
* ghost removal) stays owned by `SyncClient`.
|
|
12
|
+
*/
|
|
13
|
+
import type { BootstrapResult } from '../Database.js';
|
|
14
|
+
import type { SyncDelta } from './SyncWebSocket.js';
|
|
15
|
+
/** Rehydration statistics from bootstrap */
|
|
16
|
+
export interface RehydrationStats {
|
|
17
|
+
added: number;
|
|
18
|
+
updated: number;
|
|
19
|
+
removed: number;
|
|
20
|
+
skipped: number;
|
|
21
|
+
healed: number;
|
|
22
|
+
elapsedMs: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* What the bootstrap-apply path needs back from its host store. The two
|
|
26
|
+
* `SyncClient` facades come with enrichment pre-bound by the host, so the
|
|
27
|
+
* store's `enrichRelations` override point keeps its dynamic dispatch.
|
|
28
|
+
*/
|
|
29
|
+
export interface PoolContext {
|
|
30
|
+
/** `SyncClient.applyDeltaBatchToPool` with the host's `enrichRelations` bound. */
|
|
31
|
+
applyDeltaBatchToPool(results: NonNullable<BootstrapResult['deltaResults']>): void;
|
|
32
|
+
/** `SyncClient.applyBootstrapDataToPool` — model creation, healing, pool upsert, ghost removal. */
|
|
33
|
+
applyBootstrapDataToPool(bootstrapData: {
|
|
34
|
+
models?: Record<string, unknown[]>;
|
|
35
|
+
failedModels?: string[];
|
|
36
|
+
}, protectedIds?: ReadonlySet<string>): {
|
|
37
|
+
added: number;
|
|
38
|
+
updated: number;
|
|
39
|
+
removed: number;
|
|
40
|
+
skipped: number;
|
|
41
|
+
healed: number;
|
|
42
|
+
};
|
|
43
|
+
/** Pool size — for the completion log line. */
|
|
44
|
+
getPoolSize(): number;
|
|
45
|
+
/** Every id currently in the pool — for delta-protected-id collection. */
|
|
46
|
+
getAllPoolIds(): string[];
|
|
47
|
+
/** Deltas queued during an active bootstrap; null when none is in flight.
|
|
48
|
+
* Backed by the host's `bootstrapDeltaQueue` field (get/set accessors). */
|
|
49
|
+
bootstrapDeltaQueue: SyncDelta[] | null;
|
|
50
|
+
/** The host's atomic frame apply — `applyDeltaFrame` deliberately stays
|
|
51
|
+
* in BaseSyncedStore (the authoritative-apply correctness seam). */
|
|
52
|
+
applyDeltaFrame(deltas: SyncDelta[]): void;
|
|
53
|
+
}
|
|
54
|
+
/** Apply bootstrap data to the ObjectPool with ghost removal */
|
|
55
|
+
/** Apply bootstrap data to the ObjectPool. Delegates pool writes to SyncClient. */
|
|
56
|
+
export declare function applyBootstrapToPool(ctx: PoolContext, bootstrapResult: BootstrapResult, protectedIds?: ReadonlySet<string>): RehydrationStats;
|
|
57
|
+
/** Collect IDs that must survive ghost removal (added by deltas during bootstrap) */
|
|
58
|
+
export declare function collectDeltaProtectedIds(ctx: PoolContext, preBootstrapIds: ReadonlySet<string>): Set<string>;
|
|
59
|
+
/** Replay deltas queued during bootstrap */
|
|
60
|
+
export declare function replayQueuedDeltas(ctx: PoolContext): void;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bootstrapApply — applying bootstrap results to the in-memory pool.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from BaseSyncedStore.ts as a cohesive leaf: routing a
|
|
5
|
+
* full/partial bootstrap result through the pool-write facade, collecting
|
|
6
|
+
* the delta-protected ids that must survive ghost removal, and replaying
|
|
7
|
+
* the deltas queued during an active bootstrap. The store keeps thin
|
|
8
|
+
* protected delegates with unchanged signatures and talks back through the
|
|
9
|
+
* minimal {@link PoolContext} — never the store's class type — so no
|
|
10
|
+
* module cycle forms. The heavy lifting (model creation, healing, upsert,
|
|
11
|
+
* ghost removal) stays owned by `SyncClient`.
|
|
12
|
+
*/
|
|
13
|
+
import { getContext } from '../context.js';
|
|
14
|
+
/** Apply bootstrap data to the ObjectPool with ghost removal */
|
|
15
|
+
/** Apply bootstrap data to the ObjectPool. Delegates pool writes to SyncClient. */
|
|
16
|
+
export function applyBootstrapToPool(ctx, bootstrapResult, protectedIds) {
|
|
17
|
+
const { bootstrapData } = bootstrapResult;
|
|
18
|
+
// Partial bootstrap: Database.processDeltaBatch already wrote the deltas
|
|
19
|
+
// to IDB. Route the same results through the delta-apply path so the
|
|
20
|
+
// in-memory pool evicts deleted entities (and updates modified ones).
|
|
21
|
+
// Without this, reconnect DELETEs persist to IDB but the canvas keeps
|
|
22
|
+
// showing ghost layers until a full reload.
|
|
23
|
+
if (bootstrapData.type === 'partial') {
|
|
24
|
+
const deltaResults = bootstrapResult.deltaResults;
|
|
25
|
+
if (deltaResults && deltaResults.length > 0) {
|
|
26
|
+
ctx.applyDeltaBatchToPool(deltaResults);
|
|
27
|
+
}
|
|
28
|
+
return { added: 0, updated: 0, removed: 0, skipped: 0, healed: 0, elapsedMs: 0 };
|
|
29
|
+
}
|
|
30
|
+
if (!bootstrapData.models) {
|
|
31
|
+
return { added: 0, updated: 0, removed: 0, skipped: 0, healed: 0, elapsedMs: 0 };
|
|
32
|
+
}
|
|
33
|
+
const start = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
34
|
+
// SyncClient owns: model creation, healing, pool upsert, ghost removal
|
|
35
|
+
const stats = ctx.applyBootstrapDataToPool(bootstrapData, protectedIds);
|
|
36
|
+
const elapsedMs = Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - start);
|
|
37
|
+
getContext().logger.info('[BaseSyncedStore] Bootstrap applied', {
|
|
38
|
+
...stats, elapsedMs, poolSize: ctx.getPoolSize(),
|
|
39
|
+
});
|
|
40
|
+
return { ...stats, elapsedMs };
|
|
41
|
+
}
|
|
42
|
+
/** Collect IDs that must survive ghost removal (added by deltas during bootstrap) */
|
|
43
|
+
export function collectDeltaProtectedIds(ctx, preBootstrapIds) {
|
|
44
|
+
const protectedIds = new Set();
|
|
45
|
+
for (const id of ctx.getAllPoolIds()) {
|
|
46
|
+
if (!preBootstrapIds.has(id))
|
|
47
|
+
protectedIds.add(id);
|
|
48
|
+
}
|
|
49
|
+
for (const delta of ctx.bootstrapDeltaQueue ?? []) {
|
|
50
|
+
if (delta.actionType !== 'D' && delta.modelId)
|
|
51
|
+
protectedIds.add(delta.modelId);
|
|
52
|
+
}
|
|
53
|
+
return protectedIds;
|
|
54
|
+
}
|
|
55
|
+
/** Replay deltas queued during bootstrap */
|
|
56
|
+
export function replayQueuedDeltas(ctx) {
|
|
57
|
+
const queue = ctx.bootstrapDeltaQueue;
|
|
58
|
+
ctx.bootstrapDeltaQueue = null;
|
|
59
|
+
if (!queue || queue.length === 0)
|
|
60
|
+
return;
|
|
61
|
+
// Deltas that landed during bootstrap are a complete frame — apply
|
|
62
|
+
// them atomically (one flush, one re-render) rather than dribbling
|
|
63
|
+
// each back through the live debounce path.
|
|
64
|
+
ctx.applyDeltaFrame(queue);
|
|
65
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commit-path frame builders and claim instrumentation for the sync
|
|
3
|
+
* WebSocket — pure functions with no socket state, extracted from
|
|
4
|
+
* SyncWebSocket so the wire codec is a leaf the transport (and the frame
|
|
5
|
+
* dispatch table) can share.
|
|
6
|
+
*/
|
|
7
|
+
import type { CommitMessage } from '../wire/index.js';
|
|
8
|
+
import type { MutationOperation, ClaimEvent } from '../interfaces/index.js';
|
|
9
|
+
import type { StaleNotification, ReadDependency } from '../coordination/schema.js';
|
|
10
|
+
/**
|
|
11
|
+
* Resolution value of a commit ack. `notifications` is present only when a
|
|
12
|
+
* guarded write (`onStale: 'notify') hit a concurrent change — the
|
|
13
|
+
* advisory self-heal signal, surfaced both here and via `conflict:notified`.
|
|
14
|
+
*/
|
|
15
|
+
export interface CommitAck {
|
|
16
|
+
lastSyncId: number;
|
|
17
|
+
notifications?: StaleNotification[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Project the SDK's `MutationOperation[]` onto the canonical wire
|
|
21
|
+
* `CommitMessage`. This is the single serialize boundary between the SDK op
|
|
22
|
+
* type (loose `type: string`, plus an SDK-internal `options` the server never
|
|
23
|
+
* reads) and the strict wire contract. The per-field map gives compile-time
|
|
24
|
+
* drift detection (a `CommitOperation` shape change breaks here) and the lone
|
|
25
|
+
* `as` narrows the validated op `type` to the wire union — the only
|
|
26
|
+
* loosening, localized to this boundary.
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildCommitFrame(operations: readonly MutationOperation[], clientTxId: string, causedByTaskId?: string | null, reads?: readonly ReadDependency[] | null): CommitMessage;
|
|
29
|
+
/**
|
|
30
|
+
* Defensively validate the optional `notifications` array off a commit ack.
|
|
31
|
+
* Untrusted wire data — a malformed entry is dropped rather than throwing,
|
|
32
|
+
* so a bad notification never sinks an otherwise-successful commit.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseNotifications(raw: unknown): StaleNotification[] | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Single instrumentation point for claim events. Every `claim_*` frame routes
|
|
37
|
+
* through here so a developer debugging a collision gets one consistent trace
|
|
38
|
+
* — a console line AND a structured capture — without each dispatch case
|
|
39
|
+
* re-deriving the row/holder shape. The wire payload is loosely typed
|
|
40
|
+
* (`Record<string, unknown>`), so this is the one place that narrows it into
|
|
41
|
+
* a {@link ClaimEvent}.
|
|
42
|
+
*/
|
|
43
|
+
export declare function recordClaim(phase: ClaimEvent['phase'], payload: Record<string, unknown>): void;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commit-path frame builders and claim instrumentation for the sync
|
|
3
|
+
* WebSocket — pure functions with no socket state, extracted from
|
|
4
|
+
* SyncWebSocket so the wire codec is a leaf the transport (and the frame
|
|
5
|
+
* dispatch table) can share.
|
|
6
|
+
*/
|
|
7
|
+
import { getContext } from '../context.js';
|
|
8
|
+
import { staleNotificationSchema, wireParticipantKindSchema, } from '../coordination/schema.js';
|
|
9
|
+
import { formatClaim } from '../coordination/trace.js';
|
|
10
|
+
/**
|
|
11
|
+
* Project the SDK's `MutationOperation[]` onto the canonical wire
|
|
12
|
+
* `CommitMessage`. This is the single serialize boundary between the SDK op
|
|
13
|
+
* type (loose `type: string`, plus an SDK-internal `options` the server never
|
|
14
|
+
* reads) and the strict wire contract. The per-field map gives compile-time
|
|
15
|
+
* drift detection (a `CommitOperation` shape change breaks here) and the lone
|
|
16
|
+
* `as` narrows the validated op `type` to the wire union — the only
|
|
17
|
+
* loosening, localized to this boundary.
|
|
18
|
+
*/
|
|
19
|
+
export function buildCommitFrame(operations, clientTxId, causedByTaskId, reads) {
|
|
20
|
+
const payload = {
|
|
21
|
+
operations: operations.map((op) => ({
|
|
22
|
+
type: op.type,
|
|
23
|
+
model: op.model,
|
|
24
|
+
id: op.id,
|
|
25
|
+
input: op.input,
|
|
26
|
+
transactionId: op.transactionId,
|
|
27
|
+
readAt: op.readAt,
|
|
28
|
+
onStale: op.onStale,
|
|
29
|
+
})),
|
|
30
|
+
clientTxId,
|
|
31
|
+
};
|
|
32
|
+
if (causedByTaskId)
|
|
33
|
+
payload.causedByTaskId = causedByTaskId;
|
|
34
|
+
// Batch-level read-set (STORM layer): rows/groups the batch was premised on.
|
|
35
|
+
if (reads && reads.length > 0)
|
|
36
|
+
payload.reads = [...reads];
|
|
37
|
+
return { type: 'commit', payload };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Defensively validate the optional `notifications` array off a commit ack.
|
|
41
|
+
* Untrusted wire data — a malformed entry is dropped rather than throwing,
|
|
42
|
+
* so a bad notification never sinks an otherwise-successful commit.
|
|
43
|
+
*/
|
|
44
|
+
export function parseNotifications(raw) {
|
|
45
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
46
|
+
return undefined;
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const entry of raw) {
|
|
49
|
+
const parsed = staleNotificationSchema.safeParse(entry);
|
|
50
|
+
if (parsed.success)
|
|
51
|
+
out.push(parsed.data);
|
|
52
|
+
}
|
|
53
|
+
return out.length > 0 ? out : undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Single instrumentation point for claim events. Every `claim_*` frame routes
|
|
57
|
+
* through here so a developer debugging a collision gets one consistent trace
|
|
58
|
+
* — a console line AND a structured capture — without each dispatch case
|
|
59
|
+
* re-deriving the row/holder shape. The wire payload is loosely typed
|
|
60
|
+
* (`Record<string, unknown>`), so this is the one place that narrows it into
|
|
61
|
+
* a {@link ClaimEvent}.
|
|
62
|
+
*/
|
|
63
|
+
export function recordClaim(phase, payload) {
|
|
64
|
+
const str = (v) => typeof v === 'string' ? v : undefined;
|
|
65
|
+
// Targets arrive flat ({ entityType, entityId }) or nested under `target`.
|
|
66
|
+
const target = payload.target && typeof payload.target === 'object'
|
|
67
|
+
? payload.target
|
|
68
|
+
: payload;
|
|
69
|
+
const kind = wireParticipantKindSchema.safeParse(payload.participantKind);
|
|
70
|
+
const event = {
|
|
71
|
+
phase,
|
|
72
|
+
claimId: str(payload.claimId),
|
|
73
|
+
model: str(target.entityType) ?? str(target.model),
|
|
74
|
+
id: str(target.entityId) ?? str(target.id),
|
|
75
|
+
field: str(target.field),
|
|
76
|
+
actor: str(payload.actor) ?? str(payload.heldBy),
|
|
77
|
+
participantKind: kind.success ? kind.data : undefined,
|
|
78
|
+
position: typeof payload.position === 'number' ? payload.position : undefined,
|
|
79
|
+
reason: str(payload.policyReason) ?? str(payload.reason),
|
|
80
|
+
};
|
|
81
|
+
const message = formatClaim(event);
|
|
82
|
+
// A rejection or lost lease is the collision a developer is actively
|
|
83
|
+
// debugging → warn (shows at the default log level). The routine events
|
|
84
|
+
// (acquired/queued/granted/expired) are debug-only so they never drown the
|
|
85
|
+
// console until you opt in with `new Ablo({ debug: true })`.
|
|
86
|
+
const isCollision = phase === 'rejected' || phase === 'lost';
|
|
87
|
+
const ctx = getContext();
|
|
88
|
+
if (isCollision)
|
|
89
|
+
ctx.logger.warn(message);
|
|
90
|
+
else
|
|
91
|
+
ctx.logger.debug(message);
|
|
92
|
+
ctx.observability.breadcrumb(message, 'sync.coordination', isCollision ? 'warning' : 'info');
|
|
93
|
+
ctx.observability.captureClaim(event);
|
|
94
|
+
}
|
|
@@ -189,8 +189,9 @@ export function createClaimStream(config, transport = null) {
|
|
|
189
189
|
// If WE are in this line, trace our position (the "agent queued behind a
|
|
190
190
|
// claim" moment) — once per position change, so advancing is visible.
|
|
191
191
|
const ourIndex = line.findIndex((c) => ownClaims.has(c.id));
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
const ourClaim = ourIndex >= 0 ? line[ourIndex] : undefined;
|
|
193
|
+
if (ourClaim) {
|
|
194
|
+
const ourId = ourClaim.id;
|
|
194
195
|
if (lastLoggedQueuePos.get(ourId) !== ourIndex) {
|
|
195
196
|
lastLoggedQueuePos.set(ourId, ourIndex);
|
|
196
197
|
getContext().logger.info(`claim: queued for ${claimLabel(p.target.type, p.target.id)} — position ${ourIndex + 1} of ${line.length}, waiting`, { claimId: ourId });
|
|
@@ -26,8 +26,6 @@ export interface CreateSnapshotArgs<TSchema extends Schema = Schema, K extends k
|
|
|
26
26
|
/** Returns the engine's current `lastSyncId`. Read at snapshot time
|
|
27
27
|
* to stamp the watermark; not re-read after. */
|
|
28
28
|
getLastSyncId: () => number;
|
|
29
|
-
entities:
|
|
30
|
-
readonly [M in K]: string | readonly string[];
|
|
31
|
-
};
|
|
29
|
+
entities: Readonly<Record<K, string | readonly string[]>>;
|
|
32
30
|
}
|
|
33
31
|
export declare function createSnapshot<TSchema extends Schema, K extends keyof TSchema['models'] & string>(args: CreateSnapshotArgs<TSchema, K>): Snapshot<TSchema, K>;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* credentialLifecycle — reconnect-credential re-mint + proactive pre-roll.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from BaseSyncedStore.ts as a cohesive leaf: owns the re-mint
|
|
5
|
+
* hook, the single-flight guard, and the browser-only proactive refresh
|
|
6
|
+
* (timer + OS-wake listener). The store keeps thin public delegates
|
|
7
|
+
* (`setCredentialRefresher` / `performCredentialRefresh` /
|
|
8
|
+
* `startCredentialLifecycle`) so its published surface is unchanged, and
|
|
9
|
+
* talks back through the minimal {@link CredentialLifecycleContext} —
|
|
10
|
+
* never the store's class type — so no module cycle forms.
|
|
11
|
+
*/
|
|
12
|
+
import type { RecoveryClass } from '../errorCodes.js';
|
|
13
|
+
/**
|
|
14
|
+
* Tri-state outcome of a credential re-mint, mirroring the `getToken`
|
|
15
|
+
* contract (see {@link CredentialLifecycle.refresh}).
|
|
16
|
+
*/
|
|
17
|
+
export type CredentialRefreshOutcome = 'refreshed' | 'session_error' | 'network_error';
|
|
18
|
+
/**
|
|
19
|
+
* What an auth-rejected transport should do after the backbone has run:
|
|
20
|
+
* `'retry'` = a fresh credential is in the credential source, replay the
|
|
21
|
+
* request ONCE; `'stop'` = don't replay (terminal session loss, a rejection
|
|
22
|
+
* a re-mint can't cure, or a transient mint failure the caller's own
|
|
23
|
+
* retry/revalidation path will recover later).
|
|
24
|
+
*/
|
|
25
|
+
export type CredentialRecoveryOutcome = 'retry' | 'stop';
|
|
26
|
+
/**
|
|
27
|
+
* What a credential refresher may resolve with. The plain-string form is the
|
|
28
|
+
* classic `getToken` contract; the object form additionally carries the mint
|
|
29
|
+
* response's `expiresAt` (ISO string / epoch ms / Date) so the proactive
|
|
30
|
+
* pre-roll can schedule off the credential's ACTUAL lifetime instead of
|
|
31
|
+
* assuming the server's default TTL.
|
|
32
|
+
*/
|
|
33
|
+
export type CredentialRefreshResult = string | {
|
|
34
|
+
readonly token: string;
|
|
35
|
+
readonly expiresAt?: string | number | Date;
|
|
36
|
+
} | null;
|
|
37
|
+
/** A credential re-mint hook. `Promise<string | null>` resolvers remain
|
|
38
|
+
* assignable — the widened result type is a superset. */
|
|
39
|
+
export type CredentialRefresher = () => Promise<CredentialRefreshResult>;
|
|
40
|
+
/** Fallback pre-roll cadence AND ceiling — the historical 10-minute value,
|
|
41
|
+
* comfortably inside the server's 15m default `ek_` TTL. Used verbatim when
|
|
42
|
+
* the refresher reports no expiry. */
|
|
43
|
+
export declare const DEFAULT_PREROLL_INTERVAL_MS: number;
|
|
44
|
+
/** Floor so a very short (or already-elapsed) TTL can't hot-loop the mint. */
|
|
45
|
+
export declare const MIN_PREROLL_DELAY_MS: number;
|
|
46
|
+
/**
|
|
47
|
+
* When to pre-roll a credential that expires at `expiresAtMs`: after ~2/3 of
|
|
48
|
+
* the REMAINING lifetime (15m TTL → 10m, exactly the historical cadence),
|
|
49
|
+
* clamped to [{@link MIN_PREROLL_DELAY_MS}, {@link DEFAULT_PREROLL_INTERVAL_MS}].
|
|
50
|
+
* Unknown expiry (`null`) → the 10-minute fallback. Exported for unit tests.
|
|
51
|
+
*/
|
|
52
|
+
export declare function computePrerollDelayMs(expiresAtMs: number | null, nowMs: number): number;
|
|
53
|
+
/**
|
|
54
|
+
* What the lifecycle needs back from its host store. Deliberately minimal —
|
|
55
|
+
* three callbacks, all resolved lazily at call time (the ConnectionManager
|
|
56
|
+
* behind two of them doesn't exist until `setupWebSocketSync`).
|
|
57
|
+
*/
|
|
58
|
+
export interface CredentialLifecycleContext {
|
|
59
|
+
/** Push a freshly-minted access token into the shared credential source
|
|
60
|
+
* (no-op when the deployment wired no credential source). */
|
|
61
|
+
setAuthToken(token: string): void;
|
|
62
|
+
/** Nudge the connection FSM to re-probe with the current credential —
|
|
63
|
+
* the host's `nudgeReconnect()` (`CREDENTIAL_REFRESHED`). */
|
|
64
|
+
nudgeReconnect(): void;
|
|
65
|
+
/** Surface a terminal session loss to the connection FSM
|
|
66
|
+
* (`BOOTSTRAP_FAILED_SESSION`) — the long-lived login is gone. */
|
|
67
|
+
reportSessionExpired(): void;
|
|
68
|
+
}
|
|
69
|
+
export declare class CredentialLifecycle {
|
|
70
|
+
private readonly ctx;
|
|
71
|
+
/**
|
|
72
|
+
* Re-mint hook for the short-lived access credential (the Stripe-style
|
|
73
|
+
* `ek_`/`rk_`). Wired by the React provider from its `getToken`/`authEndpoint`
|
|
74
|
+
* — the engine owns WHEN to refresh (a stale-credential probe / an external
|
|
75
|
+
* nudge), the integrator owns HOW to mint. Mirrors the `getToken` contract:
|
|
76
|
+
* resolves a token string on success, `null` when the long-lived login is
|
|
77
|
+
* gone (terminal), and THROWS on a transient/offline failure. Used by
|
|
78
|
+
* {@link refresh}. Absent ⇒ no silent re-mint (e.g. a static
|
|
79
|
+
* `apiKey` deployment whose credential source refreshes out-of-band).
|
|
80
|
+
*/
|
|
81
|
+
private credentialRefresher;
|
|
82
|
+
/** Single-flight guard so a wake nudge + an in-flight request + a probe don't
|
|
83
|
+
* all mint at once (the classic "token thrash → random logout" bug). */
|
|
84
|
+
private inFlightCredentialRefresh;
|
|
85
|
+
/** Teardown for the proactive credential lifecycle (refresh timer + wake/
|
|
86
|
+
* online/focus listeners) installed by {@link start};
|
|
87
|
+
* cleared on `BaseSyncedStore.disconnect`. Null when no resolver is wired. */
|
|
88
|
+
private credentialLifecycleTeardown;
|
|
89
|
+
/** Epoch ms the CURRENT credential expires, when the refresher reports it
|
|
90
|
+
* (object-form {@link CredentialRefreshResult}); `null` for the classic
|
|
91
|
+
* string-form resolver → the pre-roll falls back to its fixed cadence. */
|
|
92
|
+
private credentialExpiresAtMs;
|
|
93
|
+
/** Re-arm hook for the proactive pre-roll timer (set by {@link start}).
|
|
94
|
+
* Invoked after every successful mint so a REACTIVE refresh (probe, wake,
|
|
95
|
+
* the initial `ready()` mint) immediately re-anchors the schedule to the
|
|
96
|
+
* fresh credential's real expiry instead of a stale fixed delay. */
|
|
97
|
+
private prerollReschedule;
|
|
98
|
+
constructor(ctx: CredentialLifecycleContext);
|
|
99
|
+
/**
|
|
100
|
+
* Register the access-credential re-mint hook. Called by the React provider
|
|
101
|
+
* with a thunk that mints a fresh `ek_`/`rk_` (typically its `getToken`).
|
|
102
|
+
* See {@link credentialRefresher}.
|
|
103
|
+
*/
|
|
104
|
+
setRefresher(refresher: CredentialRefresher | null): void;
|
|
105
|
+
/**
|
|
106
|
+
* Re-mint the short-lived access credential and push it into the credential
|
|
107
|
+
* source, reporting a tri-state outcome the `ConnectionManager` maps to
|
|
108
|
+
* its FSM. The contract mirrors `getToken` (and PowerSync's `fetchCredentials`
|
|
109
|
+
* / Liveblocks' `authEndpoint`, but made explicit instead of overloading
|
|
110
|
+
* return/throw):
|
|
111
|
+
* - token string → `'refreshed'` (fresh key in place; re-probe & reconnect)
|
|
112
|
+
* - `null` → `'session_error'` (login itself is gone → terminal, sign out)
|
|
113
|
+
* - throw → `'network_error'` (couldn't reach the mint endpoint → transient)
|
|
114
|
+
*
|
|
115
|
+
* SINGLE-FLIGHT: concurrent callers (a wake nudge, an in-flight request, the
|
|
116
|
+
* probe) share one in-flight promise so we never double-mint — the canonical
|
|
117
|
+
* fix for the "every 401 mints a token → thrash → spurious logout" anti-pattern.
|
|
118
|
+
*
|
|
119
|
+
* No refresher wired ⇒ `'refreshed'` (a no-op re-probe): a static-`apiKey`
|
|
120
|
+
* deployment has no session to re-mint from; its credential source refreshes
|
|
121
|
+
* out-of-band, so we just re-probe with whatever it currently holds.
|
|
122
|
+
*/
|
|
123
|
+
refresh(): Promise<CredentialRefreshOutcome>;
|
|
124
|
+
/**
|
|
125
|
+
* Route a refresh outcome into the FSM — the ONE place the tri-state is
|
|
126
|
+
* interpreted, shared by the proactive pre-roll, the wake nudge, and the
|
|
127
|
+
* HTTP auth-recovery path so every trigger converges on identical behavior.
|
|
128
|
+
*/
|
|
129
|
+
private routeRefreshOutcome;
|
|
130
|
+
/**
|
|
131
|
+
* THE recovery backbone for auth-rejected requests, whatever the transport.
|
|
132
|
+
* The WS probe already routes 401s through the FSM; HTTP consumers (the
|
|
133
|
+
* lazy-query lane, bootstrap helpers) call THIS instead of inventing their
|
|
134
|
+
* own handling — one single-flight mint, one outcome routing, one taxonomy.
|
|
135
|
+
*
|
|
136
|
+
* Mirrors `NetworkProbe`'s classification of the same codes:
|
|
137
|
+
* - `access_credential_expiry` — the routine case (an expired `ek_`/`rk_`):
|
|
138
|
+
* silently re-mint via the single-flight {@link refresh} and tell the
|
|
139
|
+
* caller to replay ONCE on success. Never a sign-out by itself — the
|
|
140
|
+
* only terminal path is the mint itself resolving `null`.
|
|
141
|
+
* - `session_expiry` — the login is gone. Report to the FSM (sign-out
|
|
142
|
+
* path) and stop; replaying is pointless.
|
|
143
|
+
* - `auth_blocked` / `permission` / everything else — re-minting produces
|
|
144
|
+
* the same rejected credential; stop without touching the FSM.
|
|
145
|
+
*/
|
|
146
|
+
recoverFromAuthRejection(recovery: RecoveryClass): Promise<CredentialRecoveryOutcome>;
|
|
147
|
+
/**
|
|
148
|
+
* Install the access-credential lifecycle the CLIENT owns (this used to live
|
|
149
|
+
* in the React provider — wrong layer). Two parts:
|
|
150
|
+
* 1. REACTIVE — register `getToken` as the re-mint hook the FSM calls when a
|
|
151
|
+
* probe finds the key stale (`credential_stale`) or on a nudge.
|
|
152
|
+
* 2. PROACTIVE — keep the short-lived key fresh ahead of trouble: a refresh
|
|
153
|
+
* timer inside the TTL, plus re-mint on OS wake. The ENTIRE proactive
|
|
154
|
+
* block is browser-gated (`typeof window`): server/SSR has no socket to
|
|
155
|
+
* keep warm and the resolver is browser-oriented, so arming it in Node
|
|
156
|
+
* would fire a relative-URL fetch and throw. (Agents pass a static
|
|
157
|
+
* `apiKey` with no resolver, so this method is never called for them.)
|
|
158
|
+
*
|
|
159
|
+
* Config-driven and invisible, like Supabase's `autoRefreshToken` — consumers
|
|
160
|
+
* never call a refresh method. Idempotent (a second call replaces the first);
|
|
161
|
+
* torn down on `BaseSyncedStore.disconnect`.
|
|
162
|
+
*
|
|
163
|
+
* `opts.proactiveInNode` arms the refresh TIMER on a windowless host too —
|
|
164
|
+
* set for agent/system participants (long-lived server sockets whose
|
|
165
|
+
* `rk_`/`ek_` must renew BEFORE the hub's keepalive reaper closes them; the
|
|
166
|
+
* PowerSync/Ably "SDK renews everywhere" model). Node timers are `unref`ed
|
|
167
|
+
* so a finishing script is never kept alive by the pre-roll. The OS-wake
|
|
168
|
+
* listener remains browser-only regardless (no `window` to listen on).
|
|
169
|
+
*/
|
|
170
|
+
start(getToken: CredentialRefresher, opts?: {
|
|
171
|
+
proactiveInNode?: boolean;
|
|
172
|
+
}): void;
|
|
173
|
+
/** Tear down the proactive credential lifecycle (idempotent). */
|
|
174
|
+
stop(): void;
|
|
175
|
+
}
|