@abloatai/ablo 0.3.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 +208 -0
- package/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +230 -0
- package/dist/BaseSyncedStore.d.ts +709 -0
- package/dist/BaseSyncedStore.js +1843 -0
- package/dist/Database.d.ts +344 -0
- package/dist/Database.js +1259 -0
- package/dist/LazyReferenceCollection.d.ts +181 -0
- package/dist/LazyReferenceCollection.js +460 -0
- package/dist/Model.d.ts +339 -0
- package/dist/Model.js +715 -0
- package/dist/ModelRegistry.d.ts +200 -0
- package/dist/ModelRegistry.js +535 -0
- package/dist/NetworkMonitor.d.ts +27 -0
- package/dist/NetworkMonitor.js +73 -0
- package/dist/ObjectPool.d.ts +202 -0
- package/dist/ObjectPool.js +1106 -0
- package/dist/SyncClient.d.ts +489 -0
- package/dist/SyncClient.js +1555 -0
- package/dist/SyncEngineContext.d.ts +46 -0
- package/dist/SyncEngineContext.js +74 -0
- package/dist/adapters/alwaysOnline.d.ts +16 -0
- package/dist/adapters/alwaysOnline.js +19 -0
- package/dist/adapters/inMemoryStorage.d.ts +30 -0
- package/dist/adapters/inMemoryStorage.js +94 -0
- package/dist/agent/Agent.d.ts +358 -0
- package/dist/agent/Agent.js +500 -0
- package/dist/agent/index.d.ts +115 -0
- package/dist/agent/index.js +128 -0
- package/dist/agent/session.d.ts +90 -0
- package/dist/agent/session.js +156 -0
- package/dist/agent/types.d.ts +73 -0
- package/dist/agent/types.js +10 -0
- package/dist/ai-sdk/coordination-context.d.ts +51 -0
- package/dist/ai-sdk/coordination-context.js +107 -0
- package/dist/ai-sdk/index.d.ts +68 -0
- package/dist/ai-sdk/index.js +68 -0
- package/dist/ai-sdk/intent-broadcast.d.ts +77 -0
- package/dist/ai-sdk/intent-broadcast.js +72 -0
- package/dist/ai-sdk/wrap.d.ts +67 -0
- package/dist/ai-sdk/wrap.js +45 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +9 -0
- package/dist/auth/index.d.ts +137 -0
- package/dist/auth/index.js +246 -0
- package/dist/client/Ablo.d.ts +835 -0
- package/dist/client/Ablo.js +1440 -0
- package/dist/client/ApiClient.d.ts +200 -0
- package/dist/client/ApiClient.js +659 -0
- package/dist/client/auth.d.ts +79 -0
- package/dist/client/auth.js +81 -0
- package/dist/client/createInternalComponents.d.ts +44 -0
- package/dist/client/createInternalComponents.js +88 -0
- package/dist/client/createModelProxy.d.ts +152 -0
- package/dist/client/createModelProxy.js +199 -0
- package/dist/client/identity.d.ts +63 -0
- package/dist/client/identity.js +156 -0
- package/dist/client/index.d.ts +36 -0
- package/dist/client/index.js +33 -0
- package/dist/client/persistence.d.ts +7 -0
- package/dist/client/persistence.js +11 -0
- package/dist/client/validateAbloOptions.d.ts +42 -0
- package/dist/client/validateAbloOptions.js +43 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +12 -0
- package/dist/context.d.ts +27 -0
- package/dist/context.js +58 -0
- package/dist/core/DatabaseManager.d.ts +108 -0
- package/dist/core/DatabaseManager.js +361 -0
- package/dist/core/QueryProcessor.d.ts +77 -0
- package/dist/core/QueryProcessor.js +262 -0
- package/dist/core/QueryView.d.ts +64 -0
- package/dist/core/QueryView.js +219 -0
- package/dist/core/StoreManager.d.ts +131 -0
- package/dist/core/StoreManager.js +334 -0
- package/dist/core/ViewRegistry.d.ts +20 -0
- package/dist/core/ViewRegistry.js +55 -0
- package/dist/core/index.d.ts +34 -0
- package/dist/core/index.js +59 -0
- package/dist/core/openIDBWithTimeout.d.ts +27 -0
- package/dist/core/openIDBWithTimeout.js +63 -0
- package/dist/core/query-utils.d.ts +37 -0
- package/dist/core/query-utils.js +60 -0
- package/dist/errors.d.ts +235 -0
- package/dist/errors.js +243 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +82 -0
- package/dist/interfaces/headless.d.ts +95 -0
- package/dist/interfaces/headless.js +41 -0
- package/dist/interfaces/index.d.ts +321 -0
- package/dist/interfaces/index.js +8 -0
- package/dist/mutators/RecordingTransaction.d.ts +36 -0
- package/dist/mutators/RecordingTransaction.js +216 -0
- package/dist/mutators/Transaction.d.ts +48 -0
- package/dist/mutators/Transaction.js +64 -0
- package/dist/mutators/UndoManager.d.ts +114 -0
- package/dist/mutators/UndoManager.js +143 -0
- package/dist/mutators/defineMutators.d.ts +55 -0
- package/dist/mutators/defineMutators.js +28 -0
- package/dist/policy/index.d.ts +19 -0
- package/dist/policy/index.js +18 -0
- package/dist/policy/types.d.ts +74 -0
- package/dist/policy/types.js +17 -0
- package/dist/principal.d.ts +44 -0
- package/dist/principal.js +49 -0
- package/dist/query/client.d.ts +43 -0
- package/dist/query/client.js +84 -0
- package/dist/query/index.d.ts +6 -0
- package/dist/query/index.js +5 -0
- package/dist/query/types.d.ts +143 -0
- package/dist/query/types.js +36 -0
- package/dist/react/AbloProvider.d.ts +205 -0
- package/dist/react/AbloProvider.js +398 -0
- package/dist/react/ClientSideSuspense.d.ts +36 -0
- package/dist/react/ClientSideSuspense.js +17 -0
- package/dist/react/DefaultFallback.d.ts +24 -0
- package/dist/react/DefaultFallback.js +43 -0
- package/dist/react/SyncGroupProvider.d.ts +19 -0
- package/dist/react/SyncGroupProvider.js +44 -0
- package/dist/react/context.d.ts +161 -0
- package/dist/react/context.js +35 -0
- package/dist/react/index.d.ts +64 -0
- package/dist/react/index.js +73 -0
- package/dist/react/internalContext.d.ts +35 -0
- package/dist/react/internalContext.js +3 -0
- package/dist/react/useAblo.d.ts +72 -0
- package/dist/react/useAblo.js +63 -0
- package/dist/react/useCurrentUserId.d.ts +21 -0
- package/dist/react/useCurrentUserId.js +33 -0
- package/dist/react/useErrorListener.d.ts +20 -0
- package/dist/react/useErrorListener.js +39 -0
- package/dist/react/useIntent.d.ts +29 -0
- package/dist/react/useIntent.js +42 -0
- package/dist/react/useMutate.d.ts +83 -0
- package/dist/react/useMutate.js +122 -0
- package/dist/react/useMutationFailureListener.d.ts +26 -0
- package/dist/react/useMutationFailureListener.js +38 -0
- package/dist/react/useMutators.d.ts +56 -0
- package/dist/react/useMutators.js +66 -0
- package/dist/react/usePresence.d.ts +32 -0
- package/dist/react/usePresence.js +41 -0
- package/dist/react/useQuery.d.ts +123 -0
- package/dist/react/useQuery.js +145 -0
- package/dist/react/useReactive.d.ts +35 -0
- package/dist/react/useReactive.js +111 -0
- package/dist/react/useReader.d.ts +69 -0
- package/dist/react/useReader.js +73 -0
- package/dist/react/useSyncStatus.d.ts +61 -0
- package/dist/react/useSyncStatus.js +76 -0
- package/dist/react/useUndoScope.d.ts +36 -0
- package/dist/react/useUndoScope.js +73 -0
- package/dist/realtime/index.d.ts +10 -0
- package/dist/realtime/index.js +9 -0
- package/dist/schema/field.d.ts +134 -0
- package/dist/schema/field.js +264 -0
- package/dist/schema/index.d.ts +29 -0
- package/dist/schema/index.js +38 -0
- package/dist/schema/model.d.ts +326 -0
- package/dist/schema/model.js +89 -0
- package/dist/schema/queries.d.ts +203 -0
- package/dist/schema/queries.js +145 -0
- package/dist/schema/relation.d.ts +172 -0
- package/dist/schema/relation.js +104 -0
- package/dist/schema/schema.d.ts +259 -0
- package/dist/schema/schema.js +188 -0
- package/dist/schema/sugar.d.ts +129 -0
- package/dist/schema/sugar.js +94 -0
- package/dist/source/index.d.ts +423 -0
- package/dist/source/index.js +320 -0
- package/dist/source/pushQueue.d.ts +112 -0
- package/dist/source/pushQueue.js +249 -0
- package/dist/stores/ObjectStore.d.ts +103 -0
- package/dist/stores/ObjectStore.js +371 -0
- package/dist/stores/ObjectStoreContract.d.ts +39 -0
- package/dist/stores/ObjectStoreContract.js +1 -0
- package/dist/stores/SyncActionStore.d.ts +101 -0
- package/dist/stores/SyncActionStore.js +481 -0
- package/dist/sync/BootstrapHelper.d.ts +127 -0
- package/dist/sync/BootstrapHelper.js +434 -0
- package/dist/sync/ConnectionManager.d.ts +136 -0
- package/dist/sync/ConnectionManager.js +465 -0
- package/dist/sync/HydrationCoordinator.d.ts +137 -0
- package/dist/sync/HydrationCoordinator.js +468 -0
- package/dist/sync/NetworkProbe.d.ts +43 -0
- package/dist/sync/NetworkProbe.js +113 -0
- package/dist/sync/OfflineFlush.d.ts +9 -0
- package/dist/sync/OfflineFlush.js +22 -0
- package/dist/sync/OfflineTransactionStore.d.ts +37 -0
- package/dist/sync/OfflineTransactionStore.js +263 -0
- package/dist/sync/SyncWebSocket.d.ts +663 -0
- package/dist/sync/SyncWebSocket.js +1336 -0
- package/dist/sync/createIntentStream.d.ts +33 -0
- package/dist/sync/createIntentStream.js +243 -0
- package/dist/sync/createPresenceStream.d.ts +46 -0
- package/dist/sync/createPresenceStream.js +192 -0
- package/dist/sync/createSnapshot.d.ts +33 -0
- package/dist/sync/createSnapshot.js +124 -0
- package/dist/sync/participants.d.ts +114 -0
- package/dist/sync/participants.js +336 -0
- package/dist/sync/schemas.d.ts +79 -0
- package/dist/sync/schemas.js +78 -0
- package/dist/testing/fixtures/bootstrap.d.ts +45 -0
- package/dist/testing/fixtures/bootstrap.js +53 -0
- package/dist/testing/fixtures/deltas.d.ts +86 -0
- package/dist/testing/fixtures/deltas.js +139 -0
- package/dist/testing/fixtures/models.d.ts +82 -0
- package/dist/testing/fixtures/models.js +270 -0
- package/dist/testing/helpers/react-wrapper.d.ts +66 -0
- package/dist/testing/helpers/react-wrapper.js +64 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +55 -0
- package/dist/testing/helpers/sync-engine-harness.js +70 -0
- package/dist/testing/helpers/wait.d.ts +25 -0
- package/dist/testing/helpers/wait.js +44 -0
- package/dist/testing/index.d.ts +21 -0
- package/dist/testing/index.js +32 -0
- package/dist/testing/mocks/MockMutationExecutor.d.ts +65 -0
- package/dist/testing/mocks/MockMutationExecutor.js +139 -0
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +20 -0
- package/dist/testing/mocks/MockNetworkMonitor.js +46 -0
- package/dist/testing/mocks/MockSyncContext.d.ts +64 -0
- package/dist/testing/mocks/MockSyncContext.js +100 -0
- package/dist/testing/mocks/MockSyncStore.d.ts +88 -0
- package/dist/testing/mocks/MockSyncStore.js +171 -0
- package/dist/testing/mocks/MockWebSocket.d.ts +66 -0
- package/dist/testing/mocks/MockWebSocket.js +117 -0
- package/dist/transactions/OptimisticEchoTracker.d.ts +82 -0
- package/dist/transactions/OptimisticEchoTracker.js +104 -0
- package/dist/transactions/TransactionQueue.d.ts +499 -0
- package/dist/transactions/TransactionQueue.js +1895 -0
- package/dist/transactions/index.d.ts +16 -0
- package/dist/transactions/index.js +7 -0
- package/dist/transactions/mutation-error-handler.d.ts +5 -0
- package/dist/transactions/mutation-error-handler.js +39 -0
- package/dist/types/global.d.ts +107 -0
- package/dist/types/global.js +38 -0
- package/dist/types/index.d.ts +241 -0
- package/dist/types/index.js +70 -0
- package/dist/types/streams.d.ts +495 -0
- package/dist/types/streams.js +11 -0
- package/dist/utils/asyncIterator.d.ts +41 -0
- package/dist/utils/asyncIterator.js +142 -0
- package/dist/utils/duration.d.ts +28 -0
- package/dist/utils/duration.js +47 -0
- package/dist/utils/mobx-setup.d.ts +42 -0
- package/dist/utils/mobx-setup.js +381 -0
- package/docs/api-keys.md +24 -0
- package/docs/api.md +230 -0
- package/docs/audit.md +81 -0
- package/docs/capabilities.md +163 -0
- package/docs/client-behavior.md +202 -0
- package/docs/data-sources.md +214 -0
- package/docs/examples/agent-human.md +84 -0
- package/docs/examples/ai-sdk-tool.md +92 -0
- package/docs/examples/existing-python-backend.md +249 -0
- package/docs/examples/nextjs.md +88 -0
- package/docs/examples/server-agent.md +86 -0
- package/docs/guarantees.md +148 -0
- package/docs/index.md +97 -0
- package/docs/integration-guide.md +493 -0
- package/docs/interaction-model.md +140 -0
- package/docs/mcp/claude-code.md +43 -0
- package/docs/mcp/cursor.md +53 -0
- package/docs/mcp/windsurf.md +46 -0
- package/docs/mcp.md +59 -0
- package/docs/quickstart.md +152 -0
- package/docs/react.md +115 -0
- package/docs/roadmap.md +45 -0
- package/examples/README.md +54 -0
- package/examples/data-source/README.md +102 -0
- package/examples/data-source/ablo-driver.ts +89 -0
- package/examples/data-source/customer-server.ts +208 -0
- package/examples/data-source/run.ts +101 -0
- package/examples/data-source/schema.ts +25 -0
- package/examples/quickstart.ts +54 -0
- package/examples/tsconfig.json +16 -0
- package/llms.txt +143 -0
- package/package.json +147 -0
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseSyncedStore — Generic sync store base class for the SDK.
|
|
3
|
+
*
|
|
4
|
+
* Exports the core types, interfaces, and a base class that app-specific
|
|
5
|
+
* stores extend. The base class provides query/mutation/delta/bootstrap
|
|
6
|
+
* orchestration. Subclasses add domain-specific lazy-loading, collaboration
|
|
7
|
+
* events, and model enrichment.
|
|
8
|
+
*
|
|
9
|
+
* Design: The app's SyncedStore extends this and adds its own methods.
|
|
10
|
+
* This file only contains types and the abstract contract — the actual
|
|
11
|
+
* implementation stays in the app's SyncedStore.ts until we incrementally
|
|
12
|
+
* pull generic methods into this base class.
|
|
13
|
+
*/
|
|
14
|
+
import { ConnectionManager } from './sync/ConnectionManager.js';
|
|
15
|
+
import type { SyncClient } from './SyncClient.js';
|
|
16
|
+
import type { Database, BootstrapResult } from './Database.js';
|
|
17
|
+
import type { ObjectPool } from './ObjectPool.js';
|
|
18
|
+
import { ModelRegistry } from './ModelRegistry.js';
|
|
19
|
+
import { SyncWebSocket, type SyncDelta, type SyncGroupChangePayload, type GroupAddedPayload, type GroupRemovedPayload, type VersionVector, type BootstrapHint, type BootstrapDataEvent, type PresenceUpdateEvent, type EventMap, type DefaultCollaborationEvents } from './sync/SyncWebSocket.js';
|
|
20
|
+
import { QueryProcessor } from './core/QueryProcessor.js';
|
|
21
|
+
import { Model } from './Model.js';
|
|
22
|
+
import { ModelScope } from './ObjectPool.js';
|
|
23
|
+
import type { Schema } from './schema/schema.js';
|
|
24
|
+
import { type ReaderActions } from './react/useReader.js';
|
|
25
|
+
/** Constructor type for Model subclasses (accepts abstract classes) */
|
|
26
|
+
export type ModelConstructor<T extends Model> = abstract new (...args: never[]) => T;
|
|
27
|
+
/** Concrete constructor type for instantiation */
|
|
28
|
+
export type ConcreteModelConstructor<T extends Model> = new (data?: any) => T;
|
|
29
|
+
/** Generic record type for model data */
|
|
30
|
+
export type ModelData = Record<string, unknown>;
|
|
31
|
+
/** Query result interface */
|
|
32
|
+
export interface QueryResult<T extends Model> {
|
|
33
|
+
data: T[];
|
|
34
|
+
total: number;
|
|
35
|
+
hasMore: boolean;
|
|
36
|
+
fromCache?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** A foreign-key index to register on the ObjectPool at construction time. */
|
|
39
|
+
export interface ForeignKeyIndexSpec {
|
|
40
|
+
/**
|
|
41
|
+
* The child model name (where the FK field lives) — this is the type
|
|
42
|
+
* that will be passed to `pool.registerForeignKey(modelName, fieldName)`
|
|
43
|
+
* and later to `pool.getByForeignKey(modelName, fieldName, value)`.
|
|
44
|
+
*
|
|
45
|
+
* Use the wire `__typename` casing (e.g., `'SlideLayer'`, not
|
|
46
|
+
* `'slideLayer'`) — that's the value `createFromData` stamps onto
|
|
47
|
+
* models and the pool indexes by.
|
|
48
|
+
*/
|
|
49
|
+
readonly modelName: string;
|
|
50
|
+
/** The FK field name on the child model, e.g. `'slideId'`. */
|
|
51
|
+
readonly fieldName: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A declarative enrichment rule for the delta-apply path.
|
|
55
|
+
*
|
|
56
|
+
* When a delta for `modelName` arrives, after the model is constructed
|
|
57
|
+
* the base store reads `data[foreignKey]` from the payload, looks up
|
|
58
|
+
* the matching parent in the ObjectPool, and attaches it as
|
|
59
|
+
* `data[relationKey]`. Best-effort: if the parent isn't yet in the
|
|
60
|
+
* pool (e.g., arrived later in the same bootstrap batch), enrichment
|
|
61
|
+
* silently no-ops.
|
|
62
|
+
*
|
|
63
|
+
* Replaces the previous pattern of overriding `enrichRelations` on a
|
|
64
|
+
* subclass to hardcode per-model enrichment logic.
|
|
65
|
+
*/
|
|
66
|
+
export interface EnrichmentPlanEntry {
|
|
67
|
+
/** The child model whose incoming deltas should be enriched. */
|
|
68
|
+
readonly modelName: string;
|
|
69
|
+
/** The FK field on the child that points at the parent's id. */
|
|
70
|
+
readonly foreignKey: string;
|
|
71
|
+
/** The property name under which to attach the parent model. */
|
|
72
|
+
readonly relationKey: string;
|
|
73
|
+
}
|
|
74
|
+
/** Configuration for SyncedStore behavior */
|
|
75
|
+
export interface SyncedStoreConfig {
|
|
76
|
+
enableOffline?: boolean;
|
|
77
|
+
enableCache?: boolean;
|
|
78
|
+
enableTelemetry?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Initial version vector keys, each seeded to 0. Merged with the
|
|
81
|
+
* schema-derived set (if a schema is provided to the constructor) —
|
|
82
|
+
* explicit keys here layer on top of derived ones. Replaces the
|
|
83
|
+
* subclass pattern of hardcoding `this.versionVector = { tasks: 0, ... }`
|
|
84
|
+
* in the constructor.
|
|
85
|
+
*/
|
|
86
|
+
versionVectorKeys?: readonly string[];
|
|
87
|
+
/**
|
|
88
|
+
* Declarative enrichment plan consumed by `enrichRelations`. Replaces
|
|
89
|
+
* the subclass override of `enrichRelations` for per-model parent
|
|
90
|
+
* attachment. Merged with schema-derived entries (relations marked
|
|
91
|
+
* `{ enrich: true }` on `belongsTo`).
|
|
92
|
+
*/
|
|
93
|
+
enrichmentPlan?: readonly EnrichmentPlanEntry[];
|
|
94
|
+
/**
|
|
95
|
+
* Foreign-key indexes to register on the ObjectPool at construction
|
|
96
|
+
* time. Replaces the subclass override of `registerForeignKeys` for
|
|
97
|
+
* per-model FK registration. Merged with schema-derived entries
|
|
98
|
+
* (relations marked `{ index: true }` on `belongsTo`). Both sets
|
|
99
|
+
* are registered before the legacy `registerForeignKeys()` hook
|
|
100
|
+
* fires, so subclasses can still add more on top.
|
|
101
|
+
*/
|
|
102
|
+
foreignKeyIndexes?: readonly ForeignKeyIndexSpec[];
|
|
103
|
+
}
|
|
104
|
+
/** Sync status for UI binding */
|
|
105
|
+
export interface SyncStatus {
|
|
106
|
+
state: 'idle' | 'syncing' | 'error' | 'offline' | 'reconnecting';
|
|
107
|
+
progress: number;
|
|
108
|
+
error?: Error;
|
|
109
|
+
/** When true, the error is a session/auth error requiring re-authentication. */
|
|
110
|
+
isSessionError: boolean;
|
|
111
|
+
lastSyncAt?: Date;
|
|
112
|
+
pendingChanges: number;
|
|
113
|
+
offlineSince?: Date;
|
|
114
|
+
}
|
|
115
|
+
/** User context for initialization */
|
|
116
|
+
export interface UserContext {
|
|
117
|
+
userId: string;
|
|
118
|
+
organizationId: string;
|
|
119
|
+
role?: string;
|
|
120
|
+
teamIds?: string[];
|
|
121
|
+
/** Participant kind on the wire. Default 'user' for browser
|
|
122
|
+
* sessions; 'agent' for headless bots / worker processes. The
|
|
123
|
+
* store routes this to SyncWebSocket so the WS URL carries
|
|
124
|
+
* `kind=agent` and the server applies capability-token auth. */
|
|
125
|
+
kind?: 'user' | 'agent' | 'system';
|
|
126
|
+
/** Biscuit capability token for `kind: 'agent'`. Sent as
|
|
127
|
+
* `?authorization=Bearer <token>` on the WS upgrade. */
|
|
128
|
+
capabilityToken?: string;
|
|
129
|
+
/** Server-authoritative sync groups, supplied by auth/capability
|
|
130
|
+
* exchange. The SDK does not invent org/user/default groups; app
|
|
131
|
+
* structure comes from schema-declared scopes and server-issued
|
|
132
|
+
* authorization. */
|
|
133
|
+
syncGroups?: readonly string[];
|
|
134
|
+
/**
|
|
135
|
+
* How aggressively this participant should pull baseline state at
|
|
136
|
+
* startup.
|
|
137
|
+
*
|
|
138
|
+
* - `'full'` (default): pull every delta in scope before `ready()`
|
|
139
|
+
* resolves. The standard browser/user replica behavior.
|
|
140
|
+
* - `'none'`: open the WebSocket and process live deltas only.
|
|
141
|
+
* Reads go through `resource.retrieve()` / filtered subscriptions
|
|
142
|
+
* backfilled by `Covering` deltas. Suitable for transactional
|
|
143
|
+
* participants — agent-worker, video-pipeline, routine runners —
|
|
144
|
+
* that don't need a local replica of the org's tenant plane.
|
|
145
|
+
*/
|
|
146
|
+
bootstrapMode?: 'full' | 'none';
|
|
147
|
+
}
|
|
148
|
+
/** Smart sync options */
|
|
149
|
+
export interface SmartSyncOptions {
|
|
150
|
+
maxDeltasBeforeBootstrap?: number;
|
|
151
|
+
maxBootstrapSize?: number;
|
|
152
|
+
batchingDelay?: number;
|
|
153
|
+
maxBatchSize?: number;
|
|
154
|
+
}
|
|
155
|
+
/** Rehydration statistics from bootstrap */
|
|
156
|
+
export interface RehydrationStats {
|
|
157
|
+
added: number;
|
|
158
|
+
updated: number;
|
|
159
|
+
removed: number;
|
|
160
|
+
skipped: number;
|
|
161
|
+
healed: number;
|
|
162
|
+
elapsedMs: number;
|
|
163
|
+
}
|
|
164
|
+
/** Bootstrap timeout configuration */
|
|
165
|
+
export declare const BOOTSTRAP_CONFIG: {
|
|
166
|
+
readonly OVERALL_TIMEOUT_MS: 15000;
|
|
167
|
+
readonly MAX_RETRY_ATTEMPTS: 3;
|
|
168
|
+
readonly RETRY_DELAY_MS: 500;
|
|
169
|
+
};
|
|
170
|
+
export { ModelScope };
|
|
171
|
+
export type { SyncDelta, SyncGroupChangePayload, GroupAddedPayload, GroupRemovedPayload, VersionVector, BootstrapHint, BootstrapDataEvent, PresenceUpdateEvent, };
|
|
172
|
+
/**
|
|
173
|
+
* BaseSyncedStore — abstract base for app-specific sync stores.
|
|
174
|
+
*
|
|
175
|
+
* Provides the dependency structure, observable status, and protected
|
|
176
|
+
* accessors that subclasses use. The actual sync orchestration (initialize,
|
|
177
|
+
* delta processing, bootstrap, query, save, delete, etc.) lives in the
|
|
178
|
+
* app's concrete subclass for now — methods will be pulled up into this
|
|
179
|
+
* base class incrementally as they are genericized.
|
|
180
|
+
*
|
|
181
|
+
* Subclasses MUST call `super(dependencies, config)` and then set up
|
|
182
|
+
* their own MobX observables.
|
|
183
|
+
*
|
|
184
|
+
* Generic over `TCollaboration` — an app-defined event map for real-time
|
|
185
|
+
* collaboration events (cursors, selections, presence beyond the core set).
|
|
186
|
+
* Subclasses pass their own event map to get typed `subscribe()` calls on
|
|
187
|
+
* the underlying SyncWebSocket without casts:
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* interface AbloEvents {
|
|
191
|
+
* 'sheet:selection': [SheetSelectionEvent];
|
|
192
|
+
* 'slide:cursor': [SlideCursorEvent];
|
|
193
|
+
* }
|
|
194
|
+
* class SyncedStore extends BaseSyncedStore<AbloEvents> {
|
|
195
|
+
* subscribeToSlideCursor(handler: (e: SlideCursorEvent) => void) {
|
|
196
|
+
* return this.syncWebSocket?.subscribe('slide:cursor', handler);
|
|
197
|
+
* }
|
|
198
|
+
* }
|
|
199
|
+
*/
|
|
200
|
+
/**
|
|
201
|
+
* Walk a schema and derive the three sync-plan arrays consumed by
|
|
202
|
+
* `BaseSyncedStore`'s constructor: version-vector keys, FK indexes to
|
|
203
|
+
* register on the pool, and the enrichment plan.
|
|
204
|
+
*
|
|
205
|
+
* Version vector keys are derived from each model's `typename` (lowercased
|
|
206
|
+
* to match the server's event-type convention — `'Task'` → `'task'`,
|
|
207
|
+
* `'SlideLayer'` → `'slidelayer'`). A fallback to the schema key applies
|
|
208
|
+
* when `typename` is unset, though `defineSchema()` now always resolves
|
|
209
|
+
* it during assembly so the fallback is defensive-only.
|
|
210
|
+
*
|
|
211
|
+
* FK indexes and enrichment entries are pulled from each `belongsTo`
|
|
212
|
+
* relation where `options.index` / `options.enrich` is set. Relations
|
|
213
|
+
* without those options are skipped — this is an opt-in mechanism so
|
|
214
|
+
* adding a `belongsTo` never silently changes delta or lookup semantics.
|
|
215
|
+
*
|
|
216
|
+
* Pure function: takes a Schema, returns three arrays. No side effects,
|
|
217
|
+
* no class state. Called once at construction time from `BaseSyncedStore`.
|
|
218
|
+
*/
|
|
219
|
+
export declare function deriveSyncPlanFromSchema(schema: Schema): {
|
|
220
|
+
versionVectorKeys: string[];
|
|
221
|
+
enrichmentPlan: EnrichmentPlanEntry[];
|
|
222
|
+
foreignKeyIndexes: ForeignKeyIndexSpec[];
|
|
223
|
+
};
|
|
224
|
+
/**
|
|
225
|
+
* Schema-derived accessor namespace exposed on `store.query`. Each key is
|
|
226
|
+
* a model name from the schema and resolves to a `ReaderActions<S, K>`
|
|
227
|
+
* with `findById` / `findMany` / `findFirst` / `count`. Return types are
|
|
228
|
+
* inferred from the schema (`InferModel<S, K>`) so callers don't need to
|
|
229
|
+
* cast or pass class constructors.
|
|
230
|
+
*
|
|
231
|
+
* Prisma / Drizzle / Zero all use this shape: `store.query.<modelKey>.*`.
|
|
232
|
+
*/
|
|
233
|
+
export type QueryNamespace<S extends Schema> = {
|
|
234
|
+
readonly [K in keyof S['models'] & string]: ReaderActions<S, K>;
|
|
235
|
+
};
|
|
236
|
+
export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaboration> = DefaultCollaborationEvents, TSchema extends Schema = Schema> {
|
|
237
|
+
syncStatus: SyncStatus;
|
|
238
|
+
protected readonly syncClient: SyncClient;
|
|
239
|
+
protected readonly database: Database;
|
|
240
|
+
protected readonly objectPool: ObjectPool;
|
|
241
|
+
protected readonly modelRegistry: ModelRegistry;
|
|
242
|
+
/**
|
|
243
|
+
* Schema the store was constructed with. Persisted so the `query`
|
|
244
|
+
* accessor namespace can build typed per-model reader actions lazily
|
|
245
|
+
* without callers having to pass the schema at every lookup site.
|
|
246
|
+
*/
|
|
247
|
+
protected readonly schema?: TSchema;
|
|
248
|
+
/** Lazily-built `query.<modelKey>.*` accessor namespace. */
|
|
249
|
+
private _queryProxy?;
|
|
250
|
+
protected syncWebSocket: SyncWebSocket<TCollaboration> | null;
|
|
251
|
+
private _syncServerUrl?;
|
|
252
|
+
/**
|
|
253
|
+
* Public accessor for the underlying SyncWebSocket. Used by the
|
|
254
|
+
* factory in `createSyncEngine` to wire the default mutation
|
|
255
|
+
* executor — the executor needs the WS handle to send commit
|
|
256
|
+
* frames, and the factory can't reach `protected` state through
|
|
257
|
+
* normal typing. Returns null until WS is initialized during
|
|
258
|
+
* `initialize()`.
|
|
259
|
+
*/
|
|
260
|
+
getSyncWebSocket(): SyncWebSocket<TCollaboration> | null;
|
|
261
|
+
protected readonly queryProcessor: QueryProcessor;
|
|
262
|
+
/**
|
|
263
|
+
* Runtime behavior flags only — the three schema/config arrays
|
|
264
|
+
* (`versionVectorKeys`, `enrichmentPlan`, `foreignKeyIndexes`) are
|
|
265
|
+
* consumed at construction time and stored on the instance as
|
|
266
|
+
* `versionVector`, `enrichmentPlan`, and pool-registered indexes.
|
|
267
|
+
* They don't need to persist on `this.config`.
|
|
268
|
+
*/
|
|
269
|
+
protected readonly config: Required<Pick<SyncedStoreConfig, 'enableOffline' | 'enableCache' | 'enableTelemetry'>>;
|
|
270
|
+
protected disposers: Array<() => void>;
|
|
271
|
+
protected initialized: boolean;
|
|
272
|
+
protected dataReady: boolean;
|
|
273
|
+
protected userContext: UserContext | null;
|
|
274
|
+
protected versionVector: VersionVector;
|
|
275
|
+
/**
|
|
276
|
+
* Declarative enrichment plan: "for model X, when a delta arrives,
|
|
277
|
+
* read data[foreignKey] and attach the matching parent from the pool
|
|
278
|
+
* as data[relationKey]." Merged from schema-derived + config at
|
|
279
|
+
* construction time. Replaces the `enrichRelations` subclass override
|
|
280
|
+
* pattern.
|
|
281
|
+
*/
|
|
282
|
+
protected enrichmentPlan: readonly EnrichmentPlanEntry[];
|
|
283
|
+
protected smartSyncOptions: Required<SmartSyncOptions>;
|
|
284
|
+
protected pendingDeltas: SyncDelta[];
|
|
285
|
+
protected batchTimer: ReturnType<typeof setTimeout> | null;
|
|
286
|
+
protected syncPromise: Promise<void> | null;
|
|
287
|
+
protected lastAckedId: number;
|
|
288
|
+
protected highestProcessedSyncId: number;
|
|
289
|
+
protected bootstrapDeltaQueue: SyncDelta[] | null;
|
|
290
|
+
protected activeBootstrapCount: number;
|
|
291
|
+
protected pendingDeletes: Set<string>;
|
|
292
|
+
protected modelTypesHydrated: Set<string>;
|
|
293
|
+
protected modelTypeHydrationInFlight: Map<string, Promise<void>>;
|
|
294
|
+
constructor(dependencies: {
|
|
295
|
+
syncClient: SyncClient;
|
|
296
|
+
database: Database;
|
|
297
|
+
objectPool: ObjectPool;
|
|
298
|
+
modelRegistry: ModelRegistry;
|
|
299
|
+
/**
|
|
300
|
+
* Optional schema. When provided, `deriveSyncPlanFromSchema` walks
|
|
301
|
+
* the schema's models + relations to auto-populate version vector
|
|
302
|
+
* keys, FK indexes, and the enrichment plan from declarative
|
|
303
|
+
* annotations. Class-based subclass users (like Ablo's legacy
|
|
304
|
+
* SyncedStore) typically pass explicit `config.versionVectorKeys`
|
|
305
|
+
* / `config.foreignKeyIndexes` / `config.enrichmentPlan` instead.
|
|
306
|
+
*/
|
|
307
|
+
schema?: TSchema;
|
|
308
|
+
/** Sync server URL for WebSocket connection. Converted to wss:// automatically. */
|
|
309
|
+
url?: string;
|
|
310
|
+
}, config?: SyncedStoreConfig);
|
|
311
|
+
/**
|
|
312
|
+
* Register foreign key indexes for O(1) lookups.
|
|
313
|
+
*
|
|
314
|
+
* Legacy override hook — in Phase 2 the preferred way to declare FK
|
|
315
|
+
* indexes is via `config.foreignKeyIndexes` at construction time, or
|
|
316
|
+
* by marking the `belongsTo` relation with `{ index: true }` in the
|
|
317
|
+
* schema. This hook still fires AFTER the schema-derived + config
|
|
318
|
+
* registrations, so subclasses can layer additional FKs on top.
|
|
319
|
+
*/
|
|
320
|
+
protected registerForeignKeys(): void;
|
|
321
|
+
/**
|
|
322
|
+
* Enrich delta data with related models from the ObjectPool.
|
|
323
|
+
*
|
|
324
|
+
* Base implementation walks `this.enrichmentPlan` — entries populated
|
|
325
|
+
* from the schema's `{ enrich: true }` relations and from
|
|
326
|
+
* `config.enrichmentPlan`. Subclasses can still override for bespoke
|
|
327
|
+
* logic, calling `super.enrichRelations(modelName, data)` first to
|
|
328
|
+
* apply the declarative plan before layering on custom work.
|
|
329
|
+
*
|
|
330
|
+
* Enrichment is best-effort: if the parent isn't yet in the pool
|
|
331
|
+
* (e.g., a child delta arrives before its parent in a bootstrap
|
|
332
|
+
* batch), the entry is silently skipped and the data passes through
|
|
333
|
+
* untouched. The next delta for the same child will re-enrich.
|
|
334
|
+
*/
|
|
335
|
+
protected enrichRelations(modelName: string, data: ModelData): ModelData;
|
|
336
|
+
/** Check if a model name represents a custom/dynamic entity type. */
|
|
337
|
+
protected isCustomEntity(modelName: string): boolean;
|
|
338
|
+
/** Create a custom entity instance from delta data. Override for domain-specific custom entities. */
|
|
339
|
+
protected createCustomEntity(_modelName: string, _modelId: string, _data: Record<string, unknown>): Model | null;
|
|
340
|
+
/** Called before save for domain-specific validation/self-healing. */
|
|
341
|
+
protected beforeSave(_model: Model): void;
|
|
342
|
+
/** Connection lifecycle event callback — set by subclass to wire connection state machine. */
|
|
343
|
+
protected onConnectionEvent?: (event: string) => void;
|
|
344
|
+
/**
|
|
345
|
+
* Internal connection FSM. Owns network probe + backoff + reconnect
|
|
346
|
+
* orchestration for the default path. Constructed lazily once we
|
|
347
|
+
* have a user context + a WebSocket (see `wireWebSocketEvents`);
|
|
348
|
+
* driven by the `onConnectionEvent` hook AND browser online/offline
|
|
349
|
+
* events it sets up itself.
|
|
350
|
+
*
|
|
351
|
+
* Every consumer gets production-grade offline-to-online recovery
|
|
352
|
+
* out of the box. Subclasses that want their own lifecycle owner
|
|
353
|
+
* can disable this by overriding `createConnectionManager()` to
|
|
354
|
+
* return null.
|
|
355
|
+
*/
|
|
356
|
+
protected connectionManager: import('./sync/ConnectionManager.js').ConnectionManager | null;
|
|
357
|
+
/**
|
|
358
|
+
* Listeners registered via `subscribeSessionError()`. Fired when the
|
|
359
|
+
* WebSocket closes with a session-invalid code (1008/4001/4003) or a
|
|
360
|
+
* session-error event is received. Separate from `onConnectionEvent`
|
|
361
|
+
* (which exists for the ConnectionStore FSM) so multiple consumers —
|
|
362
|
+
* typically `<AbloProvider>` and a connection-lifecycle owner — can
|
|
363
|
+
* both react without racing on the single-callback slot.
|
|
364
|
+
*/
|
|
365
|
+
protected sessionErrorListeners: Set<(error: Error) => void>;
|
|
366
|
+
/**
|
|
367
|
+
* Subscribe to session-error events. The returned function removes
|
|
368
|
+
* the listener. Safe to call multiple times from different consumers
|
|
369
|
+
* (each gets its own slot in the listener set).
|
|
370
|
+
*/
|
|
371
|
+
subscribeSessionError(listener: (error: Error) => void): () => void;
|
|
372
|
+
/**
|
|
373
|
+
* Subscribe to per-mutation failure payloads. Forwarded from the
|
|
374
|
+
* underlying `SyncClient.transactionQueue` so consumers (toast layer,
|
|
375
|
+
* route-level reverted boundaries, telemetry) can react without
|
|
376
|
+
* reaching across the store. Returns an unsubscribe function.
|
|
377
|
+
*
|
|
378
|
+
* Why this lives on the base store rather than SyncClient: the React
|
|
379
|
+
* `<AbloProvider>` binds against this surface, so adding it here
|
|
380
|
+
* keeps the engine's internal wiring private while still giving the
|
|
381
|
+
* SDK a single hook to expose. Mirrors `subscribeSessionError` —
|
|
382
|
+
* same shape, same lifecycle.
|
|
383
|
+
*/
|
|
384
|
+
subscribeMutationFailure(listener: (payload: {
|
|
385
|
+
transaction: import('./transactions/TransactionQueue.js').Transaction;
|
|
386
|
+
error: Error;
|
|
387
|
+
permanent?: boolean;
|
|
388
|
+
}) => void): () => void;
|
|
389
|
+
/**
|
|
390
|
+
* Wait for the in-flight transaction for (modelName, modelId) to be
|
|
391
|
+
* confirmed by the server. See `SyncClient.waitForConfirmation` for the
|
|
392
|
+
* lookup contract; resolves immediately if nothing is in flight.
|
|
393
|
+
*/
|
|
394
|
+
waitForConfirmation(modelName: string, modelId: string): Promise<void>;
|
|
395
|
+
/**
|
|
396
|
+
* Execute a bootstrap function with timeout protection and automatic retry.
|
|
397
|
+
* Prevents the common issue where bootstrap hangs on startup.
|
|
398
|
+
*/
|
|
399
|
+
protected executeBootstrapWithTimeout<T>(bootstrapFn: () => Promise<T>, _context: UserContext, signal?: AbortSignal): Promise<T>;
|
|
400
|
+
/** Create a timeout promise for bootstrap attempts */
|
|
401
|
+
protected createBootstrapTimeout(attempt: number): Promise<never>;
|
|
402
|
+
/** Reset bootstrap-related state for a clean retry */
|
|
403
|
+
protected resetBootstrapState(): void;
|
|
404
|
+
/** Perform reconnect: bootstrap + WS reconnect. Returns outcome for state machine. */
|
|
405
|
+
performReconnect(): Promise<'success' | 'session_error' | 'network_error'>;
|
|
406
|
+
/**
|
|
407
|
+
* Handle an actionType 'G' delta.
|
|
408
|
+
*
|
|
409
|
+
* The server emits 'G' via two distinct pathways, distinguished by payload
|
|
410
|
+
* shape:
|
|
411
|
+
*
|
|
412
|
+
* Incremental (EmitGroupAdded): { group, userId }
|
|
413
|
+
* - The recipient was added to a single sync group.
|
|
414
|
+
* - Subsequent 'C' (Covering) deltas deliver each newly-visible entity.
|
|
415
|
+
* - No re-bootstrap — entities arrive via the normal insert path.
|
|
416
|
+
*
|
|
417
|
+
* Legacy (EmitGroupChange): { addedGroups, removedGroups }
|
|
418
|
+
* - Single delta carrying the full group membership diff.
|
|
419
|
+
* - Forces a full re-bootstrap (disconnect + reconnect + fetch all).
|
|
420
|
+
* - Deprecated on the server; kept here for wire-level backward compat.
|
|
421
|
+
*/
|
|
422
|
+
protected handleSyncGroupChange(delta: SyncDelta): Promise<void>;
|
|
423
|
+
/**
|
|
424
|
+
* Handle an incremental GroupAdded delta.
|
|
425
|
+
*
|
|
426
|
+
* Adds the new group to the subscription metadata without triggering a
|
|
427
|
+
* re-bootstrap. The server will follow up with 'C' (Covering) deltas for
|
|
428
|
+
* each newly-visible entity, which flow through the normal insert path.
|
|
429
|
+
*/
|
|
430
|
+
protected handleGroupAdded(payload: GroupAddedPayload, syncId: number): Promise<void>;
|
|
431
|
+
/**
|
|
432
|
+
* Handle an actionType 'S' (GroupRemoved) delta.
|
|
433
|
+
*
|
|
434
|
+
* Signals that the recipient has lost access to a sync group. Because
|
|
435
|
+
* the client does not track per-entity group membership, we can't
|
|
436
|
+
* selectively purge entities belonging to that group. The safe fallback
|
|
437
|
+
* is the legacy behavior: clear local state and force a re-bootstrap
|
|
438
|
+
* with the updated group list.
|
|
439
|
+
*
|
|
440
|
+
* Future optimization: track group membership in the ObjectPool so 'S'
|
|
441
|
+
* can do a targeted purge instead of a full re-bootstrap.
|
|
442
|
+
*/
|
|
443
|
+
protected handleGroupRemoved(delta: SyncDelta): Promise<void>;
|
|
444
|
+
/** Compute new sync groups after applying additions and removals */
|
|
445
|
+
protected computeUpdatedSyncGroups(payload: SyncGroupChangePayload): string[];
|
|
446
|
+
/** Force a full re-bootstrap via connection lifecycle event.
|
|
447
|
+
*
|
|
448
|
+
* No-op for `bootstrapMode: 'none'` participants — they never pull
|
|
449
|
+
* baseline state, so a "force re-bootstrap" trigger (sync-group
|
|
450
|
+
* shrink, scope revocation) instead just flushes the local pool and
|
|
451
|
+
* relies on covering deltas to repopulate the data they actually
|
|
452
|
+
* subscribe to.
|
|
453
|
+
*/
|
|
454
|
+
protected forceFullRebootstrap(): void;
|
|
455
|
+
/**
|
|
456
|
+
* Single source of truth for the sync-group list this session is
|
|
457
|
+
* subscribed to. Server-issued (`context.syncGroups`) is authoritative.
|
|
458
|
+
* When absent, the SDK subscribes to no explicit groups. Both
|
|
459
|
+
* `checkSyncGroupShrinkage` and `setupWebSocketSync` resolve through
|
|
460
|
+
* here so the WS subscription and the security-critical shrinkage
|
|
461
|
+
* check can never disagree.
|
|
462
|
+
*/
|
|
463
|
+
protected resolveSyncGroups(context: UserContext): readonly string[];
|
|
464
|
+
/** Check if sync groups shrank since last session — force full bootstrap if so */
|
|
465
|
+
protected checkSyncGroupShrinkage(): Promise<void>;
|
|
466
|
+
/** Apply bootstrap data to the ObjectPool with ghost removal */
|
|
467
|
+
/** Apply bootstrap data to the ObjectPool. Delegates pool writes to SyncClient. */
|
|
468
|
+
protected applyBootstrapToPool(bootstrapResult: BootstrapResult, protectedIds?: ReadonlySet<string>): RehydrationStats;
|
|
469
|
+
/**
|
|
470
|
+
* Initialize the sync engine with user context.
|
|
471
|
+
* Offline-first: hydrate from IDB → show UI → bootstrap from server in background.
|
|
472
|
+
*/
|
|
473
|
+
initialize(context: UserContext, signal?: AbortSignal): Generator<Promise<unknown>, {
|
|
474
|
+
success: boolean;
|
|
475
|
+
error?: Error;
|
|
476
|
+
}, unknown>;
|
|
477
|
+
/** Background bootstrap — non-blocking, user sees cached data while this runs */
|
|
478
|
+
protected performBackgroundBootstrap(requirements: Awaited<ReturnType<typeof this.database.requiredBootstrap>>, context: UserContext, signal?: AbortSignal): Promise<void>;
|
|
479
|
+
/** Run bootstrap with delta queuing to prevent race conditions */
|
|
480
|
+
protected withDeltaQueuing<T>(fn: () => Promise<T>): Promise<T>;
|
|
481
|
+
/** Collect IDs that must survive ghost removal (added by deltas during bootstrap) */
|
|
482
|
+
protected collectDeltaProtectedIds(preBootstrapIds: ReadonlySet<string>): Set<string>;
|
|
483
|
+
/** Replay deltas queued during bootstrap */
|
|
484
|
+
protected replayQueuedDeltas(): void;
|
|
485
|
+
/**
|
|
486
|
+
* Factory for the internal `ConnectionManager`. Override to return
|
|
487
|
+
* `null` in subclasses that own their own connection lifecycle
|
|
488
|
+
* (tests, headless runners, custom FSM wrappers). Default builds a
|
|
489
|
+
* manager scoped to `_syncServerUrl` with production backoff.
|
|
490
|
+
*
|
|
491
|
+
* **Agent participants get `null`.** The FSM is wired around browser
|
|
492
|
+
* events (`visibilitychange`, `online`/`offline`, watchdog) which are
|
|
493
|
+
* meaningful for human-facing tabs and meaningless for headless agent
|
|
494
|
+
* processes. On agent hosts the FSM has no event source to drive
|
|
495
|
+
* recovery — and worse, its `offline` entry action calls
|
|
496
|
+
* `syncWebSocket.disconnect()` which sets `isManualClose=true` and
|
|
497
|
+
* cancels the reconnect that `SyncWebSocket.onclose` had just
|
|
498
|
+
* scheduled. The two recovery systems fight and the browser-only one
|
|
499
|
+
* wins by destroying the Node-compatible one's work. Returning `null`
|
|
500
|
+
* for agents leaves `SyncWebSocket`'s exponential-backoff
|
|
501
|
+
* `scheduleReconnect()` as the sole recovery path — which is correct
|
|
502
|
+
* for server-side agents whether they run on Node, Bun, Deno, or
|
|
503
|
+
* inside a Docker container with no `window`.
|
|
504
|
+
*
|
|
505
|
+
* Why gate on `kind` and not `typeof window`: env detection by global
|
|
506
|
+
* existence is fragile (SSR polyfills, jsdom, sandboxed hosts). The
|
|
507
|
+
* participant kind is the actual semantic axis — "is this a human-
|
|
508
|
+
* driven session" vs "is this a server agent". The latter never has
|
|
509
|
+
* a tab to lose focus or a network adapter to wake up.
|
|
510
|
+
*/
|
|
511
|
+
protected createConnectionManager(kind?: 'user' | 'agent' | 'system'): ConnectionManager | null;
|
|
512
|
+
/** Disconnect and clean up all resources */
|
|
513
|
+
disconnect(): Promise<void>;
|
|
514
|
+
/**
|
|
515
|
+
* Destroy every IndexedDB database owned by the sync engine.
|
|
516
|
+
*
|
|
517
|
+
* First disconnects (releases WebSocket + timers + in-memory caches),
|
|
518
|
+
* then walks `indexedDB.databases()` and deletes any database whose
|
|
519
|
+
* name starts with `ablo_` or `ablo-`. This covers:
|
|
520
|
+
* - `ablo_<hash>` workspace data DBs
|
|
521
|
+
* - `ablo_databases` meta registry
|
|
522
|
+
* - `ablo-sync` offline mutation queue
|
|
523
|
+
*
|
|
524
|
+
* Use case: session expiry (previous-user data must not persist on
|
|
525
|
+
* disk before the next sign-in races into a corrupted state) or
|
|
526
|
+
* explicit user-initiated logout.
|
|
527
|
+
*
|
|
528
|
+
* Best-effort: swallows individual delete errors. Some browsers do
|
|
529
|
+
* not support `indexedDB.databases()` — the method returns without
|
|
530
|
+
* deleting in that case, same behavior as the pre-SDK app code.
|
|
531
|
+
*/
|
|
532
|
+
purge(): Promise<void>;
|
|
533
|
+
/**
|
|
534
|
+
* Create WebSocket connection and wire all event handlers.
|
|
535
|
+
* Handles: deltas, batches, presence, bootstrap_required, errors, reconnection.
|
|
536
|
+
*/
|
|
537
|
+
/**
|
|
538
|
+
* Block until the WebSocket reports a `connected` event, or until
|
|
539
|
+
* `timeoutMs` elapses (returns false on timeout, true on connect).
|
|
540
|
+
* Used by `initialize()` for `bootstrapMode: 'none'` consumers to
|
|
541
|
+
* honor `ready()`'s "WS is connected when this resolves" contract
|
|
542
|
+
* — `setupWebSocketSync` is fire-and-forget on the upgrade, and
|
|
543
|
+
* without an explicit wait the next mutation can race the open.
|
|
544
|
+
*
|
|
545
|
+
* Resolves immediately if the WS is already connected (e.g., warm
|
|
546
|
+
* reconnect after redeploy). Resolves false on timeout rather than
|
|
547
|
+
* throwing so initialize() can complete and let the caller's first
|
|
548
|
+
* mutation attempt surface a clearer error.
|
|
549
|
+
*/
|
|
550
|
+
protected waitForWebSocketConnected(timeoutMs: number): Promise<boolean>;
|
|
551
|
+
protected setupWebSocketSync(context: UserContext, lastSyncId: number): void;
|
|
552
|
+
/** State signature for delta deduplication */
|
|
553
|
+
private extractStateSignature;
|
|
554
|
+
/** Get fields that represent meaningful state for deduplication. Override for model-specific fields. */
|
|
555
|
+
protected getStateFields(_modelName: string): string[];
|
|
556
|
+
private isSameState;
|
|
557
|
+
/** Deduplicate deltas to the same entity — keep meaningful state transitions only */
|
|
558
|
+
protected deduplicateDeltas(deltas: SyncDelta[]): SyncDelta[];
|
|
559
|
+
/** Process incoming delta with smart batching */
|
|
560
|
+
protected processDeltaWithBatching(delta: SyncDelta): void;
|
|
561
|
+
/**
|
|
562
|
+
* Cancel pending transactions for child entities when a parent is deleted.
|
|
563
|
+
*
|
|
564
|
+
* Uses `pool.getByForeignKey` (O(1) via the FK index registered at
|
|
565
|
+
* schema build time) to find children. The previous implementation did
|
|
566
|
+
* `getByType(ctor).filter(e => e.toJSON()[foreignKey] === parentId)` —
|
|
567
|
+
* a full pool scan per child model + a `toJSON()` allocation per
|
|
568
|
+
* candidate. For a deck delete with 10K layers in the pool, that was
|
|
569
|
+
* 10K toJSON allocations per cascade level. The FK-indexed lookup
|
|
570
|
+
* skips both the scan AND the allocation.
|
|
571
|
+
*/
|
|
572
|
+
protected cascadeCancelTransactionsForDeletedParent(parentModelName: string, parentId: string): void;
|
|
573
|
+
/** Flush pending deltas with deduplication and batched ObjectPool mutations */
|
|
574
|
+
/** Flush pending deltas with deduplication. Delegates pool writes to SyncClient. */
|
|
575
|
+
protected flushPendingDeltas(): Promise<void>;
|
|
576
|
+
/** Check if a model type is local-only (no sync). Override for domain-specific models. */
|
|
577
|
+
protected isLocalOnlyModel(_modelName: string): boolean;
|
|
578
|
+
/** Validate model against schema before save */
|
|
579
|
+
protected validateModel(model: Model): void;
|
|
580
|
+
/**
|
|
581
|
+
* Save a model (create or update).
|
|
582
|
+
*
|
|
583
|
+
* Accepts any entity shape with `{ id: string }` so consumers can pass the
|
|
584
|
+
* Zod-inferred model types from `InferModel<Schema, K>` without knowing
|
|
585
|
+
* about the internal `Model` base class. At runtime, every entity reaching
|
|
586
|
+
* this method came through the object pool (via `store.create`, a query
|
|
587
|
+
* accessor, or an optimistic insert) and IS a `Model` instance — the one
|
|
588
|
+
* cast below preserves that invariant inside the SDK.
|
|
589
|
+
*/
|
|
590
|
+
save<T extends {
|
|
591
|
+
id: string;
|
|
592
|
+
createdAt?: Date;
|
|
593
|
+
updatedAt?: Date;
|
|
594
|
+
}>(entity: T, options?: {
|
|
595
|
+
skipValidation?: boolean;
|
|
596
|
+
}): Promise<void>;
|
|
597
|
+
/** Save with an atomic server mutation (e.g., createSlideWithLayers) */
|
|
598
|
+
saveWithAtomicMutation(model: Model, mutation: (gql: unknown) => Promise<unknown>): Promise<void>;
|
|
599
|
+
/** Delete a model. Accepts schema-inferred entity shapes (see `save`). */
|
|
600
|
+
delete<T extends {
|
|
601
|
+
id: string;
|
|
602
|
+
}>(entity: T): Promise<void>;
|
|
603
|
+
/** Archive a model. Accepts schema-inferred entity shapes (see `save`). */
|
|
604
|
+
archive<T extends {
|
|
605
|
+
id: string;
|
|
606
|
+
archivedAt?: Date | null;
|
|
607
|
+
}>(entity: T): Promise<void>;
|
|
608
|
+
/** Unarchive a model. Accepts schema-inferred entity shapes (see `save`). */
|
|
609
|
+
unarchive<T extends {
|
|
610
|
+
id: string;
|
|
611
|
+
archivedAt?: Date | null;
|
|
612
|
+
}>(entity: T): Promise<void>;
|
|
613
|
+
/**
|
|
614
|
+
* Schema-keyed accessor namespace — the primary type-safe lookup surface.
|
|
615
|
+
*
|
|
616
|
+
* ```ts
|
|
617
|
+
* const chat = store.query.chats.retrieve(chatId); // Chat | undefined
|
|
618
|
+
* const slides = store.query.slides.findMany({ where: { deckId } }); // Slide[]
|
|
619
|
+
* ```
|
|
620
|
+
*
|
|
621
|
+
* Each `query.<modelKey>` is a `ReaderActions<Schema, K>` built lazily on
|
|
622
|
+
* first access via `createReaderActions`. The returned types are inferred
|
|
623
|
+
* from the schema (`InferModel<S, K>`), including `InferRelations` — so
|
|
624
|
+
* `chat.messages`, `slide.layers`, etc. are typed without a cast.
|
|
625
|
+
*
|
|
626
|
+
* Throws if the store was constructed without a schema (class-based
|
|
627
|
+
* subclasses that wire models via `modelRegistry.registerModel` directly
|
|
628
|
+
* don't have access to schema-derived inference).
|
|
629
|
+
*/
|
|
630
|
+
get query(): QueryNamespace<TSchema>;
|
|
631
|
+
/** Retrieve a single entity by id. Synchronous pool read. */
|
|
632
|
+
retrieve(_modelClass: ModelConstructor<Model>, id: string): Model | undefined;
|
|
633
|
+
/** Find any entity by ID regardless of type */
|
|
634
|
+
findAnyById(id: string): Model | undefined;
|
|
635
|
+
/**
|
|
636
|
+
* Lookup a model by ID alone. Matches the `SyncStoreRef.getById` contract
|
|
637
|
+
* that schema-defined computeds use when they need to resolve a related
|
|
638
|
+
* entity without holding onto its constructor.
|
|
639
|
+
*/
|
|
640
|
+
getById(id: string): Model | undefined;
|
|
641
|
+
/**
|
|
642
|
+
* Create a model instance locally, typed via the schema.
|
|
643
|
+
*
|
|
644
|
+
* ```ts
|
|
645
|
+
* const sheet = store.create('spreadsheetSheets', { name, spreadsheetId });
|
|
646
|
+
* // sheet: SpreadsheetSheet | null — no cast needed
|
|
647
|
+
* ```
|
|
648
|
+
*
|
|
649
|
+
* The `typename` arg is the schema key (camelCase plural, e.g.
|
|
650
|
+
* `'spreadsheetSheets'`); the returned instance has the
|
|
651
|
+
* `InferModel<Schema, K>` shape including computeds + relation accessors.
|
|
652
|
+
* Wraps `pool.create(...)` — the underlying runtime is unchanged, just
|
|
653
|
+
* type-narrowed.
|
|
654
|
+
*/
|
|
655
|
+
create<K extends keyof TSchema['models'] & string>(typename: K, data: Record<string, unknown>): import('./schema/schema.js').InferModel<TSchema, K> | null;
|
|
656
|
+
/**
|
|
657
|
+
* Legacy class-based query entry point — kept for callers that still pass
|
|
658
|
+
* a Model constructor + options object. New code should use the typed
|
|
659
|
+
* `store.query.<modelKey>` namespace instead, which returns properly
|
|
660
|
+
* inferred schema types without needing a class value or cast.
|
|
661
|
+
*/
|
|
662
|
+
queryByClass(modelClass: ModelConstructor<Model>, options?: {
|
|
663
|
+
predicate?: (model: Model) => boolean;
|
|
664
|
+
scope?: ModelScope;
|
|
665
|
+
orderBy?: keyof Model;
|
|
666
|
+
order?: 'asc' | 'desc';
|
|
667
|
+
limit?: number;
|
|
668
|
+
offset?: number;
|
|
669
|
+
}): QueryResult<Model>;
|
|
670
|
+
/**
|
|
671
|
+
* Get all models of a type. Returns Model[] honestly — callers that need
|
|
672
|
+
* narrow types should use `useAblo((ablo) => ablo.<model>.list(...))`
|
|
673
|
+
* which does proper inference via `InferModel<S, K>`.
|
|
674
|
+
*/
|
|
675
|
+
allModelsOfType(modelClass: ModelConstructor<Model>, scope?: ModelScope): Model[];
|
|
676
|
+
/** Error handler for fire-and-forget flushPendingDeltas calls */
|
|
677
|
+
protected handleFlushError: (error: unknown) => void;
|
|
678
|
+
/** Process a single delta (used for immediate DELETE processing). Override for domain-specific handling. */
|
|
679
|
+
protected processDelta(delta: SyncDelta): Promise<void>;
|
|
680
|
+
/** Handle bootstrap_required event */
|
|
681
|
+
protected handleBootstrapRequired(_hint: BootstrapHint): void;
|
|
682
|
+
/** Handle bootstrap_data event. Override in subclass. */
|
|
683
|
+
protected handleBootstrapData(_data: BootstrapDataEvent): void;
|
|
684
|
+
/** Handle presence_update event. Override in subclass. */
|
|
685
|
+
protected handlePresenceUpdate(_data: PresenceUpdateEvent): void;
|
|
686
|
+
protected incrementPendingChanges(): void;
|
|
687
|
+
protected decrementPendingChanges(): void;
|
|
688
|
+
protected updateSyncStatus(updates: Partial<SyncStatus>): void;
|
|
689
|
+
get pool(): ObjectPool;
|
|
690
|
+
get lastSyncId(): number;
|
|
691
|
+
get isReady(): boolean;
|
|
692
|
+
get isSyncing(): boolean;
|
|
693
|
+
get isOffline(): boolean;
|
|
694
|
+
get isReconnecting(): boolean;
|
|
695
|
+
get isError(): boolean;
|
|
696
|
+
get hasUnsyncedChanges(): boolean;
|
|
697
|
+
/** The SyncWebSocket handle — for collaboration events. */
|
|
698
|
+
get ws(): SyncWebSocket<TCollaboration> | null;
|
|
699
|
+
/** The Database instance — for demand loaders and direct IDB operations. */
|
|
700
|
+
get db(): Database;
|
|
701
|
+
/** The SyncClient instance — for assignment operations and other direct sync actions. */
|
|
702
|
+
get sc(): SyncClient;
|
|
703
|
+
/** The current organization ID — from the last initialize() call. */
|
|
704
|
+
get orgId(): string | undefined;
|
|
705
|
+
/** Count models matching a predicate. */
|
|
706
|
+
count(modelClass: ModelConstructor<Model>, predicate?: (m: Model) => boolean): number;
|
|
707
|
+
/** Get entities by foreign key (used by Model subclasses via Model.store) */
|
|
708
|
+
getByForeignKey(modelName: string, foreignKey: string, id: string): Model[];
|
|
709
|
+
}
|