@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
|
@@ -19,11 +19,13 @@ import type { Database, BootstrapResult } from './Database.js';
|
|
|
19
19
|
import type { InstanceCache } from './InstanceCache.js';
|
|
20
20
|
import { ModelRegistry } from './ModelRegistry.js';
|
|
21
21
|
import { SyncWebSocket, type SyncDelta, type SyncGroupChangePayload, type GroupAddedPayload, type GroupRemovedPayload, type BootstrapHint, type BootstrapDataEvent, type PresenceUpdate, type EventMap, type DefaultCollaborationEvents, type SyncWebSocketEventMap } from './sync/SyncWebSocket.js';
|
|
22
|
-
import { QueryProcessor } from './
|
|
22
|
+
import { QueryProcessor } from './query/QueryProcessor.js';
|
|
23
23
|
import { Model } from './Model.js';
|
|
24
|
+
import type { RuntimeContext } from './RuntimeContext.js';
|
|
25
|
+
import type { AbloPlugin, AppliedChange } from './transaction/plugin.js';
|
|
24
26
|
import { ModelScope } from './InstanceCache.js';
|
|
25
27
|
import type { Schema } from './transaction/schema/schema.js';
|
|
26
|
-
import type { SyncStatus, LocalMutation } from './
|
|
28
|
+
import type { SyncStatus, LocalMutation } from './storeContract.js';
|
|
27
29
|
import type { AuthCredentialSource } from './transaction/auth/credentialSource.js';
|
|
28
30
|
import type { ModelData } from './transaction/types/modelData.js';
|
|
29
31
|
import type { EnrichmentPlanEntry, ForeignKeyIndexSpec } from './sync/syncPlan.js';
|
|
@@ -75,7 +77,7 @@ export interface SyncedStoreConfig {
|
|
|
75
77
|
*/
|
|
76
78
|
foreignKeyIndexes?: readonly ForeignKeyIndexSpec[];
|
|
77
79
|
}
|
|
78
|
-
export type { SyncStatus } from './
|
|
80
|
+
export type { SyncStatus } from './storeContract.js';
|
|
79
81
|
/** User context for initialization */
|
|
80
82
|
export interface UserContext {
|
|
81
83
|
userId: string;
|
|
@@ -162,6 +164,10 @@ export { deriveSyncPlanFromSchema } from './sync/syncPlan.js';
|
|
|
162
164
|
*/
|
|
163
165
|
export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaboration> = DefaultCollaborationEvents, TSchema extends Schema = Schema> {
|
|
164
166
|
syncStatus: SyncStatus;
|
|
167
|
+
/** The owning client's runtime; the module-global bridge when constructed directly. */
|
|
168
|
+
protected readonly runtime: RuntimeContext;
|
|
169
|
+
/** The installed plugins the delta pipeline dispatches stage handlers to. */
|
|
170
|
+
protected readonly stagePlugins: readonly AbloPlugin[];
|
|
165
171
|
protected readonly syncClient: SyncClient;
|
|
166
172
|
protected readonly database: Database;
|
|
167
173
|
protected readonly objectPool: InstanceCache;
|
|
@@ -307,6 +313,14 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
|
|
|
307
313
|
url?: string;
|
|
308
314
|
/** Shared bearer credential source for every auth-aware transport. */
|
|
309
315
|
auth?: AuthCredentialSource;
|
|
316
|
+
/** The owning client's runtime. Defaults to the module-global bridge. */
|
|
317
|
+
runtime?: RuntimeContext;
|
|
318
|
+
/**
|
|
319
|
+
* The installed plugins, whose declared stage handlers the delta
|
|
320
|
+
* pipeline dispatches. Empty on direct construction — the store's own
|
|
321
|
+
* apply is then the whole pipeline.
|
|
322
|
+
*/
|
|
323
|
+
stagePlugins?: readonly AbloPlugin[];
|
|
310
324
|
}, config?: SyncedStoreConfig);
|
|
311
325
|
/**
|
|
312
326
|
* Register foreign-key indexes for constant-time lookups.
|
|
@@ -411,7 +425,7 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
|
|
|
411
425
|
waitForConfirmation(modelName: string, modelId: string): Promise<void>;
|
|
412
426
|
/**
|
|
413
427
|
* Observe the LOCAL mutation stream for undo recording (see
|
|
414
|
-
* {@link import('./
|
|
428
|
+
* {@link import('./storeContract.js').LocalMutation}). Taps the
|
|
415
429
|
* MutationQueue's `transaction:created` event — fired once per local
|
|
416
430
|
* create/update/delete/archive with `previousData` already captured.
|
|
417
431
|
* Remote/collaborator deltas apply via `applyDeltaBatchToPool` and never
|
|
@@ -649,6 +663,12 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
|
|
|
649
663
|
* read the live host fields). */
|
|
650
664
|
private _deltaPipelineContext;
|
|
651
665
|
private get deltaPipelineContext();
|
|
666
|
+
/**
|
|
667
|
+
* Lands persisted changes in the in-memory pool, with this store's
|
|
668
|
+
* relation enrichment bound. The one apply path: the pipeline's bridge
|
|
669
|
+
* (no plugins installed) and the `humans()` apply handler both call it.
|
|
670
|
+
*/
|
|
671
|
+
applyChangesToPool(changes: readonly AppliedChange[]): void;
|
|
652
672
|
/** Get fields that represent meaningful state for deduplication. Override for model-specific fields. */
|
|
653
673
|
protected getStateFields(_modelName: string): string[];
|
|
654
674
|
/** Deduplicate deltas to the same entity — keep meaningful state transitions only */
|
package/dist/BaseSyncedStore.js
CHANGED
|
@@ -19,9 +19,9 @@ import { resolveParticipantSyncGroups, } from './sync/participants.js';
|
|
|
19
19
|
import { ModelRegistry } from './ModelRegistry.js';
|
|
20
20
|
import { PropertyType } from './transaction/types/index.js';
|
|
21
21
|
import { SyncWebSocket, } from './sync/SyncWebSocket.js';
|
|
22
|
-
import { QueryProcessor } from './
|
|
22
|
+
import { QueryProcessor } from './query/QueryProcessor.js';
|
|
23
23
|
import { Model, rowAsModel } from './Model.js';
|
|
24
|
-
import {
|
|
24
|
+
import { globalRuntime } from './context.js';
|
|
25
25
|
import { AbloSessionError, isAccessCredentialExpiryCloseReason } from './transaction/errors.js';
|
|
26
26
|
import { ModelScope } from './InstanceCache.js';
|
|
27
27
|
import { LazyReferenceCollection } from './LazyReferenceCollection.js';
|
|
@@ -84,6 +84,10 @@ export class BaseSyncedStore {
|
|
|
84
84
|
isSessionError: false,
|
|
85
85
|
};
|
|
86
86
|
// ── Injected dependencies ──
|
|
87
|
+
/** The owning client's runtime; the module-global bridge when constructed directly. */
|
|
88
|
+
runtime;
|
|
89
|
+
/** The installed plugins the delta pipeline dispatches stage handlers to. */
|
|
90
|
+
stagePlugins;
|
|
87
91
|
syncClient;
|
|
88
92
|
database;
|
|
89
93
|
objectPool;
|
|
@@ -203,7 +207,7 @@ export class BaseSyncedStore {
|
|
|
203
207
|
this.hydratedGroups.add(g);
|
|
204
208
|
}
|
|
205
209
|
catch (err) {
|
|
206
|
-
|
|
210
|
+
this.runtime.logger.debug('[BaseSyncedStore] scoped hydrate failed', {
|
|
207
211
|
syncGroups: need,
|
|
208
212
|
error: err instanceof Error ? err.message : String(err),
|
|
209
213
|
});
|
|
@@ -280,6 +284,8 @@ export class BaseSyncedStore {
|
|
|
280
284
|
modelTypesHydrated = new Set();
|
|
281
285
|
modelTypeHydrationInFlight = new Map();
|
|
282
286
|
constructor(dependencies, config = {}) {
|
|
287
|
+
this.runtime = dependencies.runtime ?? globalRuntime;
|
|
288
|
+
this.stagePlugins = dependencies.stagePlugins ?? [];
|
|
283
289
|
this.syncClient = dependencies.syncClient;
|
|
284
290
|
this.database = dependencies.database;
|
|
285
291
|
this.objectPool = dependencies.objectPool;
|
|
@@ -516,7 +522,7 @@ export class BaseSyncedStore {
|
|
|
516
522
|
}
|
|
517
523
|
/**
|
|
518
524
|
* Observe the LOCAL mutation stream for undo recording (see
|
|
519
|
-
* {@link import('./
|
|
525
|
+
* {@link import('./storeContract.js').LocalMutation}). Taps the
|
|
520
526
|
* MutationQueue's `transaction:created` event — fired once per local
|
|
521
527
|
* create/update/delete/archive with `previousData` already captured.
|
|
522
528
|
* Remote/collaborator deltas apply via `applyDeltaBatchToPool` and never
|
|
@@ -563,18 +569,18 @@ export class BaseSyncedStore {
|
|
|
563
569
|
// negation false-positives every server-side bootstrap (e.g. the
|
|
564
570
|
// server-side agent.run dispatch path through `connectAgent`).
|
|
565
571
|
if (typeof navigator !== 'undefined' && navigator.onLine === false) {
|
|
566
|
-
|
|
572
|
+
this.runtime.observability.breadcrumb(`Bootstrap attempt ${attempt} skipped - offline`, 'sync.bootstrap', 'warning');
|
|
567
573
|
throw new AbloConnectionError('Bootstrap skipped - device is offline', {
|
|
568
574
|
code: 'bootstrap_offline',
|
|
569
575
|
});
|
|
570
576
|
}
|
|
571
577
|
try {
|
|
572
|
-
|
|
578
|
+
this.runtime.logger.info(`[BaseSyncedStore] Bootstrap attempt ${attempt}/${BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS}`);
|
|
573
579
|
const result = (await Promise.race([
|
|
574
580
|
bootstrapFn(),
|
|
575
581
|
this.createBootstrapTimeout(attempt),
|
|
576
582
|
]));
|
|
577
|
-
|
|
583
|
+
this.runtime.logger.info('[BaseSyncedStore] Bootstrap completed successfully', { attempt });
|
|
578
584
|
return result;
|
|
579
585
|
}
|
|
580
586
|
catch (error) {
|
|
@@ -587,12 +593,12 @@ export class BaseSyncedStore {
|
|
|
587
593
|
if (AbloSessionError.isSessionError(error))
|
|
588
594
|
throw error;
|
|
589
595
|
if (isNetworkError && typeof navigator !== 'undefined' && navigator.onLine === false) {
|
|
590
|
-
|
|
596
|
+
this.runtime.observability.captureBootstrapFailure(error, { type: 'network-offline' });
|
|
591
597
|
throw error;
|
|
592
598
|
}
|
|
593
|
-
|
|
599
|
+
this.runtime.observability.breadcrumb(`Bootstrap attempt ${attempt} failed`, 'sync.bootstrap', 'warning', { isTimeout, isNetworkError, willRetry: attempt < BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS });
|
|
594
600
|
if (isTimeout && attempt < BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS) {
|
|
595
|
-
|
|
601
|
+
this.runtime.logger.info('[BaseSyncedStore] Resetting state before bootstrap retry');
|
|
596
602
|
this.resetBootstrapState();
|
|
597
603
|
await new Promise((resolve) => setTimeout(resolve, BOOTSTRAP_CONFIG.RETRY_DELAY_MS));
|
|
598
604
|
}
|
|
@@ -666,10 +672,10 @@ export class BaseSyncedStore {
|
|
|
666
672
|
// is stale — clear it so re-entered groups backfill again.
|
|
667
673
|
this.hydratedGroups.clear();
|
|
668
674
|
this.hydratingGroups.clear();
|
|
669
|
-
|
|
675
|
+
this.runtime.logger.info('[BaseSyncedStore] Bootstrap state reset complete');
|
|
670
676
|
}
|
|
671
677
|
catch {
|
|
672
|
-
|
|
678
|
+
this.runtime.observability.breadcrumb('Error resetting bootstrap state', 'sync.bootstrap', 'warning');
|
|
673
679
|
}
|
|
674
680
|
}
|
|
675
681
|
// ── Reconnection ─────────────────────────────────────────────────────────
|
|
@@ -698,7 +704,7 @@ export class BaseSyncedStore {
|
|
|
698
704
|
return 'success';
|
|
699
705
|
}
|
|
700
706
|
catch (error) {
|
|
701
|
-
|
|
707
|
+
this.runtime.observability.captureBootstrapFailure(error, { type: 'connection-store-reconnect' });
|
|
702
708
|
if (AbloSessionError.isSessionError(error)) {
|
|
703
709
|
this.syncWebSocket.setSessionErrorDetected();
|
|
704
710
|
this.syncWebSocket.disconnect();
|
|
@@ -717,13 +723,13 @@ export class BaseSyncedStore {
|
|
|
717
723
|
const hydratedSize = this.objectPool.size;
|
|
718
724
|
if (hydratedSize > 0) {
|
|
719
725
|
this.dataReady = true;
|
|
720
|
-
|
|
726
|
+
this.runtime.logger.info('[BaseSyncedStore] Hydrated from local fallback', {
|
|
721
727
|
objectPoolSize: hydratedSize,
|
|
722
728
|
});
|
|
723
729
|
}
|
|
724
730
|
}
|
|
725
731
|
catch (fallbackError) {
|
|
726
|
-
|
|
732
|
+
this.runtime.logger.debug('[BaseSyncedStore] Local fallback failed', {
|
|
727
733
|
error: fallbackError.message,
|
|
728
734
|
});
|
|
729
735
|
}
|
|
@@ -792,6 +798,7 @@ export class BaseSyncedStore {
|
|
|
792
798
|
/** Narrow context the group-change leaf talks back through. */
|
|
793
799
|
groupChangeContext() {
|
|
794
800
|
return {
|
|
801
|
+
runtime: this.runtime,
|
|
795
802
|
database: this.database,
|
|
796
803
|
objectPool: this.objectPool,
|
|
797
804
|
getSubscribedSyncGroups: () => this.syncWebSocket.getSyncGroups(),
|
|
@@ -861,6 +868,7 @@ export class BaseSyncedStore {
|
|
|
861
868
|
poolContext() {
|
|
862
869
|
const store = this;
|
|
863
870
|
return {
|
|
871
|
+
runtime: this.runtime,
|
|
864
872
|
applyDeltaBatchToPool: (results) => {
|
|
865
873
|
this.syncClient.applyDeltaBatchToPool(results, (name, data) => this.enrichRelations(name, data));
|
|
866
874
|
},
|
|
@@ -901,8 +909,8 @@ export class BaseSyncedStore {
|
|
|
901
909
|
hasLocalData = this.objectPool.size > 0;
|
|
902
910
|
}
|
|
903
911
|
catch (hydrateError) {
|
|
904
|
-
|
|
905
|
-
|
|
912
|
+
this.runtime.logger.debug('[sync-engine] IDB hydration failed', { error: hydrateError });
|
|
913
|
+
this.runtime.observability.captureBootstrapFailure(hydrateError, { type: 'hydration-from-idb' });
|
|
906
914
|
}
|
|
907
915
|
// Get sync baseline for WebSocket
|
|
908
916
|
const lastSyncId = (yield this.database.getLastSyncId());
|
|
@@ -925,7 +933,7 @@ export class BaseSyncedStore {
|
|
|
925
933
|
// delta flow works regardless of this branch.
|
|
926
934
|
const requirements = (yield this.database.requiredBootstrap());
|
|
927
935
|
if (context.bootstrapMode === 'none') {
|
|
928
|
-
|
|
936
|
+
this.runtime.logger.info('[BaseSyncedStore] Bootstrap skipped (bootstrapMode=none)', { kind: context.kind ?? 'user' });
|
|
929
937
|
// `setupWebSocketSync` above creates the SyncWebSocket and
|
|
930
938
|
// initiates the upgrade, but it does NOT await the 'connected'
|
|
931
939
|
// event — it returns synchronously after wiring listeners.
|
|
@@ -952,7 +960,7 @@ export class BaseSyncedStore {
|
|
|
952
960
|
// delta-queue replay in withDeltaQueuing's finally (and the error
|
|
953
961
|
// handler itself), which would otherwise vanish unhandled.
|
|
954
962
|
void this.performBackgroundBootstrap(requirements, context, signal).catch((error) => {
|
|
955
|
-
|
|
963
|
+
this.runtime.observability.captureBootstrapFailure(error, {
|
|
956
964
|
type: 'background-orchestration',
|
|
957
965
|
});
|
|
958
966
|
});
|
|
@@ -984,7 +992,7 @@ export class BaseSyncedStore {
|
|
|
984
992
|
return { success: false, error: error };
|
|
985
993
|
}
|
|
986
994
|
const isSession = AbloSessionError.isSessionError(error);
|
|
987
|
-
|
|
995
|
+
this.runtime.observability.captureBootstrapFailure(error, { type: 'initialize' });
|
|
988
996
|
if (isSession) {
|
|
989
997
|
this.syncWebSocket.setSessionErrorDetected();
|
|
990
998
|
this.syncWebSocket.disconnect();
|
|
@@ -1021,11 +1029,11 @@ export class BaseSyncedStore {
|
|
|
1021
1029
|
this.updateSyncStatus({ state: 'idle', progress: 100 });
|
|
1022
1030
|
}
|
|
1023
1031
|
catch (error) {
|
|
1024
|
-
|
|
1032
|
+
this.runtime.logger.debug('[sync-engine] Background bootstrap failed', {
|
|
1025
1033
|
error: error instanceof Error ? error.message : String(error),
|
|
1026
1034
|
cause: error,
|
|
1027
1035
|
});
|
|
1028
|
-
|
|
1036
|
+
this.runtime.observability.captureBootstrapFailure(error, { type: 'background' });
|
|
1029
1037
|
if (AbloSessionError.isSessionError(error)) {
|
|
1030
1038
|
this.syncWebSocket.setSessionErrorDetected();
|
|
1031
1039
|
this.syncWebSocket.disconnect();
|
|
@@ -1210,7 +1218,7 @@ export class BaseSyncedStore {
|
|
|
1210
1218
|
return;
|
|
1211
1219
|
resolved = true;
|
|
1212
1220
|
unsubscribe();
|
|
1213
|
-
|
|
1221
|
+
this.runtime.logger.debug(`[BaseSyncedStore] waitForWebSocketConnected timed out after ${timeoutMs}ms — initialize() will return but the next mutation may race the upgrade.`);
|
|
1214
1222
|
resolve(false);
|
|
1215
1223
|
}, timeoutMs);
|
|
1216
1224
|
});
|
|
@@ -1227,7 +1235,7 @@ export class BaseSyncedStore {
|
|
|
1227
1235
|
*/
|
|
1228
1236
|
setupWebSocketSync(context, lastSyncId) {
|
|
1229
1237
|
if (!context.userId || !context.organizationId) {
|
|
1230
|
-
|
|
1238
|
+
this.runtime.observability.breadcrumb('Cannot setup WebSocket sync without user context', 'sync.websocket', 'warning');
|
|
1231
1239
|
return;
|
|
1232
1240
|
}
|
|
1233
1241
|
if (context.kind)
|
|
@@ -1280,7 +1288,7 @@ export class BaseSyncedStore {
|
|
|
1280
1288
|
this.updateSyncStatus({ state: 'offline', offlineSince: new Date() });
|
|
1281
1289
|
});
|
|
1282
1290
|
const onReconnecting = this.syncWebSocket.subscribe('reconnecting', ({ attempt, delay }) => {
|
|
1283
|
-
|
|
1291
|
+
this.runtime.logger.info('[BaseSyncedStore] WebSocket reconnecting', { attempt, delay });
|
|
1284
1292
|
this.updateSyncStatus({ state: 'reconnecting' });
|
|
1285
1293
|
});
|
|
1286
1294
|
// Delta events → feed into processing pipeline
|
|
@@ -1313,7 +1321,7 @@ export class BaseSyncedStore {
|
|
|
1313
1321
|
// Terminal session loss (revocation / the login itself is gone): notify,
|
|
1314
1322
|
// route the FSM to its terminal state, and clear local data.
|
|
1315
1323
|
const handleTerminalSessionError = (error) => {
|
|
1316
|
-
|
|
1324
|
+
this.runtime.observability.captureWebSocketError({ context: 'session-error', error: error.message });
|
|
1317
1325
|
this.onConnectionEvent?.('WS_SESSION_ERROR');
|
|
1318
1326
|
for (const listener of this.sessionErrorListeners) {
|
|
1319
1327
|
try {
|
|
@@ -1326,8 +1334,8 @@ export class BaseSyncedStore {
|
|
|
1326
1334
|
// When auth is revoked, locally cached data must not persist on disk.
|
|
1327
1335
|
this.database.clear({ includeWriteJournal: true }).catch((clearErr) => {
|
|
1328
1336
|
// consumer register: session ended, but cached data may remain on disk
|
|
1329
|
-
|
|
1330
|
-
|
|
1337
|
+
this.runtime.logger.error('Your session ended, but some locally cached data could not be cleared from this device.');
|
|
1338
|
+
this.runtime.logger.debug('[BaseSyncedStore] Failed to clear database on session error', clearErr);
|
|
1331
1339
|
});
|
|
1332
1340
|
this.objectPool.clear();
|
|
1333
1341
|
};
|
|
@@ -1342,7 +1350,7 @@ export class BaseSyncedStore {
|
|
|
1342
1350
|
// through to the terminal path. Without this branch, every credential
|
|
1343
1351
|
// TTL elapse wedged the socket behind the write-once session latch.
|
|
1344
1352
|
if (AbloSessionError.isSessionError(error) && isAccessCredentialExpiryCloseReason(error.message)) {
|
|
1345
|
-
|
|
1353
|
+
this.runtime.observability.breadcrumb('WebSocket closed for expired access credential — re-minting', 'sync.websocket', 'warning');
|
|
1346
1354
|
// Un-latch BEFORE the async mint so the FSM's own recovery
|
|
1347
1355
|
// (probe → refreshing_credential → reconnect) is never blocked on
|
|
1348
1356
|
// our .then() ordering.
|
|
@@ -1390,8 +1398,8 @@ export class BaseSyncedStore {
|
|
|
1390
1398
|
});
|
|
1391
1399
|
const onReconnectFailed = this.syncWebSocket.subscribe('reconnect_failed', ({ attempts }) => {
|
|
1392
1400
|
// consumer register: reconnection exhausted — the app is now offline
|
|
1393
|
-
|
|
1394
|
-
|
|
1401
|
+
this.runtime.logger.warn('Lost connection to the sync service and could not reconnect. Your app is now offline; changes will sync once the connection is restored.');
|
|
1402
|
+
this.runtime.logger.debug('[BaseSyncedStore] WebSocket reconnection gave up', { attempts });
|
|
1395
1403
|
this.updateSyncStatus({ state: 'reconnecting' });
|
|
1396
1404
|
});
|
|
1397
1405
|
this.disposers.push(onConnected, onDisconnected, onReconnecting, onDelta, onDeltaBatch, onBootstrapRequired, onBootstrapData, onPresenceUpdate, onError, onSessionError, onHandshakeFailed, onReconnectFailed, () => { this.areaOfInterest.dispose(); });
|
|
@@ -1513,6 +1521,8 @@ export class BaseSyncedStore {
|
|
|
1513
1521
|
return this._deltaPipelineContext;
|
|
1514
1522
|
const store = this;
|
|
1515
1523
|
this._deltaPipelineContext = {
|
|
1524
|
+
runtime: this.runtime,
|
|
1525
|
+
stagePlugins: this.stagePlugins,
|
|
1516
1526
|
// Shared pipeline state, backed by the host fields.
|
|
1517
1527
|
get pendingDeltas() { return store.pendingDeltas; },
|
|
1518
1528
|
set pendingDeltas(deltas) { store.pendingDeltas = deltas; },
|
|
@@ -1530,9 +1540,7 @@ export class BaseSyncedStore {
|
|
|
1530
1540
|
advancePersisted: (syncId) => { this.syncClient.position.advancePersisted(syncId); },
|
|
1531
1541
|
// Persistence + pool writes.
|
|
1532
1542
|
processDeltaBatch: (deltas) => this.database.processDeltaBatch(deltas),
|
|
1533
|
-
applyDeltaBatchToPool: (results) => {
|
|
1534
|
-
this.syncClient.applyDeltaBatchToPool(results, (name, data) => this.enrichRelations(name, data));
|
|
1535
|
-
},
|
|
1543
|
+
applyDeltaBatchToPool: (results) => { this.applyChangesToPool(results); },
|
|
1536
1544
|
acknowledge: (syncId) => { this.syncWebSocket.acknowledge(syncId); },
|
|
1537
1545
|
get objectPool() { return store.objectPool; },
|
|
1538
1546
|
// Dynamic-dispatch hooks — protected override points on this class.
|
|
@@ -1551,6 +1559,14 @@ export class BaseSyncedStore {
|
|
|
1551
1559
|
};
|
|
1552
1560
|
return this._deltaPipelineContext;
|
|
1553
1561
|
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Lands persisted changes in the in-memory pool, with this store's
|
|
1564
|
+
* relation enrichment bound. The one apply path: the pipeline's bridge
|
|
1565
|
+
* (no plugins installed) and the `humans()` apply handler both call it.
|
|
1566
|
+
*/
|
|
1567
|
+
applyChangesToPool(changes) {
|
|
1568
|
+
this.syncClient.applyDeltaBatchToPool(changes, (name, data) => this.enrichRelations(name, data));
|
|
1569
|
+
}
|
|
1554
1570
|
/** Get fields that represent meaningful state for deduplication. Override for model-specific fields. */
|
|
1555
1571
|
getStateFields(_modelName) {
|
|
1556
1572
|
return ['status', 'state', 'isActive'];
|
|
@@ -1663,7 +1679,7 @@ export class BaseSyncedStore {
|
|
|
1663
1679
|
}
|
|
1664
1680
|
}
|
|
1665
1681
|
if (totalCancelled > 0) {
|
|
1666
|
-
|
|
1682
|
+
this.runtime.logger.info('[BaseSyncedStore] Cascade cancelled orphaned transactions', {
|
|
1667
1683
|
parentModel: parentModelName,
|
|
1668
1684
|
parentId: parentId.slice(0, 12),
|
|
1669
1685
|
totalCancelled,
|
|
@@ -1843,13 +1859,13 @@ export class BaseSyncedStore {
|
|
|
1843
1859
|
}
|
|
1844
1860
|
/** Error handler for fire-and-forget flushPendingDeltas calls */
|
|
1845
1861
|
handleFlushError = (error) => {
|
|
1846
|
-
|
|
1862
|
+
this.runtime.observability.captureMutationFailure({
|
|
1847
1863
|
context: 'flush-pending-deltas',
|
|
1848
1864
|
modelName: 'batch',
|
|
1849
1865
|
modelId: 'batch',
|
|
1850
1866
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
1851
1867
|
});
|
|
1852
|
-
|
|
1868
|
+
this.runtime.logger.debug('[BaseSyncedStore] Delta flush error', {
|
|
1853
1869
|
error: error instanceof Error ? error.message : String(error),
|
|
1854
1870
|
});
|
|
1855
1871
|
};
|
package/dist/Database.d.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* server. {@link BaseSyncedStore} drives it, and {@link InstanceCache} holds the
|
|
6
6
|
* in-memory mirror of what this class persists.
|
|
7
7
|
*/
|
|
8
|
-
import { type DatabaseInfo, type WorkspaceMetadata } from './
|
|
8
|
+
import { type DatabaseInfo, type WorkspaceMetadata } from './stores/DatabaseManager.js';
|
|
9
9
|
import { ModelRegistry } from './ModelRegistry.js';
|
|
10
10
|
import { LoadStrategy } from './transaction/types/index.js';
|
|
11
|
+
import type { RuntimeContext } from './RuntimeContext.js';
|
|
12
|
+
import type { AppliedChange } from './transaction/plugin.js';
|
|
11
13
|
import type { BootstrapFetcher, BootstrapData } from './sync/BootstrapFetcher.js';
|
|
12
14
|
import { InMemoryObjectStore } from './adapters/inMemoryStorage.js';
|
|
13
15
|
import type { SyncDeltaAction } from './transaction/wire/delta.js';
|
|
@@ -68,12 +70,7 @@ export interface BootstrapResult {
|
|
|
68
70
|
* was disconnected — without this, DELETE deltas persist to IDB but
|
|
69
71
|
* ghost entities linger in the pool until a full reload.
|
|
70
72
|
*/
|
|
71
|
-
deltaResults?:
|
|
72
|
-
action: 'add' | 'update' | 'remove' | 'archive' | 'verify';
|
|
73
|
-
modelName: string;
|
|
74
|
-
modelId: string;
|
|
75
|
-
data?: ModelData | null;
|
|
76
|
-
}[];
|
|
73
|
+
deltaResults?: AppliedChange[];
|
|
77
74
|
}
|
|
78
75
|
export declare class Database {
|
|
79
76
|
private databaseManager;
|
|
@@ -115,12 +112,14 @@ export declare class Database {
|
|
|
115
112
|
* new Database(registry, bootstrap, { inMemory: true })
|
|
116
113
|
*/
|
|
117
114
|
private readonly inMemory;
|
|
115
|
+
private readonly runtime;
|
|
118
116
|
/** In-memory stores used when inMemory=true. Keyed by model name. */
|
|
119
117
|
private inMemoryStores;
|
|
120
118
|
/** In-memory workspace metadata when inMemory=true. */
|
|
121
119
|
private inMemoryMetadata;
|
|
122
120
|
constructor(modelRegistry: ModelRegistry, bootstrapHelper: BootstrapFetcher, options?: {
|
|
123
121
|
inMemory?: boolean;
|
|
122
|
+
runtime?: RuntimeContext;
|
|
124
123
|
});
|
|
125
124
|
/**
|
|
126
125
|
* Get store for a model, or `undefined` if no store exists.
|
|
@@ -196,12 +195,7 @@ export declare class Database {
|
|
|
196
195
|
modelName: string;
|
|
197
196
|
modelId: string;
|
|
198
197
|
data: ModelData | null;
|
|
199
|
-
}): Promise<
|
|
200
|
-
action: 'add' | 'update' | 'remove' | 'archive' | 'verify';
|
|
201
|
-
modelName: string;
|
|
202
|
-
modelId: string;
|
|
203
|
-
data?: ModelData | null;
|
|
204
|
-
}>;
|
|
198
|
+
}): Promise<AppliedChange>;
|
|
205
199
|
/**
|
|
206
200
|
* Apply many deltas to the local store in as few IndexedDB transactions as
|
|
207
201
|
* possible. Deltas are grouped by store, and each store's writes commit in a
|
|
@@ -240,13 +234,7 @@ export declare class Database {
|
|
|
240
234
|
*/
|
|
241
235
|
transactionId?: string;
|
|
242
236
|
}[]): Promise<{
|
|
243
|
-
results:
|
|
244
|
-
action: 'add' | 'update' | 'remove' | 'archive' | 'verify';
|
|
245
|
-
modelName: string;
|
|
246
|
-
modelId: string;
|
|
247
|
-
data?: ModelData | null;
|
|
248
|
-
transactionId?: string;
|
|
249
|
-
}[];
|
|
237
|
+
results: AppliedChange[];
|
|
250
238
|
/**
|
|
251
239
|
* Highest syncId whose IDB store transaction actually committed in this
|
|
252
240
|
* batch. The runtime delta cursor (WS `lastSyncId`, server-side
|