@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,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound frame dispatch for the sync WebSocket.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the monolithic frame `switch` that used to live inside
|
|
5
|
+
* `SyncWebSocket.setupEventHandlers` with a frame-type → handler table of
|
|
6
|
+
* functions over a minimal {@link WsSession} interface — only the members
|
|
7
|
+
* the handlers actually touch, never the transport class itself (no
|
|
8
|
+
* import cycle). The host's `onmessage` stays responsible for JSON
|
|
9
|
+
* parsing, heartbeat proof-of-life, and the outer try/catch; everything
|
|
10
|
+
* after that funnels through {@link dispatchWsFrame}.
|
|
11
|
+
*/
|
|
12
|
+
import { getContext } from '../context.js';
|
|
13
|
+
import { CapabilityError, errorFromWire, } from '../errors.js';
|
|
14
|
+
import { subscriptionAckPayloadSchema } from '../coordination/schema.js';
|
|
15
|
+
import { formatConflict } from '../coordination/trace.js';
|
|
16
|
+
import { parseNotifications, recordClaim } from './commitFrames.js';
|
|
17
|
+
/** Narrow arbitrary wire data to a plain string-keyed record. */
|
|
18
|
+
export function isRecord(value) {
|
|
19
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Envelope guard for the raw `JSON.parse` result of an inbound WS message.
|
|
23
|
+
* A frame is any plain object whose `type`, when present, is a string —
|
|
24
|
+
* payload-level validation stays with each handler (deltas go through the
|
|
25
|
+
* canonical `clientSyncDeltaSchema` at the `normalizeWireDelta` seam).
|
|
26
|
+
*/
|
|
27
|
+
export function isWsInboundFrame(value) {
|
|
28
|
+
if (!isRecord(value))
|
|
29
|
+
return false;
|
|
30
|
+
const type = value.type;
|
|
31
|
+
return type === undefined || typeof type === 'string';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Ack for a prior `commit` we sent. Canonical shape is
|
|
35
|
+
* `MutationResultMessage` in `@abloatai/ablo/wire`. This stays a
|
|
36
|
+
* DEFENSIVE parse (not a typed cast) because the payload is
|
|
37
|
+
* untrusted wire data that may be malformed or from an older server.
|
|
38
|
+
*/
|
|
39
|
+
const handleMutationResult = (session, message) => {
|
|
40
|
+
const p = (message.payload ?? message);
|
|
41
|
+
const { clientTxId, success, lastSyncId, error } = p ?? {};
|
|
42
|
+
// Defensive: validate notifications against the canonical schema —
|
|
43
|
+
// untrusted wire data from a possibly-older/newer server.
|
|
44
|
+
const notifications = parseNotifications(p?.notifications);
|
|
45
|
+
const pending = typeof clientTxId === 'string'
|
|
46
|
+
? session.pendingMutations.get(clientTxId)
|
|
47
|
+
: undefined;
|
|
48
|
+
if (!pending)
|
|
49
|
+
return;
|
|
50
|
+
clearTimeout(pending.timeout);
|
|
51
|
+
// `pending` exists ⇒ clientTxId was a string key (the guard above).
|
|
52
|
+
session.pendingMutations.delete(clientTxId);
|
|
53
|
+
if (success) {
|
|
54
|
+
// Coerce defensively — bigint columns serialize as strings
|
|
55
|
+
// from older servers (see normalizeWireDelta).
|
|
56
|
+
const ackedSyncId = Number(lastSyncId);
|
|
57
|
+
// Notify-instead-of-abort: a guarded write's premise moved. Emit
|
|
58
|
+
// the advisory signal so an agent loop can self-heal, AND resolve
|
|
59
|
+
// the receipt with it (the commit still succeeded).
|
|
60
|
+
if (notifications && notifications.length > 0) {
|
|
61
|
+
const txId = typeof clientTxId === 'string' ? clientTxId : '';
|
|
62
|
+
const event = {
|
|
63
|
+
clientTxId: txId,
|
|
64
|
+
rows: notifications.map((n) => ({
|
|
65
|
+
model: n.model,
|
|
66
|
+
id: n.id,
|
|
67
|
+
fields: n.conflictingFields,
|
|
68
|
+
writtenBy: n.writtenBy?.kind,
|
|
69
|
+
})),
|
|
70
|
+
};
|
|
71
|
+
const message = formatConflict(event);
|
|
72
|
+
const ctx = getContext();
|
|
73
|
+
ctx.logger.warn(message);
|
|
74
|
+
ctx.observability.breadcrumb(message, 'sync.coordination', 'warning');
|
|
75
|
+
ctx.observability.captureConflict(event);
|
|
76
|
+
session.emit('conflict:notified', {
|
|
77
|
+
clientTxId: txId,
|
|
78
|
+
notifications,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
pending.resolve({
|
|
82
|
+
lastSyncId: Number.isFinite(ackedSyncId) ? ackedSyncId : 0,
|
|
83
|
+
...(notifications && notifications.length > 0
|
|
84
|
+
? { notifications }
|
|
85
|
+
: {}),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// Capture the FULL server error so the user can see what
|
|
90
|
+
// actually rejected the mutation. Without this, every
|
|
91
|
+
// rejection becomes the generic "mutation failed on
|
|
92
|
+
// server" — useless when debugging chart batches that
|
|
93
|
+
// tank 40+ ops at once. We stringify object errors so
|
|
94
|
+
// structured server payloads (e.g., Zod issues, schema
|
|
95
|
+
// violations) survive the trip through `new Error(...)`.
|
|
96
|
+
let errorMessage;
|
|
97
|
+
let errorCode;
|
|
98
|
+
let requiredCapability;
|
|
99
|
+
if (typeof error === 'string') {
|
|
100
|
+
errorMessage = error;
|
|
101
|
+
}
|
|
102
|
+
else if (error != null && typeof error === 'object') {
|
|
103
|
+
const obj = error;
|
|
104
|
+
if (typeof obj.code === 'string')
|
|
105
|
+
errorCode = obj.code;
|
|
106
|
+
if (typeof obj.message === 'string') {
|
|
107
|
+
errorMessage = obj.message;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
try {
|
|
111
|
+
errorMessage = JSON.stringify(error);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
errorMessage = String(error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (obj.requiredCapability != null &&
|
|
118
|
+
typeof obj.requiredCapability === 'object' &&
|
|
119
|
+
typeof obj.requiredCapability.scope === 'string') {
|
|
120
|
+
requiredCapability = obj.requiredCapability;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
errorMessage = 'mutation failed on server';
|
|
125
|
+
}
|
|
126
|
+
// Coordination collision: a stale-context rejection (the write's
|
|
127
|
+
// readAt premise moved underneath) or a foreign-claim conflict is
|
|
128
|
+
// exactly the collision ClaimLog exists to surface. The notify
|
|
129
|
+
// path (success + notifications) emits captureConflict above; a
|
|
130
|
+
// HARD rejection must too — otherwise observability.collisions()
|
|
131
|
+
// silently misses every rejected write. The conflicted rows ride
|
|
132
|
+
// along on the typed error's `conflicts` detail (see
|
|
133
|
+
// AbloStaleContextError.toJSON / errorEnvelope).
|
|
134
|
+
if (errorCode === 'stale_context' ||
|
|
135
|
+
errorCode === 'claim_conflict' ||
|
|
136
|
+
errorCode === 'entity_claimed' ||
|
|
137
|
+
errorCode?.startsWith('policy:') === true) {
|
|
138
|
+
const rawConflicts = error != null &&
|
|
139
|
+
typeof error === 'object' &&
|
|
140
|
+
Array.isArray(error.conflicts)
|
|
141
|
+
? error
|
|
142
|
+
.conflicts
|
|
143
|
+
: [];
|
|
144
|
+
const conflictEvent = {
|
|
145
|
+
clientTxId: typeof clientTxId === 'string' ? clientTxId : '',
|
|
146
|
+
rows: rawConflicts.map((r) => ({
|
|
147
|
+
model: typeof r.model === 'string' ? r.model : 'unknown',
|
|
148
|
+
id: typeof r.id === 'string' ? r.id : 'unknown',
|
|
149
|
+
fields: [],
|
|
150
|
+
})),
|
|
151
|
+
};
|
|
152
|
+
const ctx = getContext();
|
|
153
|
+
ctx.observability.breadcrumb(formatConflict(conflictEvent), 'sync.coordination', 'warning');
|
|
154
|
+
ctx.observability.captureConflict(conflictEvent);
|
|
155
|
+
}
|
|
156
|
+
// Build the proper typed AbloError from the wire code via the
|
|
157
|
+
// shared factory — the same code→class mapping the HTTP commit
|
|
158
|
+
// path uses (`translateHttpError`). This keeps rejected commits
|
|
159
|
+
// inside the typed hierarchy (capability denials →
|
|
160
|
+
// CapabilityError with `.requiredCapability`; foreign-claim
|
|
161
|
+
// conflicts → AbloClaimedError; everything else → the subclass
|
|
162
|
+
// its registry `httpStatus` implies) instead of a hand-rolled
|
|
163
|
+
// `new Error`, so callers can `instanceof`/`e.type` it and
|
|
164
|
+
// downstream retry logic can read the contract's retryability.
|
|
165
|
+
pending.reject(errorFromWire(errorMessage, {
|
|
166
|
+
code: errorCode,
|
|
167
|
+
requiredCapability,
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Ack for a prior `claim` we sent. Wire format mirrors
|
|
173
|
+
* apps/sync-server/src/hub/types.ts ClaimAckMessage:
|
|
174
|
+
* { type: 'claim_ack',
|
|
175
|
+
* payload: { claimId, success, syncGroups?,
|
|
176
|
+
* ttlSeconds?, error? } }
|
|
177
|
+
*/
|
|
178
|
+
const handleClaimAck = (session, message) => {
|
|
179
|
+
const p = (message.payload ?? {});
|
|
180
|
+
const { claimId, success, syncGroups, ttlSeconds, error } = p;
|
|
181
|
+
const pending = typeof claimId === 'string'
|
|
182
|
+
? session.pendingClaims.get(claimId)
|
|
183
|
+
: undefined;
|
|
184
|
+
if (!pending)
|
|
185
|
+
return;
|
|
186
|
+
clearTimeout(pending.timeout);
|
|
187
|
+
// `pending` exists ⇒ claimId was a string key (the guard above).
|
|
188
|
+
session.pendingClaims.delete(claimId);
|
|
189
|
+
if (success) {
|
|
190
|
+
pending.resolve({
|
|
191
|
+
syncGroups: Array.isArray(syncGroups) ? syncGroups : [],
|
|
192
|
+
ttlSeconds: typeof ttlSeconds === 'number' ? ttlSeconds : undefined,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
const err = error;
|
|
197
|
+
const code = err?.code && typeof err.code === 'string'
|
|
198
|
+
? err.code
|
|
199
|
+
: 'claim_rejected';
|
|
200
|
+
const msg = err?.message && typeof err.message === 'string'
|
|
201
|
+
? err.message
|
|
202
|
+
: 'claim rejected by server';
|
|
203
|
+
// Capability denials get the typed CapabilityError so
|
|
204
|
+
// callers can read `.requiredCapability` and attenuate-
|
|
205
|
+
// and-retry the claim with a narrower token.
|
|
206
|
+
if (code === 'capability_scope_denied' ||
|
|
207
|
+
code === 'capability_invalid') {
|
|
208
|
+
const rc = error
|
|
209
|
+
?.requiredCapability;
|
|
210
|
+
const requiredCapability = rc != null &&
|
|
211
|
+
typeof rc === 'object' &&
|
|
212
|
+
typeof rc.scope === 'string'
|
|
213
|
+
? rc
|
|
214
|
+
: undefined;
|
|
215
|
+
pending.reject(new CapabilityError(code, msg, requiredCapability));
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// Route through the shared factory so a failed claim_ack is a
|
|
219
|
+
// typed AbloError (registry code → right subclass), symmetric
|
|
220
|
+
// with the commit `mutation_result` path — never a bare Error.
|
|
221
|
+
pending.reject(errorFromWire(msg, { code }));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* Ack for a prior `update_subscription`. The wire carries no
|
|
227
|
+
* correlation id, so FIFO-match against the oldest pending
|
|
228
|
+
* request — the server applies and acks subscription updates
|
|
229
|
+
* in receive order. Validated through the canonical zod schema
|
|
230
|
+
* (mirrors how the Hub validates inbound frames).
|
|
231
|
+
*/
|
|
232
|
+
const handleSubscriptionAck = (session, message) => {
|
|
233
|
+
const pending = session.shiftPendingSubscription();
|
|
234
|
+
if (!pending)
|
|
235
|
+
return;
|
|
236
|
+
clearTimeout(pending.timeout);
|
|
237
|
+
const parsed = subscriptionAckPayloadSchema.safeParse(message.payload);
|
|
238
|
+
if (!parsed.success) {
|
|
239
|
+
// Unreadable ack — resolve the pending request as a failure
|
|
240
|
+
// rather than hang it until timeout.
|
|
241
|
+
pending.reject(errorFromWire('malformed subscription_ack from server', {
|
|
242
|
+
code: 'malformed_subscription',
|
|
243
|
+
}));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const ack = parsed.data;
|
|
247
|
+
if (ack.success) {
|
|
248
|
+
// Keep the reconnect URL aligned with current interest: a
|
|
249
|
+
// reconnect re-subscribes from `this.options.syncGroups`.
|
|
250
|
+
session.options.syncGroups = ack.syncGroups;
|
|
251
|
+
pending.resolve({ syncGroups: ack.syncGroups });
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
pending.reject(errorFromWire(ack.error?.message ?? 'update_subscription rejected by server', { code: ack.error?.code ?? 'malformed_subscription' }));
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* `delta` frames carry either a single delta or a `{ deltas: [...] }` batch.
|
|
259
|
+
* Only DISCRIMINATES the two shapes here — each delta is validated exactly
|
|
260
|
+
* once downstream (the host's `normalizeWireDelta` seam), so batch elements
|
|
261
|
+
* are handed over raw rather than pre-parsed.
|
|
262
|
+
*/
|
|
263
|
+
const handleDeltaFrame = (session, message) => {
|
|
264
|
+
const p = message.payload;
|
|
265
|
+
if (!isRecord(p))
|
|
266
|
+
return;
|
|
267
|
+
if (p.actionType || p.modelName) {
|
|
268
|
+
session.handleDelta(p);
|
|
269
|
+
}
|
|
270
|
+
else if (Array.isArray(p.deltas)) {
|
|
271
|
+
for (const d of p.deltas) {
|
|
272
|
+
session.handleDelta(d);
|
|
273
|
+
}
|
|
274
|
+
// `p.newVersions` from pre-cutover servers is ignored — the version
|
|
275
|
+
// vector was removed in W4a (sync_id is the causality token).
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* Frame-type → handler table. Every named server frame the SDK
|
|
280
|
+
* understands dispatches through here; anything else falls to the
|
|
281
|
+
* collaboration-event / unknown-type path in {@link dispatchWsFrame}.
|
|
282
|
+
*/
|
|
283
|
+
export const wsFrameHandlers = {
|
|
284
|
+
sync_response: (session, message) => { session.handleSyncResponse(message.payload); },
|
|
285
|
+
bootstrap_response: (session, message) => { session.handleBootstrapResponse(message.payload); },
|
|
286
|
+
presence_update: (session, message) => { session.handlePresenceUpdate(message); },
|
|
287
|
+
mutation_result: handleMutationResult,
|
|
288
|
+
claim_ack: handleClaimAck,
|
|
289
|
+
subscription_ack: handleSubscriptionAck,
|
|
290
|
+
claim_expired: (session, message) => {
|
|
291
|
+
// Server-initiated expiry notification. Emit as a typed
|
|
292
|
+
// event so consumers can react (re-claim with a fresh
|
|
293
|
+
// capability, or accept the drop). The claim is already
|
|
294
|
+
// inactive server-side by the time this arrives.
|
|
295
|
+
const p = (message.payload ?? {});
|
|
296
|
+
if (typeof p.claimId === 'string') {
|
|
297
|
+
recordClaim('expired', p);
|
|
298
|
+
session.emit('claim_expired', { claimId: p.claimId });
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
claim_rejected: (session, message) => {
|
|
302
|
+
// Server denied an `claim_begin` because the target is
|
|
303
|
+
// already claimed by another participant. Forward the
|
|
304
|
+
// payload as-is — the ClaimStream consumer interprets
|
|
305
|
+
// the conflict shape (peerId, target, etc.).
|
|
306
|
+
recordClaim('rejected', (message.payload ?? {}));
|
|
307
|
+
session.emit('claim_rejected', message.payload ?? {});
|
|
308
|
+
},
|
|
309
|
+
claim_acquired: (session, message) => {
|
|
310
|
+
// Opt-in fair queue: the target was free, so the lease is ours
|
|
311
|
+
// immediately (no waiting). Payload carries { claimId, target }.
|
|
312
|
+
recordClaim('acquired', (message.payload ?? {}));
|
|
313
|
+
session.emit('claim_acquired', message.payload ?? {});
|
|
314
|
+
},
|
|
315
|
+
claim_queue: (session, message) => {
|
|
316
|
+
// Per-entity wait-queue snapshot for reactive `queue(id)`. Not a
|
|
317
|
+
// single claim's state change, so it isn't logged — the per-claim
|
|
318
|
+
// `queued`/`granted` events already tell that story.
|
|
319
|
+
session.emit('claim_queue', message.payload ?? {});
|
|
320
|
+
},
|
|
321
|
+
claim_queued: (session, message) => {
|
|
322
|
+
// Opt-in fair queue: our claim is waiting in line. Payload
|
|
323
|
+
// carries { claimId, target, position }.
|
|
324
|
+
recordClaim('queued', (message.payload ?? {}));
|
|
325
|
+
session.emit('claim_queued', message.payload ?? {});
|
|
326
|
+
},
|
|
327
|
+
claim_granted: (session, message) => {
|
|
328
|
+
// Our queued claim reached the head — the lease is now ours.
|
|
329
|
+
recordClaim('granted', (message.payload ?? {}));
|
|
330
|
+
session.emit('claim_granted', message.payload ?? {});
|
|
331
|
+
},
|
|
332
|
+
claim_lost: (session, message) => {
|
|
333
|
+
// A held/granted claim was taken from us (TTL lapse, revoke).
|
|
334
|
+
recordClaim('lost', (message.payload ?? {}));
|
|
335
|
+
session.emit('claim_lost', message.payload ?? {});
|
|
336
|
+
},
|
|
337
|
+
delta: handleDeltaFrame,
|
|
338
|
+
};
|
|
339
|
+
/**
|
|
340
|
+
* Route one parsed inbound frame to its handler. Mirrors the original
|
|
341
|
+
* inline switch exactly: keepalives are ignored, a missing `type` is
|
|
342
|
+
* the legacy bare-delta form, unknown types fall through to the
|
|
343
|
+
* collaboration-event map (underscore wire format → colon event key).
|
|
344
|
+
*/
|
|
345
|
+
export function dispatchWsFrame(session, message) {
|
|
346
|
+
if (message.type === 'pong' || message.type === 'ping') {
|
|
347
|
+
// Ignore keepalive messages
|
|
348
|
+
getContext().logger.debug('Received keepalive', { type: message.type });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (message.type === undefined) {
|
|
352
|
+
// Legacy support: bare delta (validated at the host's
|
|
353
|
+
// normalizeWireDelta seam like every other delta).
|
|
354
|
+
if (message.actionType || message.modelName) {
|
|
355
|
+
session.handleDelta(message);
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
// Own-property lookup so wire types like 'toString' can never hit
|
|
360
|
+
// Object.prototype members — those fall through to the unknown-type
|
|
361
|
+
// path exactly as the switch's `default` did.
|
|
362
|
+
const handler = Object.prototype.hasOwnProperty.call(wsFrameHandlers, message.type)
|
|
363
|
+
? wsFrameHandlers[message.type]
|
|
364
|
+
: undefined;
|
|
365
|
+
if (handler) {
|
|
366
|
+
handler(session, message);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
// Collaboration events use underscore wire format (e.g., 'sheet_selection')
|
|
370
|
+
// Convert to colon format for the event map (e.g., 'sheet:selection')
|
|
371
|
+
const eventKey = message.type?.replace(/_/g, ':');
|
|
372
|
+
if (eventKey && session.collaborationEventTypes.has(eventKey)) {
|
|
373
|
+
session.emit(eventKey, message.payload);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
getContext().logger.debug('Received unknown message type', { message });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -4,20 +4,18 @@
|
|
|
4
4
|
* Creates well-formed bootstrap responses matching the server API.
|
|
5
5
|
*/
|
|
6
6
|
import type { BootstrapType } from '../../types/index.js';
|
|
7
|
-
export
|
|
8
|
-
[modelName: string]: Array<Record<string, unknown>>;
|
|
9
|
-
}
|
|
7
|
+
export type BootstrapModelData = Record<string, Record<string, unknown>[]>;
|
|
10
8
|
export interface BootstrapResponse {
|
|
11
9
|
type: BootstrapType;
|
|
12
10
|
lastSyncId: number;
|
|
13
11
|
models?: BootstrapModelData;
|
|
14
|
-
deltas?:
|
|
12
|
+
deltas?: {
|
|
15
13
|
id: number;
|
|
16
14
|
modelName: string;
|
|
17
15
|
modelId: string;
|
|
18
16
|
action: string;
|
|
19
17
|
data: Record<string, unknown>;
|
|
20
|
-
}
|
|
18
|
+
}[];
|
|
21
19
|
deltaCount?: number;
|
|
22
20
|
failedModels?: string[];
|
|
23
21
|
timestamp: number;
|
|
@@ -34,12 +32,12 @@ export declare function createPartialBootstrapResponse(deltas: BootstrapResponse
|
|
|
34
32
|
* Create a full bootstrap response with test model data pre-populated.
|
|
35
33
|
*/
|
|
36
34
|
export declare function createTestBootstrapResponse(options?: {
|
|
37
|
-
tasks?:
|
|
38
|
-
projects?:
|
|
39
|
-
slideDecks?:
|
|
40
|
-
slides?:
|
|
41
|
-
slideLayers?:
|
|
42
|
-
comments?:
|
|
35
|
+
tasks?: Record<string, unknown>[];
|
|
36
|
+
projects?: Record<string, unknown>[];
|
|
37
|
+
slideDecks?: Record<string, unknown>[];
|
|
38
|
+
slides?: Record<string, unknown>[];
|
|
39
|
+
slideLayers?: Record<string, unknown>[];
|
|
40
|
+
comments?: Record<string, unknown>[];
|
|
43
41
|
lastSyncId?: number;
|
|
44
42
|
failedModels?: string[];
|
|
45
43
|
}): BootstrapResponse;
|
|
@@ -78,7 +78,7 @@ export declare function createGroupRemovedDelta(userId: string, group: string, s
|
|
|
78
78
|
/**
|
|
79
79
|
* Create a batch of deltas with sequential sync IDs.
|
|
80
80
|
*/
|
|
81
|
-
export declare function createDeltaBatch(deltas:
|
|
81
|
+
export declare function createDeltaBatch(deltas: Omit<CreateDeltaOptions, 'id'>[], startingSyncId?: number): SyncAction[];
|
|
82
82
|
/**
|
|
83
83
|
* Create a confirmation delta — used to confirm that a mutation
|
|
84
84
|
* was persisted by the server (TransactionQueue watches for this).
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* TestSlideDeck (10) → TestSlide (15, FK→SlideDeck) → TestSlideLayer (20, FK→Slide)
|
|
8
8
|
*/
|
|
9
9
|
import { Model } from '../../Model.js';
|
|
10
|
+
import { ModelRegistry } from '../../ModelRegistry.js';
|
|
10
11
|
import { PropertyType, LoadStrategy } from '../../types/index.js';
|
|
11
12
|
// ─────────────────────────────────────────────
|
|
12
13
|
// Test Model Classes
|
|
@@ -9,7 +9,6 @@ import { ObjectPool } from '../../ObjectPool.js';
|
|
|
9
9
|
import { MockMutationExecutor } from '../mocks/MockMutationExecutor.js';
|
|
10
10
|
import { MockNetworkMonitor } from '../mocks/MockNetworkMonitor.js';
|
|
11
11
|
import { MockWebSocket } from '../mocks/MockWebSocket.js';
|
|
12
|
-
import { MockMutationDispatcher } from '../mocks/MockSyncContext.js';
|
|
13
12
|
import type { TestContextResult } from '../mocks/MockSyncContext.js';
|
|
14
13
|
export interface TestHarness {
|
|
15
14
|
/** Pre-registered ModelRegistry with test models */
|
|
@@ -24,8 +23,6 @@ export interface TestHarness {
|
|
|
24
23
|
mutationExecutor: MockMutationExecutor;
|
|
25
24
|
/** Shorthand: mock network monitor */
|
|
26
25
|
networkMonitor: MockNetworkMonitor;
|
|
27
|
-
/** Shorthand: mock mutation dispatcher */
|
|
28
|
-
mutationDispatcher: MockMutationDispatcher;
|
|
29
26
|
/** Cleanup everything */
|
|
30
27
|
cleanup: () => void;
|
|
31
28
|
}
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { ModelRegistry, setActiveRegistry } from '../../ModelRegistry.js';
|
|
8
8
|
import { ObjectPool } from '../../ObjectPool.js';
|
|
9
|
+
import { MockMutationExecutor } from '../mocks/MockMutationExecutor.js';
|
|
10
|
+
import { MockNetworkMonitor } from '../mocks/MockNetworkMonitor.js';
|
|
9
11
|
import { MockWebSocket } from '../mocks/MockWebSocket.js';
|
|
10
12
|
import { createTestContext } from '../mocks/MockSyncContext.js';
|
|
11
13
|
import { registerTestModels, createTestConfig, resetFixtureCounter, } from '../fixtures/models.js';
|
|
@@ -58,7 +60,6 @@ export function createTestHarness(options = {}) {
|
|
|
58
60
|
context,
|
|
59
61
|
mutationExecutor: context.mocks.mutationExecutor,
|
|
60
62
|
networkMonitor: context.mocks.networkMonitor,
|
|
61
|
-
mutationDispatcher: context.mocks.mutationDispatcher,
|
|
62
63
|
cleanup: () => {
|
|
63
64
|
pool.clear();
|
|
64
65
|
webSocket.reset();
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type { CapturedMutation, MockMutationExecutorOptions } from './mocks/Mock
|
|
|
9
9
|
export { MockNetworkMonitor } from './mocks/MockNetworkMonitor.js';
|
|
10
10
|
export { MockWebSocket } from './mocks/MockWebSocket.js';
|
|
11
11
|
export type { MockDelta, MockBootstrapHint } from './mocks/MockWebSocket.js';
|
|
12
|
-
export { createTestContext,
|
|
12
|
+
export { createTestContext, } from './mocks/MockSyncContext.js';
|
|
13
13
|
export type { TestContextOptions, TestContextResult } from './mocks/MockSyncContext.js';
|
|
14
14
|
export { TestProject, TestTask, TestComment, TestSlideDeck, TestSlide, TestSlideLayer, TEST_MODEL_PRIORITIES, registerTestModels, createTestConfig, resetFixtureCounter, createProjectFixture, createTaskFixture, createCommentFixture, createSlideDeckFixture, createSlideFixture, createSlideLayerFixture, } from './fixtures/models.js';
|
|
15
15
|
export { createDelta, createInsertDelta, createUpdateDelta, createDeleteDelta, createArchiveDelta, createUnarchiveDelta, createCoveringDelta, createGroupAddedDelta, createLegacyGroupChangeDelta, createGroupRemovedDelta, createDeltaBatch, createConfirmationDelta, resetDeltaCounter, } from './fixtures/deltas.js';
|
package/dist/testing/index.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
export { MockMutationExecutor } from './mocks/MockMutationExecutor.js';
|
|
11
11
|
export { MockNetworkMonitor } from './mocks/MockNetworkMonitor.js';
|
|
12
12
|
export { MockWebSocket } from './mocks/MockWebSocket.js';
|
|
13
|
-
export { createTestContext,
|
|
13
|
+
export { createTestContext, } from './mocks/MockSyncContext.js';
|
|
14
14
|
// ─────────────────────────────────────────────
|
|
15
15
|
// Fixtures: Models
|
|
16
16
|
// ─────────────────────────────────────────────
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* returns handles to all mocks for test assertions.
|
|
7
7
|
*/
|
|
8
8
|
import type { SyncEngineContext } from '../../SyncEngineContext.js';
|
|
9
|
-
import type { SyncLogger, SyncObservabilityProvider, SessionErrorDetector,
|
|
9
|
+
import type { SyncLogger, SyncObservabilityProvider, SessionErrorDetector, SyncEngineConfig } from '../../interfaces/index.js';
|
|
10
10
|
import { MockMutationExecutor } from './MockMutationExecutor.js';
|
|
11
11
|
import { MockNetworkMonitor } from './MockNetworkMonitor.js';
|
|
12
12
|
export interface TestContextOptions {
|
|
@@ -29,27 +29,11 @@ export interface TestContextResult {
|
|
|
29
29
|
/** Mock handles for test assertions */
|
|
30
30
|
mocks: {
|
|
31
31
|
mutationExecutor: MockMutationExecutor;
|
|
32
|
-
mutationDispatcher: MockMutationDispatcher;
|
|
33
32
|
networkMonitor: MockNetworkMonitor;
|
|
34
33
|
};
|
|
35
34
|
/** Cleanup: calls resetSyncEngine() */
|
|
36
35
|
cleanup: () => void;
|
|
37
36
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Simple mock mutation dispatcher that records dispatch calls.
|
|
40
|
-
*/
|
|
41
|
-
export declare class MockMutationDispatcher implements MutationDispatcher {
|
|
42
|
-
readonly dispatched: Array<{
|
|
43
|
-
operationName: string;
|
|
44
|
-
variables: Record<string, unknown>;
|
|
45
|
-
}>;
|
|
46
|
-
private _shouldSucceed;
|
|
47
|
-
private _error?;
|
|
48
|
-
dispatch(operationName: string, variables: Record<string, unknown>): Promise<void>;
|
|
49
|
-
failAll(error?: Error): void;
|
|
50
|
-
succeedAll(): void;
|
|
51
|
-
reset(): void;
|
|
52
|
-
}
|
|
53
37
|
/**
|
|
54
38
|
* Create a test SyncEngineContext with all mocks pre-wired.
|
|
55
39
|
* Calls initSyncEngine() so the global context is set.
|
|
@@ -7,37 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { noopLogger, noopObservability, noopAnalytics, defaultSessionErrorDetector, emptyConfig, } from '../../SyncEngineContext.js';
|
|
9
9
|
import { initSyncEngine, resetSyncEngine } from '../../context.js';
|
|
10
|
-
import { ModelRegistry, setActiveRegistry, hasActiveRegistry, } from '../../ModelRegistry.js';
|
|
10
|
+
import { ModelRegistry, setActiveRegistry, hasActiveRegistry, clearActiveRegistry, } from '../../ModelRegistry.js';
|
|
11
11
|
import { registerTestModels } from '../fixtures/models.js';
|
|
12
12
|
import { MockMutationExecutor } from './MockMutationExecutor.js';
|
|
13
13
|
import { MockNetworkMonitor } from './MockNetworkMonitor.js';
|
|
14
|
-
/**
|
|
15
|
-
* Simple mock mutation dispatcher that records dispatch calls.
|
|
16
|
-
*/
|
|
17
|
-
export class MockMutationDispatcher {
|
|
18
|
-
dispatched = [];
|
|
19
|
-
_shouldSucceed = true;
|
|
20
|
-
_error;
|
|
21
|
-
async dispatch(operationName, variables) {
|
|
22
|
-
this.dispatched.push({ operationName, variables });
|
|
23
|
-
if (!this._shouldSucceed) {
|
|
24
|
-
throw this._error ?? new Error(`Mock dispatch failed: ${operationName}`);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
failAll(error) {
|
|
28
|
-
this._shouldSucceed = false;
|
|
29
|
-
this._error = error;
|
|
30
|
-
}
|
|
31
|
-
succeedAll() {
|
|
32
|
-
this._shouldSucceed = true;
|
|
33
|
-
this._error = undefined;
|
|
34
|
-
}
|
|
35
|
-
reset() {
|
|
36
|
-
this.dispatched.length = 0;
|
|
37
|
-
this._shouldSucceed = true;
|
|
38
|
-
this._error = undefined;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
14
|
/**
|
|
42
15
|
* Create a test SyncEngineContext with all mocks pre-wired.
|
|
43
16
|
* Calls initSyncEngine() so the global context is set.
|
|
@@ -51,7 +24,6 @@ export class MockMutationDispatcher {
|
|
|
51
24
|
*/
|
|
52
25
|
export function createTestContext(options = {}) {
|
|
53
26
|
const mutationExecutor = new MockMutationExecutor(options.mutationExecutorOptions);
|
|
54
|
-
const mutationDispatcher = new MockMutationDispatcher();
|
|
55
27
|
const networkMonitor = new MockNetworkMonitor(!options.startOffline);
|
|
56
28
|
const config = {
|
|
57
29
|
...emptyConfig,
|
|
@@ -66,7 +38,6 @@ export function createTestContext(options = {}) {
|
|
|
66
38
|
sessionErrorDetector: options.sessionErrorDetector ?? defaultSessionErrorDetector,
|
|
67
39
|
onlineStatus: networkMonitor,
|
|
68
40
|
mutationExecutor,
|
|
69
|
-
mutationDispatcher,
|
|
70
41
|
config,
|
|
71
42
|
};
|
|
72
43
|
initSyncEngine(context);
|
|
@@ -82,7 +53,6 @@ export function createTestContext(options = {}) {
|
|
|
82
53
|
context,
|
|
83
54
|
mocks: {
|
|
84
55
|
mutationExecutor,
|
|
85
|
-
mutationDispatcher,
|
|
86
56
|
networkMonitor,
|
|
87
57
|
},
|
|
88
58
|
cleanup: () => {
|
|
@@ -93,7 +63,6 @@ export function createTestContext(options = {}) {
|
|
|
93
63
|
// registry in place keeps those calls valid; the next createTestContext
|
|
94
64
|
// with hasActiveRegistry()===true simply reuses it.
|
|
95
65
|
mutationExecutor.reset();
|
|
96
|
-
mutationDispatcher.reset();
|
|
97
66
|
networkMonitor.reset();
|
|
98
67
|
},
|
|
99
68
|
};
|
|
@@ -27,14 +27,14 @@ type ModelCtor<T extends Model> = abstract new (...args: never[]) => T;
|
|
|
27
27
|
export declare class MockSyncStore implements SyncStoreContract {
|
|
28
28
|
private byClass;
|
|
29
29
|
calls: {
|
|
30
|
-
retrieve:
|
|
30
|
+
retrieve: {
|
|
31
31
|
modelClass: ModelCtor<Model>;
|
|
32
32
|
id: string;
|
|
33
|
-
}
|
|
34
|
-
query:
|
|
33
|
+
}[];
|
|
34
|
+
query: {
|
|
35
35
|
modelClass: ModelCtor<Model>;
|
|
36
36
|
options?: QueryOptions<Model>;
|
|
37
|
-
}
|
|
37
|
+
}[];
|
|
38
38
|
save: Model[];
|
|
39
39
|
delete: Model[];
|
|
40
40
|
archive: Model[];
|
|
@@ -30,10 +30,10 @@ export declare class MockWebSocket {
|
|
|
30
30
|
private _sessionError;
|
|
31
31
|
private _listeners;
|
|
32
32
|
/** Track all emitted events for assertions */
|
|
33
|
-
readonly emittedEvents:
|
|
33
|
+
readonly emittedEvents: {
|
|
34
34
|
type: string;
|
|
35
35
|
data: unknown;
|
|
36
|
-
}
|
|
36
|
+
}[];
|
|
37
37
|
get connected(): boolean;
|
|
38
38
|
get sessionError(): boolean;
|
|
39
39
|
on(event: string, handler: EventHandler): () => void;
|