@abloatai/ablo 0.35.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +713 -629
- package/README.md +56 -519
- package/bin/ablo.cjs +39 -0
- package/dist/BaseSyncedStore.d.ts +24 -4
- package/dist/BaseSyncedStore.js +53 -37
- package/dist/Database.d.ts +8 -20
- package/dist/Database.js +61 -59
- package/dist/InstanceCache.d.ts +6 -2
- package/dist/InstanceCache.js +18 -16
- package/dist/Model.d.ts +17 -7
- package/dist/Model.js +17 -7
- package/dist/ModelRegistry.d.ts +4 -0
- package/dist/ModelRegistry.js +17 -15
- package/dist/NetworkMonitor.d.ts +3 -1
- package/dist/NetworkMonitor.js +7 -5
- package/dist/SyncClient.d.ts +5 -15
- package/dist/SyncClient.js +60 -57
- package/dist/client/Ablo.js +30 -19
- package/dist/client/createInternalComponents.d.ts +4 -0
- package/dist/client/createInternalComponents.js +8 -2
- package/dist/client/createModelProxy.d.ts +21 -1
- package/dist/client/createModelProxy.js +123 -57
- package/dist/client/humans.d.ts +30 -9
- package/dist/client/humans.js +45 -19
- package/dist/client/reactiveEngine.d.ts +16 -11
- package/dist/client/reactiveEngine.js +113 -335
- package/dist/client/storeCluster.d.ts +47 -0
- package/dist/client/storeCluster.js +118 -0
- package/dist/client/storeLifecycle.d.ts +61 -0
- package/dist/client/storeLifecycle.js +231 -0
- package/dist/context.d.ts +13 -0
- package/dist/context.js +23 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/docs/catalog.js +6 -3
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/query/client.d.ts +3 -0
- package/dist/query/client.js +6 -5
- package/dist/react/AbloProvider.d.ts +13 -1
- package/dist/react/AbloProvider.js +5 -2
- package/dist/react/context.d.ts +2 -2
- package/dist/react/createAbloReact.d.ts +56 -0
- package/dist/react/createAbloReact.js +51 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +3 -0
- package/dist/react/useAblo.d.ts +9 -2
- package/dist/react/useAblo.js +25 -7
- package/dist/schema/coordination.js +5 -1
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +4 -0
- package/dist/schema/select.js +3 -0
- package/dist/schema/serialize.js +3 -0
- package/dist/source/adapter.d.ts +7 -5
- package/dist/source/adapter.js +7 -5
- package/dist/source/index.d.ts +1 -1
- package/dist/source/index.js +1 -1
- package/dist/{core/storeContract.d.ts → storeContract.d.ts} +5 -5
- package/dist/{core → stores}/DatabaseManager.d.ts +3 -1
- package/dist/{core → stores}/DatabaseManager.js +13 -12
- package/dist/{core → stores}/StoreManager.d.ts +5 -3
- package/dist/{core → stores}/StoreManager.js +24 -22
- package/dist/stores/SyncActionStore.d.ts +3 -1
- package/dist/stores/SyncActionStore.js +9 -7
- package/dist/sync/BootstrapFetcher.d.ts +4 -0
- package/dist/sync/BootstrapFetcher.js +28 -26
- package/dist/sync/OnDemandLoader.d.ts +3 -0
- package/dist/sync/OnDemandLoader.js +1 -0
- package/dist/sync/bootstrapApply.d.ts +3 -0
- package/dist/sync/bootstrapApply.js +2 -2
- package/dist/sync/deltaPipeline.d.ts +13 -12
- package/dist/sync/deltaPipeline.js +21 -4
- package/dist/sync/groupChange.d.ts +3 -0
- package/dist/sync/groupChange.js +16 -14
- package/dist/sync/participants.d.ts +19 -2
- package/dist/sync/participants.js +3 -1
- package/dist/sync/schemas.d.ts +2 -1
- package/dist/sync/schemas.js +3 -3
- package/dist/syncLog/contract.d.ts +20 -0
- package/dist/syncLog/contract.js +19 -0
- package/dist/syncLog/index.d.ts +1 -0
- package/dist/syncLog/index.js +1 -0
- package/dist/transaction/auth/capability.d.ts +35 -0
- package/dist/transaction/auth/capability.js +25 -0
- package/dist/transaction/coordination/awaitClaimGrant.d.ts +7 -0
- package/dist/transaction/coordination/awaitClaimGrant.js +12 -0
- package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +34 -0
- package/dist/transaction/coordination/claimHeartbeatLoop.js +20 -0
- package/dist/transaction/coordination/index.d.ts +4 -4
- package/dist/transaction/coordination/index.js +4 -3
- package/dist/transaction/coordination/locator.d.ts +23 -2
- package/dist/transaction/coordination/locator.js +22 -2
- package/dist/transaction/coordination/schema.d.ts +125 -62
- package/dist/transaction/coordination/schema.js +228 -64
- package/dist/transaction/coordination/targetConflict.js +32 -28
- package/dist/transaction/errorCodes.d.ts +2 -2
- package/dist/transaction/errorCodes.js +13 -9
- package/dist/transaction/plugin.d.ts +95 -2
- package/dist/transaction/plugin.js +21 -2
- package/dist/transaction/resources/httpResources.d.ts +57 -2
- package/dist/transaction/resources/modelOperations.d.ts +148 -40
- package/dist/transaction/resources/where.d.ts +16 -0
- package/dist/transaction/resources/where.js +45 -0
- package/dist/transaction/schema/field.d.ts +12 -18
- package/dist/transaction/schema/fieldRef.d.ts +38 -0
- package/dist/transaction/schema/fieldRef.js +11 -0
- package/dist/transaction/schema/openapi.d.ts +16 -15
- package/dist/transaction/schema/openapi.js +186 -25
- package/dist/transaction/schema/relation.d.ts +7 -2
- package/dist/transaction/schema/schema.d.ts +27 -0
- package/dist/transaction/schema/schema.js +20 -0
- package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +1 -1
- package/dist/transaction/transactions/settlement/pendingWrite.d.ts +1 -1
- package/dist/transaction/transport/httpClient.d.ts +9 -1
- package/dist/transaction/transport/httpClient.js +1 -0
- package/dist/transaction/transport/httpTransport.js +156 -44
- package/dist/transaction/transport/wsTransport.d.ts +2 -4
- package/dist/transaction/transport/wsTransport.js +16 -10
- package/dist/transaction/types/streams.d.ts +10 -0
- package/dist/transaction/utils/duration.d.ts +25 -0
- package/dist/transaction/utils/duration.js +32 -0
- package/dist/transaction/wire/accountResponses.d.ts +69 -0
- package/dist/transaction/wire/accountResponses.js +36 -1
- package/dist/transaction/wire/auth.d.ts +9 -2
- package/dist/transaction/wire/auth.js +7 -1
- package/dist/transaction/wire/claims.d.ts +164 -97
- package/dist/transaction/wire/claims.js +126 -28
- package/dist/transaction/wire/commit.d.ts +1 -1
- package/dist/transaction/wire/feedEvent.d.ts +27 -0
- package/dist/transaction/wire/feedEvent.js +27 -1
- package/dist/transaction/wire/frames.d.ts +2 -2
- package/dist/transaction/wire/inboundFrames.d.ts +12 -2
- package/dist/transaction/wire/index.d.ts +10 -6
- package/dist/transaction/wire/index.js +12 -3
- package/dist/transaction/wire/modelMutations.d.ts +31 -0
- package/dist/transaction/wire/modelMutations.js +52 -0
- package/dist/transaction/wire/modelShape.d.ts +78 -0
- package/dist/transaction/wire/modelShape.js +74 -0
- package/dist/transactions/mutations/MutationQueue.d.ts +6 -0
- package/dist/transactions/mutations/MutationQueue.js +55 -45
- package/dist/transactions/mutations/commitPayload.d.ts +3 -2
- package/dist/transactions/mutations/commitPayload.js +5 -5
- package/dist/transactions/mutations/deltaConfirmation.d.ts +4 -0
- package/dist/transactions/mutations/deltaConfirmation.js +10 -8
- package/dist/transactions/mutations/replayValidation.d.ts +2 -1
- package/dist/transactions/mutations/replayValidation.js +3 -2
- package/dist/{core → views}/QueryView.d.ts +1 -1
- package/dist/{core → views}/QueryView.js +1 -1
- package/dist/{core → views}/ViewRegistry.d.ts +1 -1
- package/dist/{core/queryUtils.d.ts → views/incrementalView.d.ts} +6 -6
- package/dist/{core/queryUtils.js → views/incrementalView.js} +6 -6
- package/docs/agents.md +1 -1
- package/docs/api-keys.md +6 -6
- package/docs/api.md +5 -43
- package/docs/audit.md +4 -3
- package/docs/cli.md +11 -11
- package/docs/client-behavior.md +4 -4
- package/docs/concurrency-convention.md +28 -42
- package/docs/coordination.md +235 -83
- package/docs/data-sources.md +4 -4
- package/docs/debugging.md +34 -12
- package/docs/deployment.md +8 -8
- package/docs/examples/scoped-agent.md +3 -3
- package/docs/groups.md +57 -3
- package/docs/guarantees.md +37 -10
- package/docs/how-it-works.md +29 -5
- package/docs/idempotency.md +6 -6
- package/docs/identity.md +22 -23
- package/docs/index.md +8 -8
- package/docs/integration-guide.md +14 -3
- package/docs/mcp.md +7 -7
- package/docs/migration.md +34 -15
- package/docs/projects.md +1 -1
- package/docs/react.md +19 -8
- package/docs/sessions.md +1 -1
- package/docs/webhooks.md +9 -9
- package/llms.txt +4 -4
- package/package.json +13 -20
- package/dist/cli.cjs +0 -288600
- package/dist/testing/fixtures/bootstrap.d.ts +0 -49
- package/dist/testing/fixtures/bootstrap.js +0 -59
- package/dist/testing/fixtures/deltas.d.ts +0 -83
- package/dist/testing/fixtures/deltas.js +0 -136
- package/dist/testing/fixtures/httpResponses.d.ts +0 -70
- package/dist/testing/fixtures/httpResponses.js +0 -90
- package/dist/testing/fixtures/models.d.ts +0 -83
- package/dist/testing/fixtures/models.js +0 -272
- package/dist/testing/helpers/reactWrapper.d.ts +0 -69
- package/dist/testing/helpers/reactWrapper.js +0 -67
- package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
- package/dist/testing/helpers/syncEngineHarness.js +0 -73
- package/dist/testing/helpers/wait.d.ts +0 -30
- package/dist/testing/helpers/wait.js +0 -49
- package/dist/testing/index.d.ts +0 -23
- package/dist/testing/index.js +0 -33
- package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
- package/dist/testing/mocks/FakeDatabase.js +0 -10
- package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
- package/dist/testing/mocks/MockMutationExecutor.js +0 -186
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
- package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
- package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
- package/dist/testing/mocks/MockSyncContext.js +0 -72
- package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
- package/dist/testing/mocks/MockSyncStore.js +0 -171
- package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
- package/dist/testing/mocks/MockWebSocket.js +0 -118
- package/docs/interaction-model.md +0 -99
- /package/dist/{core → query}/QueryProcessor.d.ts +0 -0
- /package/dist/{core → query}/QueryProcessor.js +0 -0
- /package/dist/{core/storeContract.js → storeContract.js} +0 -0
- /package/dist/{core → stores}/openIDBWithTimeout.d.ts +0 -0
- /package/dist/{core → stores}/openIDBWithTimeout.js +0 -0
- /package/dist/{source → transaction}/footprint.d.ts +0 -0
- /package/dist/{source → transaction}/footprint.js +0 -0
- /package/dist/{core → views}/ViewRegistry.js +0 -0
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* An in-memory {@link SyncStoreContract} for tests. It lets you seed rows,
|
|
3
|
-
* read and write them synchronously, and inspect every call the code under
|
|
4
|
-
* test made — all without a real sync backend.
|
|
5
|
-
*/
|
|
6
|
-
import { ViewRegistry } from '../../core/ViewRegistry.js';
|
|
7
|
-
import { AbloValidationError } from '../../transaction/errors.js';
|
|
8
|
-
/**
|
|
9
|
-
* An in-memory implementation of {@link SyncStoreContract}. Seed rows with
|
|
10
|
-
* {@link MockSyncStore.setModels}, then read and write through the contract
|
|
11
|
-
* methods; every call is recorded on {@link MockSyncStore.calls} so tests can
|
|
12
|
-
* assert on what happened.
|
|
13
|
-
*/
|
|
14
|
-
export class MockSyncStore {
|
|
15
|
-
// Seeded data, keyed by model class
|
|
16
|
-
byClass = new Map();
|
|
17
|
-
// Call tracking for assertions
|
|
18
|
-
calls = {
|
|
19
|
-
retrieve: [],
|
|
20
|
-
query: [],
|
|
21
|
-
save: [],
|
|
22
|
-
delete: [],
|
|
23
|
-
archive: [],
|
|
24
|
-
unarchive: [],
|
|
25
|
-
};
|
|
26
|
-
/**
|
|
27
|
-
* Seed the store with models of a specific class.
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* mockStore.setModels(Task, [task1, task2]);
|
|
31
|
-
*/
|
|
32
|
-
setModels(modelClass, models) {
|
|
33
|
-
const map = new Map();
|
|
34
|
-
for (const m of models) {
|
|
35
|
-
map.set(m.id, m);
|
|
36
|
-
}
|
|
37
|
-
this.byClass.set(modelClass, map);
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Adds or replaces a single model, keyed by its id.
|
|
41
|
-
*/
|
|
42
|
-
addModel(modelClass, model) {
|
|
43
|
-
let map = this.byClass.get(modelClass);
|
|
44
|
-
if (!map) {
|
|
45
|
-
map = new Map();
|
|
46
|
-
this.byClass.set(modelClass, map);
|
|
47
|
-
}
|
|
48
|
-
map.set(model.id, model);
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Remove a model by ID.
|
|
52
|
-
*/
|
|
53
|
-
removeModel(modelClass, id) {
|
|
54
|
-
this.byClass.get(modelClass)?.delete(id);
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Clear all seeded data and call history.
|
|
58
|
-
*/
|
|
59
|
-
reset() {
|
|
60
|
-
this.byClass.clear();
|
|
61
|
-
this.calls = {
|
|
62
|
-
retrieve: [],
|
|
63
|
-
query: [],
|
|
64
|
-
save: [],
|
|
65
|
-
delete: [],
|
|
66
|
-
archive: [],
|
|
67
|
-
unarchive: [],
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
// ── SyncStoreContract implementation ──────────────────────────────────
|
|
71
|
-
retrieve(modelClass, id) {
|
|
72
|
-
this.calls.retrieve.push({ modelClass: modelClass, id });
|
|
73
|
-
return this.byClass.get(modelClass)?.get(id);
|
|
74
|
-
}
|
|
75
|
-
queryByClass(modelClass, options) {
|
|
76
|
-
this.calls.query.push({
|
|
77
|
-
modelClass: modelClass,
|
|
78
|
-
options: options,
|
|
79
|
-
});
|
|
80
|
-
const map = this.byClass.get(modelClass);
|
|
81
|
-
if (!map) {
|
|
82
|
-
return { data: [] };
|
|
83
|
-
}
|
|
84
|
-
let data = Array.from(map.values());
|
|
85
|
-
// Apply predicate
|
|
86
|
-
if (options?.predicate) {
|
|
87
|
-
data = data.filter(options.predicate);
|
|
88
|
-
}
|
|
89
|
-
// Apply ordering
|
|
90
|
-
if (options?.orderBy) {
|
|
91
|
-
const key = options.orderBy;
|
|
92
|
-
const order = options.order ?? 'asc';
|
|
93
|
-
data.sort((a, b) => {
|
|
94
|
-
const av = a[key];
|
|
95
|
-
const bv = b[key];
|
|
96
|
-
if (av === bv)
|
|
97
|
-
return 0;
|
|
98
|
-
const cmp = av < bv ? -1 : 1;
|
|
99
|
-
return order === 'asc' ? cmp : -cmp;
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
// Apply pagination
|
|
103
|
-
if (options?.offset) {
|
|
104
|
-
data = data.slice(options.offset);
|
|
105
|
-
}
|
|
106
|
-
if (options?.limit !== undefined) {
|
|
107
|
-
data = data.slice(0, options.limit);
|
|
108
|
-
}
|
|
109
|
-
return { data };
|
|
110
|
-
}
|
|
111
|
-
async save(model) {
|
|
112
|
-
this.calls.save.push(model);
|
|
113
|
-
// Store the model under its constructor so a later retrieve or query
|
|
114
|
-
// returns it.
|
|
115
|
-
const ctor = model.constructor;
|
|
116
|
-
this.addModel(ctor, model);
|
|
117
|
-
}
|
|
118
|
-
async delete(model) {
|
|
119
|
-
this.calls.delete.push(model);
|
|
120
|
-
const ctor = model.constructor;
|
|
121
|
-
this.removeModel(ctor, model.id);
|
|
122
|
-
}
|
|
123
|
-
async archive(model) {
|
|
124
|
-
this.calls.archive.push(model);
|
|
125
|
-
}
|
|
126
|
-
async unarchive(model) {
|
|
127
|
-
this.calls.unarchive.push(model);
|
|
128
|
-
}
|
|
129
|
-
// Sync-status getters default to "ready, idle". Tests that exercise
|
|
130
|
-
// offline/reconnect flows can set these before rendering.
|
|
131
|
-
isReady = true;
|
|
132
|
-
isSyncing = false;
|
|
133
|
-
isOffline = false;
|
|
134
|
-
isReconnecting = false;
|
|
135
|
-
isError = false;
|
|
136
|
-
hasUnsyncedChanges = false;
|
|
137
|
-
syncStatus = {
|
|
138
|
-
state: 'idle',
|
|
139
|
-
progress: 100,
|
|
140
|
-
pendingChanges: 0,
|
|
141
|
-
isSessionError: false,
|
|
142
|
-
};
|
|
143
|
-
/** A minimal object pool backing the useEntity and useEntities hooks in tests. */
|
|
144
|
-
pool = {
|
|
145
|
-
get: (id) => {
|
|
146
|
-
for (const models of this.byClass.values()) {
|
|
147
|
-
const model = models.get(id);
|
|
148
|
-
if (model)
|
|
149
|
-
return model;
|
|
150
|
-
}
|
|
151
|
-
return undefined;
|
|
152
|
-
},
|
|
153
|
-
getByTypeName: (_typename) => [],
|
|
154
|
-
getByForeignKey: () => [],
|
|
155
|
-
createFromData: () => null,
|
|
156
|
-
hasForeignKeyIndex: () => false,
|
|
157
|
-
createView: () => {
|
|
158
|
-
throw new AbloValidationError('MockSyncStore does not support createView', {
|
|
159
|
-
code: 'mock_unsupported_operation',
|
|
160
|
-
});
|
|
161
|
-
},
|
|
162
|
-
viewRegistry: new ViewRegistry(),
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Create a new MockSyncStore.
|
|
167
|
-
* Shorthand for `new MockSyncStore()`.
|
|
168
|
-
*/
|
|
169
|
-
export function createMockSyncStore() {
|
|
170
|
-
return new MockSyncStore();
|
|
171
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A controllable WebSocket stand-in for sync engine tests. It reproduces the
|
|
3
|
-
* event interface of the real connection without opening a socket, so tests
|
|
4
|
-
* can drive connection changes, deltas, and bootstrap hints by hand.
|
|
5
|
-
*/
|
|
6
|
-
import type { SyncActionType } from '../../stores/syncAction.js';
|
|
7
|
-
import type { BootstrapReason } from '../../transaction/wire/bootstrapReason.js';
|
|
8
|
-
/** The shape of a single delta — one change the server pushes to the client. */
|
|
9
|
-
export interface MockDelta {
|
|
10
|
-
id: number;
|
|
11
|
-
modelName: string;
|
|
12
|
-
modelId: string;
|
|
13
|
-
action: SyncActionType;
|
|
14
|
-
data: Record<string, unknown>;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* A hint from the server that the client has fallen too far behind and should
|
|
18
|
-
* rebuild its data from scratch rather than catch up one delta at a time.
|
|
19
|
-
*/
|
|
20
|
-
export interface MockBootstrapHint {
|
|
21
|
-
reason: BootstrapReason;
|
|
22
|
-
tables?: string[];
|
|
23
|
-
staleTables?: string[];
|
|
24
|
-
}
|
|
25
|
-
type EventHandler = (...args: unknown[]) => void;
|
|
26
|
-
/**
|
|
27
|
-
* A controllable, event-based stand-in for the sync engine's WebSocket
|
|
28
|
-
* connection. Subscribe with {@link MockWebSocket.on} exactly as production
|
|
29
|
-
* code does, then use the `simulate*` and `receive*` methods to push
|
|
30
|
-
* connection changes, deltas, and bootstrap hints. Every event the mock emits
|
|
31
|
-
* is recorded on {@link MockWebSocket.emittedEvents} for assertions.
|
|
32
|
-
*/
|
|
33
|
-
export declare class MockWebSocket {
|
|
34
|
-
private _connected;
|
|
35
|
-
private _sessionError;
|
|
36
|
-
private _listeners;
|
|
37
|
-
/** Every event the mock has emitted, in order, for assertions. */
|
|
38
|
-
readonly emittedEvents: {
|
|
39
|
-
type: string;
|
|
40
|
-
data: unknown;
|
|
41
|
-
}[];
|
|
42
|
-
get connected(): boolean;
|
|
43
|
-
get sessionError(): boolean;
|
|
44
|
-
on(event: string, handler: EventHandler): () => void;
|
|
45
|
-
private emit;
|
|
46
|
-
/** Fires a successful connection, emitting the `connected` event. */
|
|
47
|
-
simulateConnect(): void;
|
|
48
|
-
/** Drops the connection, emitting the `disconnected` event. */
|
|
49
|
-
simulateDisconnect(): void;
|
|
50
|
-
/** Emits a `reconnecting` event carrying the attempt number and retry delay. */
|
|
51
|
-
simulateReconnecting(attempt: number, delay: number): void;
|
|
52
|
-
/** Emits a `session_error` event, as when the server rejects the session as unauthorized. The code defaults to 401. */
|
|
53
|
-
simulateSessionError(code?: number): void;
|
|
54
|
-
/** Emits a `reconnect_failed` event, signaling the client gave up after exhausting its retries. */
|
|
55
|
-
simulateReconnectFailed(): void;
|
|
56
|
-
/** Delivers a single delta, emitting a `delta` event as if the server had sent it. */
|
|
57
|
-
receiveDelta(delta: MockDelta): void;
|
|
58
|
-
/** Delivers a batch of deltas, emitting a `delta_batch` event. */
|
|
59
|
-
receiveDeltas(deltas: MockDelta[]): void;
|
|
60
|
-
/** Emits a `bootstrap_required` hint, telling the client to rebuild its data instead of catching up. */
|
|
61
|
-
simulateBootstrapHint(hint: MockBootstrapHint): void;
|
|
62
|
-
/** Emits a `presence_update` event carrying the given payload. */
|
|
63
|
-
simulatePresenceUpdate(data: Record<string, unknown>): void;
|
|
64
|
-
/** Returns the payloads of every emitted event of the given type, in order. */
|
|
65
|
-
getEvents(type: string): unknown[];
|
|
66
|
-
/** Reports whether an event of the given type has been emitted. */
|
|
67
|
-
hasEmitted(type: string): boolean;
|
|
68
|
-
/** Clears connection state, listeners, and the record of emitted events. */
|
|
69
|
-
reset(): void;
|
|
70
|
-
}
|
|
71
|
-
export {};
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A controllable WebSocket stand-in for sync engine tests. It reproduces the
|
|
3
|
-
* event interface of the real connection without opening a socket, so tests
|
|
4
|
-
* can drive connection changes, deltas, and bootstrap hints by hand.
|
|
5
|
-
*/
|
|
6
|
-
/**
|
|
7
|
-
* A controllable, event-based stand-in for the sync engine's WebSocket
|
|
8
|
-
* connection. Subscribe with {@link MockWebSocket.on} exactly as production
|
|
9
|
-
* code does, then use the `simulate*` and `receive*` methods to push
|
|
10
|
-
* connection changes, deltas, and bootstrap hints. Every event the mock emits
|
|
11
|
-
* is recorded on {@link MockWebSocket.emittedEvents} for assertions.
|
|
12
|
-
*/
|
|
13
|
-
export class MockWebSocket {
|
|
14
|
-
_connected = false;
|
|
15
|
-
_sessionError = false;
|
|
16
|
-
_listeners = new Map();
|
|
17
|
-
/** Every event the mock has emitted, in order, for assertions. */
|
|
18
|
-
emittedEvents = [];
|
|
19
|
-
get connected() {
|
|
20
|
-
return this._connected;
|
|
21
|
-
}
|
|
22
|
-
get sessionError() {
|
|
23
|
-
return this._sessionError;
|
|
24
|
-
}
|
|
25
|
-
// ─────────────────────────────────────────────
|
|
26
|
-
// Event subscription (matches the real connection's API)
|
|
27
|
-
// ─────────────────────────────────────────────
|
|
28
|
-
on(event, handler) {
|
|
29
|
-
if (!this._listeners.has(event)) {
|
|
30
|
-
this._listeners.set(event, new Set());
|
|
31
|
-
}
|
|
32
|
-
this._listeners.get(event).add(handler);
|
|
33
|
-
return () => {
|
|
34
|
-
this._listeners.get(event)?.delete(handler);
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
emit(event, ...args) {
|
|
38
|
-
this.emittedEvents.push({ type: event, data: args[0] });
|
|
39
|
-
const handlers = this._listeners.get(event);
|
|
40
|
-
if (handlers) {
|
|
41
|
-
for (const handler of handlers) {
|
|
42
|
-
handler(...args);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
// ─────────────────────────────────────────────
|
|
47
|
-
// Test control: connection lifecycle
|
|
48
|
-
// ─────────────────────────────────────────────
|
|
49
|
-
/** Fires a successful connection, emitting the `connected` event. */
|
|
50
|
-
simulateConnect() {
|
|
51
|
-
this._connected = true;
|
|
52
|
-
this._sessionError = false;
|
|
53
|
-
this.emit('connected');
|
|
54
|
-
}
|
|
55
|
-
/** Drops the connection, emitting the `disconnected` event. */
|
|
56
|
-
simulateDisconnect() {
|
|
57
|
-
this._connected = false;
|
|
58
|
-
this.emit('disconnected');
|
|
59
|
-
}
|
|
60
|
-
/** Emits a `reconnecting` event carrying the attempt number and retry delay. */
|
|
61
|
-
simulateReconnecting(attempt, delay) {
|
|
62
|
-
this.emit('reconnecting', { attempt, delay });
|
|
63
|
-
}
|
|
64
|
-
/** Emits a `session_error` event, as when the server rejects the session as unauthorized. The code defaults to 401. */
|
|
65
|
-
simulateSessionError(code = 401) {
|
|
66
|
-
this._sessionError = true;
|
|
67
|
-
this._connected = false;
|
|
68
|
-
this.emit('session_error', { code });
|
|
69
|
-
}
|
|
70
|
-
/** Emits a `reconnect_failed` event, signaling the client gave up after exhausting its retries. */
|
|
71
|
-
simulateReconnectFailed() {
|
|
72
|
-
this._connected = false;
|
|
73
|
-
this.emit('reconnect_failed');
|
|
74
|
-
}
|
|
75
|
-
// ─────────────────────────────────────────────
|
|
76
|
-
// Test control: delta injection
|
|
77
|
-
// ─────────────────────────────────────────────
|
|
78
|
-
/** Delivers a single delta, emitting a `delta` event as if the server had sent it. */
|
|
79
|
-
receiveDelta(delta) {
|
|
80
|
-
this.emit('delta', delta);
|
|
81
|
-
}
|
|
82
|
-
/** Delivers a batch of deltas, emitting a `delta_batch` event. */
|
|
83
|
-
receiveDeltas(deltas) {
|
|
84
|
-
this.emit('delta_batch', deltas);
|
|
85
|
-
}
|
|
86
|
-
// ─────────────────────────────────────────────
|
|
87
|
-
// Test control: bootstrap hints
|
|
88
|
-
// ─────────────────────────────────────────────
|
|
89
|
-
/** Emits a `bootstrap_required` hint, telling the client to rebuild its data instead of catching up. */
|
|
90
|
-
simulateBootstrapHint(hint) {
|
|
91
|
-
this.emit('bootstrap_required', hint);
|
|
92
|
-
}
|
|
93
|
-
// ─────────────────────────────────────────────
|
|
94
|
-
// Test control: presence
|
|
95
|
-
// ─────────────────────────────────────────────
|
|
96
|
-
/** Emits a `presence_update` event carrying the given payload. */
|
|
97
|
-
simulatePresenceUpdate(data) {
|
|
98
|
-
this.emit('presence_update', data);
|
|
99
|
-
}
|
|
100
|
-
// ─────────────────────────────────────────────
|
|
101
|
-
// Assertions
|
|
102
|
-
// ─────────────────────────────────────────────
|
|
103
|
-
/** Returns the payloads of every emitted event of the given type, in order. */
|
|
104
|
-
getEvents(type) {
|
|
105
|
-
return this.emittedEvents.filter((e) => e.type === type).map((e) => e.data);
|
|
106
|
-
}
|
|
107
|
-
/** Reports whether an event of the given type has been emitted. */
|
|
108
|
-
hasEmitted(type) {
|
|
109
|
-
return this.emittedEvents.some((e) => e.type === type);
|
|
110
|
-
}
|
|
111
|
-
/** Clears connection state, listeners, and the record of emitted events. */
|
|
112
|
-
reset() {
|
|
113
|
-
this._connected = false;
|
|
114
|
-
this._sessionError = false;
|
|
115
|
-
this._listeners.clear();
|
|
116
|
-
this.emittedEvents.length = 0;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
# Interaction Model
|
|
2
|
-
|
|
3
|
-
> The one write path every actor shares: load, claim, update, confirm.
|
|
4
|
-
|
|
5
|
-
When two agents, a server action, and a person can all write to the same row, you
|
|
6
|
-
need one write path that stops them from clobbering each other. Ablo gives you
|
|
7
|
-
exactly one: load the row, claim it while you work, update it, and wait for
|
|
8
|
-
confirmation. This page walks through that path and the few primitives behind it.
|
|
9
|
-
|
|
10
|
-
Here's the whole path in one block — claim a row, update it inside the claim, and
|
|
11
|
-
let the claim release when your callback returns:
|
|
12
|
-
|
|
13
|
-
```ts
|
|
14
|
-
const report = await ablo.weatherReports.retrieve({ id: 'report_stockholm' });
|
|
15
|
-
|
|
16
|
-
await using claim = await ablo.weatherReports.claim({ id: 'report_stockholm' });
|
|
17
|
-
await ablo.weatherReports.update({ id: claim.data.id, data: { status: 'ready' }, wait: 'confirmed' });
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Claims don't lock. If another writer holds the row, `claim` waits for them,
|
|
21
|
-
re-reads the fresh row, then hands it to you — so two writers serialize instead
|
|
22
|
-
of clobbering.
|
|
23
|
-
|
|
24
|
-
## Primitives
|
|
25
|
-
|
|
26
|
-
| Primitive | Plane | Purpose |
|
|
27
|
-
|---|---|---|
|
|
28
|
-
| `Schema` | State | Declares typed models the app and agents can read and write. |
|
|
29
|
-
| `Model` | State | The generated `ablo.<model>` model. Use `retrieve`/`list` (async reads), `local.retrieve`/`local.list`/`local.count` (the same verbs, synchronous and local-only), `create`, `update`, and `delete`. |
|
|
30
|
-
| `Claim` | Coordination | Who is working on a target. Taken via `ablo.<model>.claim({ id })` and read via `ablo.<model>.claim.state({ id })`. Ephemeral — never persisted. |
|
|
31
|
-
| `Commit` | Protocol | The durable write underneath model updates. Most users do not call it directly. |
|
|
32
|
-
| `Receipt` | Protocol | The lower-level durable result for custom runtimes. Schema writes use `wait: 'confirmed'`. |
|
|
33
|
-
|
|
34
|
-
### Why each primitive is separate
|
|
35
|
-
|
|
36
|
-
Why are `Claim`, `Commit`, and `Receipt` separate things instead of one? Each
|
|
37
|
-
does a job the others can't. If you're coming from Replicache or Yjs you'd
|
|
38
|
-
expect just `Commit`; here's what the other two buy you over that minimum:
|
|
39
|
-
|
|
40
|
-
- **`Claim` is not a read lock.** Reads stay open. Claims serialize
|
|
41
|
-
acting-on-the-row, so slow work can wait in FIFO order, re-read, and write
|
|
42
|
-
from fresh state.
|
|
43
|
-
- **`Receipt` is not a `200 OK`.** It's the durable artifact a commit
|
|
44
|
-
produced — accepted commit id, server-assigned timestamps, stale-check
|
|
45
|
-
outcome — addressable after the fact and replayable into a different
|
|
46
|
-
client. A status code can't be re-read by a sub-agent that wasn't on
|
|
47
|
-
the original call.
|
|
48
|
-
|
|
49
|
-
## Run Loop
|
|
50
|
-
|
|
51
|
-
A normal schema-backed run is:
|
|
52
|
-
|
|
53
|
-
```ts
|
|
54
|
-
const report = await ablo.weatherReports.retrieve({ id });
|
|
55
|
-
const active = ablo.weatherReports.claim.state({ id });
|
|
56
|
-
await using claim = await ablo.weatherReports.claim({ id });
|
|
57
|
-
await ablo.weatherReports.update({ id: claim.data.id, data: patch, wait: 'confirmed' });
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
`retrieve({ id })` is an async server read (await it). `claim.state({ id })` is a
|
|
61
|
-
synchronous local read of who currently holds the row — it never blocks.
|
|
62
|
-
|
|
63
|
-
## Coordination
|
|
64
|
-
|
|
65
|
-
> Loop view only. Full claim reference — methods, the claim-state object, the
|
|
66
|
-
> `claim.queue`, errors — is [Coordination](./coordination.md).
|
|
67
|
-
|
|
68
|
-
Claims broadcast across the org. Call `claim({ id })`, do your writes with the
|
|
69
|
-
normal `update` inside the `await using` scope, and the claim releases
|
|
70
|
-
automatically when the scope exits:
|
|
71
|
-
|
|
72
|
-
```ts
|
|
73
|
-
await using claim = await ablo.weatherReports.claim({
|
|
74
|
-
id: 'report_stockholm',
|
|
75
|
-
description: 'editing',
|
|
76
|
-
});
|
|
77
|
-
await ablo.weatherReports.update({ id: claim.data.id, data: { status: 'ready' } }); // rejected if the row changed under the claim
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
`ablo.weatherReports.claim.state({ id: 'report_stockholm' })` reads the live claim (or
|
|
81
|
-
`null`) without blocking. Claims don't lock: if another participant holds the
|
|
82
|
-
row, `claim` waits for them to finish, re-reads, and then hands you the fresh
|
|
83
|
-
row. The same signal is visible to every schema client through `claim.state({ id })`
|
|
84
|
-
and the live claim stream.
|
|
85
|
-
|
|
86
|
-
## Conflict resolution
|
|
87
|
-
|
|
88
|
-
Schema updates can carry `readAt` and `onStale`. If the state advanced past
|
|
89
|
-
`readAt`, Ablo applies the `onStale` policy:
|
|
90
|
-
|
|
91
|
-
- `reject` — fail the commit (first writer wins).
|
|
92
|
-
- `notify` — accept the write, but flag it for product review.
|
|
93
|
-
- `overwrite` — apply the write unconditionally.
|
|
94
|
-
|
|
95
|
-
The choice is per-commit. No CRDT default; the policy is explicit.
|
|
96
|
-
|
|
97
|
-
## The contract in one sentence
|
|
98
|
-
|
|
99
|
-
Declare schema, load state, coordinate a claim, update the model, and wait for confirmation.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|