@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
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The default `MutationExecutor` — sends `{ type: 'commit', ... }` frames over
|
|
3
|
+
* the engine's own WebSocket, resolved lazily at commit time (the WS doesn't
|
|
4
|
+
* exist when the executor is constructed).
|
|
5
|
+
*
|
|
6
|
+
* Extracted from `Ablo.ts`; the factory wires it up with a `getWs` closure
|
|
7
|
+
* over the store holder unless the caller supplies its own executor.
|
|
8
|
+
*/
|
|
9
|
+
import { AbloError, AbloConnectionError } from '../errors.js';
|
|
10
|
+
// ── Default mutation executor (wire: `commit` frame over WebSocket) ──────
|
|
11
|
+
/**
|
|
12
|
+
* Default mutation executor: sends `{ type: 'commit', payload: ... }` over
|
|
13
|
+
* the sync engine's own WebSocket.
|
|
14
|
+
*
|
|
15
|
+
* Transport ownership follows the Zero / Liveblocks pattern — the engine
|
|
16
|
+
* owns its socket end-to-end and the executor is internal. Apps pass URLs
|
|
17
|
+
* and auth; they do NOT inject transport callbacks. That's why this
|
|
18
|
+
* factory takes a `getWs` closure instead of a full SyncWebSocket: the WS
|
|
19
|
+
* doesn't exist when the executor is constructed (it's created later in
|
|
20
|
+
* `Ablo` during `BaseSyncedStore` init), so we resolve it
|
|
21
|
+
* lazily at commit time. Same trick Zero uses internally — see
|
|
22
|
+
* `packages/zero-client/src/client/zero.ts` where `Pusher`/`Puller` are
|
|
23
|
+
* constructed before the socket then wired up at connect time.
|
|
24
|
+
*
|
|
25
|
+
* `options.idempotencyKey` becomes the wire-level `clientTxId` when set,
|
|
26
|
+
* matching Stripe-style retry semantics. Otherwise the SDK generates one.
|
|
27
|
+
*/
|
|
28
|
+
export function createDefaultMutationExecutor(getWs) {
|
|
29
|
+
async function commit(operations, options) {
|
|
30
|
+
const ws = getWs();
|
|
31
|
+
if (!ws?.sendCommit) {
|
|
32
|
+
throw new AbloConnectionError('SyncWebSocket not ready for commit. The engine must finish bootstrap ' +
|
|
33
|
+
'before mutations can be sent.', { code: 'ws_not_ready' });
|
|
34
|
+
}
|
|
35
|
+
const clientTxId = options?.idempotencyKey ??
|
|
36
|
+
(typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
37
|
+
? crypto.randomUUID()
|
|
38
|
+
: `tx_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`);
|
|
39
|
+
try {
|
|
40
|
+
return await ws.sendCommit(operations, clientTxId, undefined, // use sendCommit's built-in 15s default; no per-call override
|
|
41
|
+
options?.causedByTaskId, options?.reads);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
// Wrap transport-level failures as connection errors so the
|
|
45
|
+
// TransactionQueue's retry classifier treats them as transient
|
|
46
|
+
// (matches the old HTTP path's network-error handling).
|
|
47
|
+
if (err instanceof AbloError)
|
|
48
|
+
throw err;
|
|
49
|
+
if (err instanceof Error) {
|
|
50
|
+
if (/not connected|timed out|connection|ECONN/i.test(err.message)) {
|
|
51
|
+
const wrapped = new AbloConnectionError(err.message, { cause: err });
|
|
52
|
+
// Preserve any `diagnostics` snapshot the underlying SyncWebSocket
|
|
53
|
+
// attached to the rejection. Without this, the wrapped error
|
|
54
|
+
// bottoms out at "AbloConnectionError: not connected" with no
|
|
55
|
+
// attribution to which close code / heartbeat trip / session
|
|
56
|
+
// error caused it. See SyncWebSocket.notConnectedError().
|
|
57
|
+
if (err &&
|
|
58
|
+
typeof err === 'object' &&
|
|
59
|
+
'diagnostics' in err &&
|
|
60
|
+
err.diagnostics) {
|
|
61
|
+
wrapped.diagnostics = err.diagnostics;
|
|
62
|
+
}
|
|
63
|
+
throw wrapped;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
commit,
|
|
71
|
+
executeCreate: (model, id, input, _txId, options) => commit([{ type: 'CREATE', model: model.toLowerCase(), id, input }], options).then(() => { }),
|
|
72
|
+
executeUpdate: (model, id, data, _txId, options) => commit([{ type: 'UPDATE', model: model.toLowerCase(), id, input: data }], options),
|
|
73
|
+
executeDelete: (model, id, _txId, options) => commit([{ type: 'DELETE', model: model.toLowerCase(), id }], options).then(() => { }),
|
|
74
|
+
executeArchive: (model, id, _txId, options) => commit([{ type: 'ARCHIVE', model: model.toLowerCase(), id }], options).then(() => { }),
|
|
75
|
+
executeUnarchive: (model, id, _txId, options) => commit([{ type: 'UNARCHIVE', model: model.toLowerCase(), id }], options).then(() => { }),
|
|
76
|
+
};
|
|
77
|
+
}
|
package/dist/context.js
CHANGED
|
@@ -2,5 +2,12 @@
|
|
|
2
2
|
* `@abloatai/ablo/coordination` — the canonical wire schema for the three
|
|
3
3
|
* coordination layers (presence, pessimistic claims, optimistic stale-context).
|
|
4
4
|
* See `./schema.ts` for the model and the per-layer schemas.
|
|
5
|
+
*
|
|
6
|
+
* Explicit named list (not `export *`): every addition to schema.ts must be a
|
|
7
|
+
* deliberate export decision here, so new symbols don't silently become public
|
|
8
|
+
* API of the published package.
|
|
5
9
|
*/
|
|
6
|
-
export
|
|
10
|
+
export { targetRangeSchema, participantKindSchema, wireParticipantKindSchema, participantKindFromWire, descriptionFromMeta, targetRefSchema, onStaleModeSchema, writeGuardSchema, staleNotificationSchema, readDependencySchema, claimStatusSchema, wireClaimSummarySchema, claimErrorSchema, wireClaimSchema, claimRejectionSchema, modelTargetSchema, modelClaimSchema, claimBeginPayloadSchema, claimAbandonPayloadSchema, claimReorderPayloadSchema, updateSubscriptionPayloadSchema, subscriptionAckPayloadSchema, commitOperationTypeSchema, commitOperationSchema, presenceKindSchema, presenceActivitySchema, presenceUpdateFrameSchema, } from './schema.js';
|
|
11
|
+
export { defaultPolicy, capabilityPreemptPolicy, interpretConflictAxis } from '../policy/types.js';
|
|
12
|
+
export type { Conflict, ConflictAxis, ConflictDecision, ConflictKind, ConflictOperation, ConflictPolicy, StaleContextConflict, ClaimHeldConflict, } from '../policy/types.js';
|
|
13
|
+
export type { TargetRange, ParticipantKind, TargetRef, OnStaleMode, WriteGuard, StaleNotification, ReadDependency, ClaimStatus, WireClaimSummary, ClaimError, WireClaim, ClaimRejection, ModelTarget, ModelClaim, ClaimBeginPayload, ClaimAbandonPayload, ClaimReorderPayload, UpdateSubscriptionPayload, SubscriptionAckPayload, CommitOperationType, CommitOperation, AnyCommitOperation, PresenceKind, PresenceActivity, PresenceUpdateFrame, } from './schema.js';
|
|
@@ -2,5 +2,28 @@
|
|
|
2
2
|
* `@abloatai/ablo/coordination` — the canonical wire schema for the three
|
|
3
3
|
* coordination layers (presence, pessimistic claims, optimistic stale-context).
|
|
4
4
|
* See `./schema.ts` for the model and the per-layer schemas.
|
|
5
|
+
*
|
|
6
|
+
* Explicit named list (not `export *`): every addition to schema.ts must be a
|
|
7
|
+
* deliberate export decision here, so new symbols don't silently become public
|
|
8
|
+
* API of the published package.
|
|
5
9
|
*/
|
|
6
|
-
|
|
10
|
+
// Runtime schemas + helpers.
|
|
11
|
+
export {
|
|
12
|
+
// Shared primitives
|
|
13
|
+
targetRangeSchema, participantKindSchema, wireParticipantKindSchema, participantKindFromWire, descriptionFromMeta, targetRefSchema,
|
|
14
|
+
// Layer 3 — optimistic stale-context
|
|
15
|
+
onStaleModeSchema, writeGuardSchema, staleNotificationSchema, readDependencySchema,
|
|
16
|
+
// Layer 2 — pessimistic claim / claim-lease
|
|
17
|
+
claimStatusSchema, wireClaimSummarySchema, claimErrorSchema, wireClaimSchema, claimRejectionSchema, modelTargetSchema, modelClaimSchema, claimBeginPayloadSchema, claimAbandonPayloadSchema, claimReorderPayloadSchema,
|
|
18
|
+
// Read interest — area-of-interest navigation
|
|
19
|
+
updateSubscriptionPayloadSchema, subscriptionAckPayloadSchema,
|
|
20
|
+
// Commit operation — carries the optimistic write-guard
|
|
21
|
+
commitOperationTypeSchema, commitOperationSchema,
|
|
22
|
+
// Layer 1 — presence
|
|
23
|
+
presenceKindSchema, presenceActivitySchema, presenceUpdateFrameSchema, } from './schema.js';
|
|
24
|
+
// Conflict-policy runtime — the engine detects, the policy decides. Lives in
|
|
25
|
+
// `../policy/types.js` (a dependency-free leaf: its only imports are types),
|
|
26
|
+
// exported here so a server-side consumer reaches the coordination vocabulary
|
|
27
|
+
// AND its default resolution through one leaf subpath instead of the root
|
|
28
|
+
// barrel (which would evaluate the whole browser client stack in Node).
|
|
29
|
+
export { defaultPolicy, capabilityPreemptPolicy, interpretConflictAxis } from '../policy/types.js';
|
|
@@ -83,7 +83,6 @@ export declare class ClaimLog implements SyncObservabilityProvider {
|
|
|
83
83
|
captureReconciliation(): void;
|
|
84
84
|
captureDeltaRetryExhausted(): void;
|
|
85
85
|
captureWebSocketError(): void;
|
|
86
|
-
captureOfflineFlushFailure(): void;
|
|
87
86
|
captureSelfHealing(): void;
|
|
88
87
|
captureCommitZeroSyncId(): void;
|
|
89
88
|
startSpan<T>(_name: string, _op: string, fn: () => T): T;
|
|
@@ -135,7 +135,6 @@ export class ClaimLog {
|
|
|
135
135
|
captureReconciliation() { }
|
|
136
136
|
captureDeltaRetryExhausted() { }
|
|
137
137
|
captureWebSocketError() { }
|
|
138
|
-
captureOfflineFlushFailure() { }
|
|
139
138
|
captureSelfHealing() { }
|
|
140
139
|
captureCommitZeroSyncId() { }
|
|
141
140
|
startSpan(_name, _op, fn) {
|
|
@@ -107,7 +107,7 @@ export class DatabaseManager {
|
|
|
107
107
|
schemaHash,
|
|
108
108
|
schemaVersion,
|
|
109
109
|
userVersion,
|
|
110
|
-
createdAt: existingInfo?.createdAt
|
|
110
|
+
createdAt: existingInfo?.createdAt ?? new Date(),
|
|
111
111
|
updatedAt: new Date(),
|
|
112
112
|
};
|
|
113
113
|
}
|
|
@@ -143,8 +143,8 @@ export class DatabaseManager {
|
|
|
143
143
|
tx.oncomplete = () => {
|
|
144
144
|
resolve();
|
|
145
145
|
};
|
|
146
|
-
tx.onerror = () => reject(tx.error);
|
|
147
|
-
request.onerror = () => reject(request.error);
|
|
146
|
+
tx.onerror = () => { reject(tx.error); };
|
|
147
|
+
request.onerror = () => { reject(request.error); };
|
|
148
148
|
});
|
|
149
149
|
}
|
|
150
150
|
/**
|
|
@@ -157,8 +157,8 @@ export class DatabaseManager {
|
|
|
157
157
|
const tx = this.metaDb.transaction(['databases'], 'readonly');
|
|
158
158
|
const store = tx.objectStore('databases');
|
|
159
159
|
const request = store.get(name);
|
|
160
|
-
request.onsuccess = () => resolve(request.result
|
|
161
|
-
request.onerror = () => reject(request.error);
|
|
160
|
+
request.onsuccess = () => { resolve(request.result ?? null); };
|
|
161
|
+
request.onerror = () => { reject(request.error); };
|
|
162
162
|
});
|
|
163
163
|
}
|
|
164
164
|
/**
|
|
@@ -172,8 +172,8 @@ export class DatabaseManager {
|
|
|
172
172
|
const store = tx.objectStore('databases');
|
|
173
173
|
const index = store.index('userId');
|
|
174
174
|
const request = index.getAll(userId);
|
|
175
|
-
request.onsuccess = () => resolve(request.result || []);
|
|
176
|
-
request.onerror = () => reject(request.error);
|
|
175
|
+
request.onsuccess = () => { resolve(request.result || []); };
|
|
176
|
+
request.onerror = () => { reject(request.error); };
|
|
177
177
|
});
|
|
178
178
|
}
|
|
179
179
|
/**
|
|
@@ -231,18 +231,20 @@ export class DatabaseManager {
|
|
|
231
231
|
return;
|
|
232
232
|
}
|
|
233
233
|
const meta = {
|
|
234
|
-
lastSyncId: data.lastSyncId
|
|
235
|
-
firstSyncId: data.firstSyncId
|
|
236
|
-
backendDatabaseVersion: data.backendDatabaseVersion
|
|
237
|
-
subscribedSyncGroups: data.subscribedSyncGroups
|
|
234
|
+
lastSyncId: data.lastSyncId ?? 0,
|
|
235
|
+
firstSyncId: data.firstSyncId ?? 0,
|
|
236
|
+
backendDatabaseVersion: data.backendDatabaseVersion ?? 1,
|
|
237
|
+
subscribedSyncGroups: data.subscribedSyncGroups ?? [],
|
|
238
238
|
updatedAt: data.updatedAt ? new Date(data.updatedAt) : new Date(),
|
|
239
239
|
schemaHash: data.schemaHash,
|
|
240
240
|
syncGroups: data.syncGroups,
|
|
241
|
-
|
|
241
|
+
// NOTE: old persisted records may still carry a `versions` key
|
|
242
|
+
// (the removed per-entity version vector) — tolerated by simply
|
|
243
|
+
// not picking it here.
|
|
242
244
|
};
|
|
243
245
|
resolve(meta);
|
|
244
246
|
};
|
|
245
|
-
request.onerror = () => reject(request.error);
|
|
247
|
+
request.onerror = () => { reject(request.error); };
|
|
246
248
|
});
|
|
247
249
|
}
|
|
248
250
|
/**
|
|
@@ -253,9 +255,9 @@ export class DatabaseManager {
|
|
|
253
255
|
const tx = db.transaction(['__meta'], 'readwrite');
|
|
254
256
|
const store = tx.objectStore('__meta');
|
|
255
257
|
const request = store.put(metadata, 'metadata');
|
|
256
|
-
tx.oncomplete = () => resolve();
|
|
257
|
-
tx.onerror = () => reject(tx.error);
|
|
258
|
-
request.onerror = () => reject(request.error);
|
|
258
|
+
tx.oncomplete = () => { resolve(); };
|
|
259
|
+
tx.onerror = () => { reject(tx.error); };
|
|
260
|
+
request.onerror = () => { reject(request.error); };
|
|
259
261
|
});
|
|
260
262
|
}
|
|
261
263
|
/**
|
|
@@ -270,7 +272,7 @@ export class DatabaseManager {
|
|
|
270
272
|
const data = request.result;
|
|
271
273
|
resolve(data?.persisted === true);
|
|
272
274
|
};
|
|
273
|
-
request.onerror = () => reject(request.error);
|
|
275
|
+
request.onerror = () => { reject(request.error); };
|
|
274
276
|
});
|
|
275
277
|
}
|
|
276
278
|
/**
|
|
@@ -287,9 +289,9 @@ export class DatabaseManager {
|
|
|
287
289
|
updatedAt: new Date().toISOString(),
|
|
288
290
|
};
|
|
289
291
|
const request = store.put(persistenceData, modelName);
|
|
290
|
-
tx.oncomplete = () => resolve();
|
|
291
|
-
tx.onerror = () => reject(tx.error);
|
|
292
|
-
request.onerror = () => reject(request.error);
|
|
292
|
+
tx.oncomplete = () => { resolve(); };
|
|
293
|
+
tx.onerror = () => { reject(tx.error); };
|
|
294
|
+
request.onerror = () => { reject(request.error); };
|
|
293
295
|
});
|
|
294
296
|
}
|
|
295
297
|
/**
|
|
@@ -312,7 +314,7 @@ export class DatabaseManager {
|
|
|
312
314
|
}
|
|
313
315
|
resolve(states);
|
|
314
316
|
};
|
|
315
|
-
request.onerror = () => reject(request.error);
|
|
317
|
+
request.onerror = () => { reject(request.error); };
|
|
316
318
|
});
|
|
317
319
|
}
|
|
318
320
|
/**
|
|
@@ -362,10 +364,10 @@ export class DatabaseManager {
|
|
|
362
364
|
const schemaVersions = {};
|
|
363
365
|
for (const db of databases) {
|
|
364
366
|
// Count by user
|
|
365
|
-
databasesByUser[db.userId] = (databasesByUser[db.userId]
|
|
367
|
+
databasesByUser[db.userId] = (databasesByUser[db.userId] ?? 0) + 1;
|
|
366
368
|
// Count schema versions
|
|
367
369
|
const versionKey = `v${db.schemaVersion}`;
|
|
368
|
-
schemaVersions[versionKey] = (schemaVersions[versionKey]
|
|
370
|
+
schemaVersions[versionKey] = (schemaVersions[versionKey] ?? 0) + 1;
|
|
369
371
|
}
|
|
370
372
|
resolve({
|
|
371
373
|
metaDatabaseSize: databases.length,
|
|
@@ -374,7 +376,7 @@ export class DatabaseManager {
|
|
|
374
376
|
schemaVersions,
|
|
375
377
|
});
|
|
376
378
|
};
|
|
377
|
-
request.onerror = () => reject(request.error);
|
|
379
|
+
request.onerror = () => { reject(request.error); };
|
|
378
380
|
});
|
|
379
381
|
}
|
|
380
382
|
/**
|
|
@@ -48,9 +48,8 @@ class BasicQueryCache {
|
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
50
|
// Fast path: Check if pattern is a simple model type match like ".*ModelType.*"
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
53
|
-
const modelType = simpleMatch[1];
|
|
51
|
+
const modelType = /^\.\*(\w+)\.\*$/.exec(pattern)?.[1];
|
|
52
|
+
if (modelType !== undefined) {
|
|
54
53
|
const keysToDelete = this.modelTypeIndex.get(modelType);
|
|
55
54
|
if (keysToDelete) {
|
|
56
55
|
for (const key of keysToDelete) {
|
|
@@ -88,8 +87,9 @@ class BasicQueryCache {
|
|
|
88
87
|
* Cache key format: "operation:ModelType:options"
|
|
89
88
|
*/
|
|
90
89
|
extractModelType(key) {
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
// `?? null` is equivalent to the old length check: a missing second
|
|
91
|
+
// segment reads as undefined, and split never yields undefined otherwise.
|
|
92
|
+
return key.split(':')[1] ?? null;
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
export class QueryProcessor {
|
|
@@ -229,9 +229,8 @@ export class QueryProcessor {
|
|
|
229
229
|
this.cache.invalidate(pattern);
|
|
230
230
|
// Also invalidate predicate result cache for this model type
|
|
231
231
|
if (pattern) {
|
|
232
|
-
const
|
|
233
|
-
if (
|
|
234
|
-
const modelType = simpleMatch[1];
|
|
232
|
+
const modelType = /^\.\*(\w+)\.\*$/.exec(pattern)?.[1];
|
|
233
|
+
if (modelType !== undefined) {
|
|
235
234
|
for (const key of this.predicateResultCache.keys()) {
|
|
236
235
|
if (key.includes(modelType)) {
|
|
237
236
|
this.predicateResultCache.delete(key);
|
package/dist/core/QueryView.d.ts
CHANGED
|
@@ -9,10 +9,22 @@
|
|
|
9
9
|
* directly and receive granular updates (splice/push, never replacement).
|
|
10
10
|
*/
|
|
11
11
|
import { type IObservableArray } from 'mobx';
|
|
12
|
+
import { type Model } from '../Model.js';
|
|
12
13
|
import { ModelScope } from '../types/index.js';
|
|
13
|
-
import type { ObjectPool } from '../ObjectPool.js';
|
|
14
14
|
import type { ViewRegistry } from './ViewRegistry.js';
|
|
15
15
|
import type { IncrementalView } from './query-utils.js';
|
|
16
|
+
/**
|
|
17
|
+
* The slice of `ObjectPool` a QueryView reads — a minimal structural
|
|
18
|
+
* interface (LogFoldContext-style) instead of the concrete class, so this
|
|
19
|
+
* module never imports `../ObjectPool.js` back (ObjectPool imports QueryView
|
|
20
|
+
* at runtime for `createView()`; the reverse edge was the last core-layer
|
|
21
|
+
* import cycle). `ObjectPool` satisfies it structurally.
|
|
22
|
+
*/
|
|
23
|
+
export interface QueryViewPool {
|
|
24
|
+
hasForeignKeyIndex(typename: string, fieldName: string): boolean;
|
|
25
|
+
getByForeignKey(modelName: string, fieldName: string, fieldValue: string): Model[];
|
|
26
|
+
getByTypeName(modelName: string, scope?: ModelScope): Model[];
|
|
27
|
+
}
|
|
16
28
|
export interface QueryViewOptions<T> {
|
|
17
29
|
where?: Partial<T>;
|
|
18
30
|
filter?: (entity: T) => boolean;
|
|
@@ -47,7 +59,7 @@ export declare class QueryView<T extends Record<string, unknown>> implements Inc
|
|
|
47
59
|
private readonly fkField;
|
|
48
60
|
private readonly fkValue;
|
|
49
61
|
private disposed;
|
|
50
|
-
constructor(typename: string, pool:
|
|
62
|
+
constructor(typename: string, pool: QueryViewPool, registry: ViewRegistry, options?: QueryViewOptions<T>);
|
|
51
63
|
private initialScan;
|
|
52
64
|
handleAdded(model: Record<string, unknown>): void;
|
|
53
65
|
handleUpdated(model: Record<string, unknown>): void;
|
package/dist/core/QueryView.js
CHANGED
|
@@ -54,10 +54,9 @@ export class QueryView {
|
|
|
54
54
|
// Check for FK-index optimization: single-field where with an indexed FK
|
|
55
55
|
this.fkField = null;
|
|
56
56
|
this.fkValue = null;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const [field, value] = this.whereEntries[0];
|
|
57
|
+
const soleWhereEntry = this.whereEntries && this.whereEntries.length === 1 ? this.whereEntries[0] : undefined;
|
|
58
|
+
if (soleWhereEntry && typeof soleWhereEntry[1] === 'string') {
|
|
59
|
+
const [field, value] = soleWhereEntry;
|
|
61
60
|
if (pool.hasForeignKeyIndex(typename, field)) {
|
|
62
61
|
this.fkField = field;
|
|
63
62
|
this.fkValue = value;
|
|
@@ -120,7 +119,7 @@ export class QueryView {
|
|
|
120
119
|
if (this.disposed)
|
|
121
120
|
return;
|
|
122
121
|
const entity = model;
|
|
123
|
-
const id = entity
|
|
122
|
+
const id = entity.id;
|
|
124
123
|
const idx = id !== undefined ? this.findIndexById(id) : -1;
|
|
125
124
|
const matchesNow = this.matchesFilter(entity);
|
|
126
125
|
runInAction(() => {
|
|
@@ -120,12 +120,12 @@ export declare class StoreManager {
|
|
|
120
120
|
notReady: number;
|
|
121
121
|
};
|
|
122
122
|
totalRecords: number;
|
|
123
|
-
storeDetails:
|
|
123
|
+
storeDetails: {
|
|
124
124
|
modelName: string;
|
|
125
125
|
storeName: string;
|
|
126
126
|
strategy: LoadStrategy;
|
|
127
127
|
ready: boolean;
|
|
128
128
|
count: number;
|
|
129
|
-
}
|
|
129
|
+
}[];
|
|
130
130
|
}>;
|
|
131
131
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Creates appropriate store types based on model load strategies.
|
|
6
6
|
* Follows Linear's architecture with 80+ ObjectStore instances.
|
|
7
7
|
*/
|
|
8
|
+
import { ModelRegistry } from '../ModelRegistry.js';
|
|
8
9
|
import { ObjectStore } from '../stores/ObjectStore.js';
|
|
9
10
|
import { SyncActionStore } from '../stores/SyncActionStore.js';
|
|
10
11
|
import { LoadStrategy } from '../types/index.js';
|
|
@@ -198,7 +199,10 @@ export class StoreManager {
|
|
|
198
199
|
// Sample a few stores — don't check all 30+ if the first one has data
|
|
199
200
|
const samplesToCheck = Math.min(storeEntries.length, 3);
|
|
200
201
|
for (let i = 0; i < samplesToCheck; i++) {
|
|
201
|
-
const
|
|
202
|
+
const entry = storeEntries[i];
|
|
203
|
+
if (!entry)
|
|
204
|
+
break;
|
|
205
|
+
const [, store] = entry;
|
|
202
206
|
try {
|
|
203
207
|
const count = await store.count();
|
|
204
208
|
if (count > 0)
|
package/dist/core/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export { LazyReferenceCollection, type LazyCollectionOptions, } from '../LazyRef
|
|
|
20
20
|
export { ModelRegistry, getActiveRegistry, } from '../ModelRegistry.js';
|
|
21
21
|
export { postQuery, type PostQueryOptions } from '../query/client.js';
|
|
22
22
|
export { computeFKDepthPriority, type InternalAbloOptions } from '../client/Ablo.js';
|
|
23
|
-
export type { SyncLogger, SyncObservabilityProvider, MutationExecutor,
|
|
23
|
+
export type { SyncLogger, SyncObservabilityProvider, MutationExecutor, SessionErrorDetector, OnlineStatusProvider, CommitResult, MutationOperation, } from '../interfaces/index.js';
|
|
24
24
|
export { SyncWebSocket, type SyncDelta, type SyncWebSocketOptions, } from '../sync/SyncWebSocket.js';
|
|
25
25
|
export { BootstrapHelper } from '../sync/BootstrapHelper.js';
|
|
26
26
|
export { createClaimStream, type AttachableClaimStream, type ClaimStreamConfig, } from '../sync/createClaimStream.js';
|
|
@@ -95,23 +95,27 @@ export function openIDBWithTimeout(name, version, options = {}) {
|
|
|
95
95
|
// A consumer reaction must never break the close.
|
|
96
96
|
}
|
|
97
97
|
};
|
|
98
|
-
settle(() => resolve(db));
|
|
98
|
+
settle(() => { resolve(db); });
|
|
99
99
|
};
|
|
100
|
-
request.onerror = () => settle(() => reject(request.error));
|
|
100
|
+
request.onerror = () => { settle(() => { reject(request.error); }); };
|
|
101
101
|
// The critical handler: another tab is blocking us. Native API leaves
|
|
102
102
|
// the request pending indefinitely; we fail fast with a clear error so
|
|
103
103
|
// the UI can tell the user to close other tabs.
|
|
104
104
|
request.onblocked = () => {
|
|
105
|
-
settle(() =>
|
|
106
|
-
`
|
|
105
|
+
settle(() => {
|
|
106
|
+
reject(new IDBOpenTimeoutError(name, 'blocked', `IndexedDB \"${name}\" open blocked — another tab is holding an ` +
|
|
107
|
+
`older version. Close other Ablo tabs and reload.`));
|
|
108
|
+
});
|
|
107
109
|
};
|
|
108
110
|
// Catch-all timeout: even without `onblocked`, some browsers in some
|
|
109
111
|
// storage states hang without firing any event. Bounded wait →
|
|
110
112
|
// deterministic error.
|
|
111
113
|
const timer = setTimeout(() => {
|
|
112
|
-
settle(() =>
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
settle(() => {
|
|
115
|
+
reject(new IDBOpenTimeoutError(name, 'timeout', `IndexedDB \"${name}\" open did not resolve within ${timeoutMs}ms. ` +
|
|
116
|
+
`Storage may be in a bad state — clearing site data and reloading ` +
|
|
117
|
+
`usually fixes this.`));
|
|
118
|
+
});
|
|
115
119
|
}, timeoutMs);
|
|
116
120
|
});
|
|
117
121
|
}
|
|
@@ -142,9 +146,9 @@ export function deleteIDBWithTimeout(name, timeoutMs = 5_000) {
|
|
|
142
146
|
resolve(value);
|
|
143
147
|
};
|
|
144
148
|
const request = indexedDB.deleteDatabase(name);
|
|
145
|
-
request.onsuccess = () => settle(true);
|
|
146
|
-
request.onerror = () => settle(false);
|
|
147
|
-
request.onblocked = () => settle(false);
|
|
148
|
-
const timer = setTimeout(() => settle(false), timeoutMs);
|
|
149
|
+
request.onsuccess = () => { settle(true); };
|
|
150
|
+
request.onerror = () => { settle(false); };
|
|
151
|
+
request.onblocked = () => { settle(false); };
|
|
152
|
+
const timer = setTimeout(() => { settle(false); }, timeoutMs);
|
|
149
153
|
});
|
|
150
154
|
}
|
package/dist/core/query-utils.js
CHANGED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* storeContract — the framework-neutral store contract.
|
|
3
|
+
*
|
|
4
|
+
* `SyncStoreContract` is the minimal store interface the SDK's hooks and
|
|
5
|
+
* mutators program against; `BaseSyncedStore` is the concrete engine class
|
|
6
|
+
* that implements it. The contract used to live in `react/context.ts`, which
|
|
7
|
+
* meant the CORE store layer imported its own contract from the React
|
|
8
|
+
* adapter (a module that runtime-imports 'react') — an L2-core →
|
|
9
|
+
* react-integration inversion and a module cycle. It now lives here, in a
|
|
10
|
+
* dependency-free core leaf: `react/context.ts` re-exports these types so
|
|
11
|
+
* React consumers are unchanged, and the core layer never touches 'react'.
|
|
12
|
+
*
|
|
13
|
+
* Everything in this module is type-only — no runtime imports, no runtime
|
|
14
|
+
* exports beyond erased interfaces.
|
|
15
|
+
*/
|
|
16
|
+
import type { Model } from '../Model.js';
|
|
17
|
+
import type { ModelScope } from '../types/index.js';
|
|
18
|
+
import type { QueryView, QueryViewOptions } from './QueryView.js';
|
|
19
|
+
import type { ViewRegistry } from './ViewRegistry.js';
|
|
20
|
+
import type { ParticipantScope } from '../sync/participants.js';
|
|
21
|
+
/** Sync status for UI binding */
|
|
22
|
+
export interface SyncStatus {
|
|
23
|
+
state: 'idle' | 'syncing' | 'error' | 'offline' | 'reconnecting';
|
|
24
|
+
progress: number;
|
|
25
|
+
error?: Error;
|
|
26
|
+
/** When true, the error is a session/auth error requiring re-authentication. */
|
|
27
|
+
isSessionError: boolean;
|
|
28
|
+
lastSyncAt?: Date;
|
|
29
|
+
pendingChanges: number;
|
|
30
|
+
offlineSince?: Date;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A single LOCAL mutation as observed off the commit stream — the substrate
|
|
34
|
+
* the undo system records from. One is emitted per local create/update/
|
|
35
|
+
* delete/archive (remote/collaborator deltas never appear here: they apply
|
|
36
|
+
* through a separate pool path that doesn't queue mutations). `previousData`
|
|
37
|
+
* holds the pre-edit field values (captured from the model's
|
|
38
|
+
* `modifiedProperties` first-old-wins baseline), so an inverse op is fully
|
|
39
|
+
* derivable from the event alone — no separate snapshot pass.
|
|
40
|
+
*
|
|
41
|
+
* This mirrors how Yjs's `UndoManager` derives reverse-ops by observing the
|
|
42
|
+
* doc and Liveblocks' `room.history` records room ops: undo listens to the
|
|
43
|
+
* one place all local writes converge, rather than wrapping the write call.
|
|
44
|
+
*/
|
|
45
|
+
export interface LocalMutation {
|
|
46
|
+
type: 'create' | 'update' | 'delete' | 'archive' | 'unarchive';
|
|
47
|
+
/** Registered model name (e.g. `'SlideLayer'`); resolved to a schema key by the recorder. */
|
|
48
|
+
modelName: string;
|
|
49
|
+
modelId: string;
|
|
50
|
+
/** New field values (create/update). */
|
|
51
|
+
data?: Record<string, unknown> | null;
|
|
52
|
+
/** Pre-edit field values (update → inverse patch; delete → full re-create row). */
|
|
53
|
+
previousData?: Record<string, unknown> | null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Minimal store interface that the SDK hooks need.
|
|
57
|
+
* Consumers provide their concrete store (e.g., SyncedStore) that implements this.
|
|
58
|
+
*/
|
|
59
|
+
export interface SyncStoreContract {
|
|
60
|
+
/**
|
|
61
|
+
* Subscribe to the LOCAL mutation stream (optimistic, pre-ack) for undo
|
|
62
|
+
* recording. Optional so minimal test doubles can omit it — when absent,
|
|
63
|
+
* undo scopes simply record nothing. The concrete store
|
|
64
|
+
* (`BaseSyncedStore`) wires this to the TransactionQueue's
|
|
65
|
+
* `transaction:created` event. Returns an unsubscribe function.
|
|
66
|
+
*/
|
|
67
|
+
subscribeLocalMutations?(handler: (mutation: LocalMutation) => void): () => void;
|
|
68
|
+
retrieve(modelClass: abstract new (...args: never[]) => Model, id: string): Model | undefined;
|
|
69
|
+
queryByClass(modelClass: abstract new (...args: never[]) => Model, options?: {
|
|
70
|
+
predicate?: (model: Model) => boolean;
|
|
71
|
+
scope?: ModelScope;
|
|
72
|
+
orderBy?: keyof Model;
|
|
73
|
+
order?: 'asc' | 'desc';
|
|
74
|
+
limit?: number;
|
|
75
|
+
offset?: number;
|
|
76
|
+
}): {
|
|
77
|
+
data: Model[];
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Save (create or update) one entity. Calling `save` in a tight loop
|
|
81
|
+
* produces a single wire commit with one `batchIndex`: the SyncClient
|
|
82
|
+
* debounces IDB persistence and the server push to one microtask, and
|
|
83
|
+
* TransactionQueue coalesces every transaction staged in the tick into
|
|
84
|
+
* one batch. There is intentionally no `saveMany` — Zero, Replicache,
|
|
85
|
+
* and the rest of the local-first lineage all expose one-row writes
|
|
86
|
+
* and rely on the implicit tick boundary.
|
|
87
|
+
*
|
|
88
|
+
* `skipValidation` exists for trusted bulk paths (AI sandbox layer
|
|
89
|
+
* generation, PPTX import, hydration) where the producer has already
|
|
90
|
+
* type-checked and per-row Zod is a measurable cost.
|
|
91
|
+
*/
|
|
92
|
+
save(model: Model, options?: {
|
|
93
|
+
skipValidation?: boolean;
|
|
94
|
+
}): Promise<void>;
|
|
95
|
+
delete(model: Model): Promise<void>;
|
|
96
|
+
archive(model: Model): Promise<void>;
|
|
97
|
+
unarchive(model: Model): Promise<void>;
|
|
98
|
+
/** The ObjectPool — for entity/collection lookups by ID or typename. */
|
|
99
|
+
pool: {
|
|
100
|
+
get(id: string): Model | undefined;
|
|
101
|
+
getByTypeName(typename: string, scope?: ModelScope): Model[];
|
|
102
|
+
getByForeignKey(modelName: string, fieldName: string, fieldValue: string): Model[];
|
|
103
|
+
createFromData(data: Record<string, unknown>): Model | null;
|
|
104
|
+
hasForeignKeyIndex(typename: string, fieldName: string): boolean;
|
|
105
|
+
createView<T extends Record<string, unknown>>(typename: string, options?: QueryViewOptions<T>): QueryView<T>;
|
|
106
|
+
viewRegistry: ViewRegistry;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Reactive sync-status getters. Powered by MobX `computed` inside
|
|
110
|
+
* `BaseSyncedStore`, so they're safe to read in `observer` components
|
|
111
|
+
* and inside `reaction(() => store.isReady, ...)`. Consumers that
|
|
112
|
+
* don't want to touch MobX should prefer the `useSyncStatus()` hook.
|
|
113
|
+
*/
|
|
114
|
+
readonly isReady: boolean;
|
|
115
|
+
readonly isSyncing: boolean;
|
|
116
|
+
readonly isOffline: boolean;
|
|
117
|
+
readonly isReconnecting: boolean;
|
|
118
|
+
readonly isError: boolean;
|
|
119
|
+
readonly hasUnsyncedChanges: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Area-of-interest (dynamic read subscription). `enterScope`/`leaveScope`
|
|
122
|
+
* move the connection's read interest as the user navigates (open/close a
|
|
123
|
+
* deck, sheet, doc); `pinScope`/`unpinScope` express prominence (an active
|
|
124
|
+
* claim keeps a group subscribed). Each resolves the scope through the same
|
|
125
|
+
* resolver the claim path uses, so read interest and write claims agree on
|
|
126
|
+
* the sync-group string. Optional so minimal test doubles can omit them;
|
|
127
|
+
* no-ops before the socket exists. The concrete store (`BaseSyncedStore`)
|
|
128
|
+
* forwards to its `AreaOfInterestManager`.
|
|
129
|
+
*/
|
|
130
|
+
enterScope?(scope: ParticipantScope, opts?: {
|
|
131
|
+
hydrate?: boolean;
|
|
132
|
+
}): Promise<void>;
|
|
133
|
+
leaveScope?(scope: ParticipantScope): Promise<void>;
|
|
134
|
+
pinScope?(scope: ParticipantScope): Promise<void>;
|
|
135
|
+
unpinScope?(scope: ParticipantScope): Promise<void>;
|
|
136
|
+
/**
|
|
137
|
+
* Raw MobX-observable `SyncStatus` record. `useSyncStatus()` reads
|
|
138
|
+
* `state`, `progress`, `pendingChanges`, `isSessionError`, `error`
|
|
139
|
+
* from this to build its tagged union. Exposed on the contract so
|
|
140
|
+
* consumer-facing hooks and test doubles can manipulate it directly.
|
|
141
|
+
*/
|
|
142
|
+
readonly syncStatus: SyncStatus;
|
|
143
|
+
}
|