@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.
Files changed (215) hide show
  1. package/CHANGELOG.md +713 -629
  2. package/README.md +56 -519
  3. package/bin/ablo.cjs +39 -0
  4. package/dist/BaseSyncedStore.d.ts +24 -4
  5. package/dist/BaseSyncedStore.js +53 -37
  6. package/dist/Database.d.ts +8 -20
  7. package/dist/Database.js +61 -59
  8. package/dist/InstanceCache.d.ts +6 -2
  9. package/dist/InstanceCache.js +18 -16
  10. package/dist/Model.d.ts +17 -7
  11. package/dist/Model.js +17 -7
  12. package/dist/ModelRegistry.d.ts +4 -0
  13. package/dist/ModelRegistry.js +17 -15
  14. package/dist/NetworkMonitor.d.ts +3 -1
  15. package/dist/NetworkMonitor.js +7 -5
  16. package/dist/SyncClient.d.ts +5 -15
  17. package/dist/SyncClient.js +60 -57
  18. package/dist/client/Ablo.js +30 -19
  19. package/dist/client/createInternalComponents.d.ts +4 -0
  20. package/dist/client/createInternalComponents.js +8 -2
  21. package/dist/client/createModelProxy.d.ts +21 -1
  22. package/dist/client/createModelProxy.js +123 -57
  23. package/dist/client/humans.d.ts +30 -9
  24. package/dist/client/humans.js +45 -19
  25. package/dist/client/reactiveEngine.d.ts +16 -11
  26. package/dist/client/reactiveEngine.js +113 -335
  27. package/dist/client/storeCluster.d.ts +47 -0
  28. package/dist/client/storeCluster.js +118 -0
  29. package/dist/client/storeLifecycle.d.ts +61 -0
  30. package/dist/client/storeLifecycle.js +231 -0
  31. package/dist/context.d.ts +13 -0
  32. package/dist/context.js +23 -0
  33. package/dist/core/index.d.ts +1 -1
  34. package/dist/docs/catalog.js +6 -3
  35. package/dist/index.d.ts +4 -2
  36. package/dist/index.js +4 -2
  37. package/dist/query/client.d.ts +3 -0
  38. package/dist/query/client.js +6 -5
  39. package/dist/react/AbloProvider.d.ts +13 -1
  40. package/dist/react/AbloProvider.js +5 -2
  41. package/dist/react/context.d.ts +2 -2
  42. package/dist/react/createAbloReact.d.ts +56 -0
  43. package/dist/react/createAbloReact.js +51 -0
  44. package/dist/react/index.d.ts +1 -0
  45. package/dist/react/index.js +3 -0
  46. package/dist/react/useAblo.d.ts +9 -2
  47. package/dist/react/useAblo.js +25 -7
  48. package/dist/schema/coordination.js +5 -1
  49. package/dist/schema/index.d.ts +1 -0
  50. package/dist/schema/index.js +4 -0
  51. package/dist/schema/select.js +3 -0
  52. package/dist/schema/serialize.js +3 -0
  53. package/dist/source/adapter.d.ts +7 -5
  54. package/dist/source/adapter.js +7 -5
  55. package/dist/source/index.d.ts +1 -1
  56. package/dist/source/index.js +1 -1
  57. package/dist/{core/storeContract.d.ts → storeContract.d.ts} +5 -5
  58. package/dist/{core → stores}/DatabaseManager.d.ts +3 -1
  59. package/dist/{core → stores}/DatabaseManager.js +13 -12
  60. package/dist/{core → stores}/StoreManager.d.ts +5 -3
  61. package/dist/{core → stores}/StoreManager.js +24 -22
  62. package/dist/stores/SyncActionStore.d.ts +3 -1
  63. package/dist/stores/SyncActionStore.js +9 -7
  64. package/dist/sync/BootstrapFetcher.d.ts +4 -0
  65. package/dist/sync/BootstrapFetcher.js +28 -26
  66. package/dist/sync/OnDemandLoader.d.ts +3 -0
  67. package/dist/sync/OnDemandLoader.js +1 -0
  68. package/dist/sync/bootstrapApply.d.ts +3 -0
  69. package/dist/sync/bootstrapApply.js +2 -2
  70. package/dist/sync/deltaPipeline.d.ts +13 -12
  71. package/dist/sync/deltaPipeline.js +21 -4
  72. package/dist/sync/groupChange.d.ts +3 -0
  73. package/dist/sync/groupChange.js +16 -14
  74. package/dist/sync/participants.d.ts +19 -2
  75. package/dist/sync/participants.js +3 -1
  76. package/dist/sync/schemas.d.ts +2 -1
  77. package/dist/sync/schemas.js +3 -3
  78. package/dist/syncLog/contract.d.ts +20 -0
  79. package/dist/syncLog/contract.js +19 -0
  80. package/dist/syncLog/index.d.ts +1 -0
  81. package/dist/syncLog/index.js +1 -0
  82. package/dist/transaction/auth/capability.d.ts +35 -0
  83. package/dist/transaction/auth/capability.js +25 -0
  84. package/dist/transaction/coordination/awaitClaimGrant.d.ts +7 -0
  85. package/dist/transaction/coordination/awaitClaimGrant.js +12 -0
  86. package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +34 -0
  87. package/dist/transaction/coordination/claimHeartbeatLoop.js +20 -0
  88. package/dist/transaction/coordination/index.d.ts +4 -4
  89. package/dist/transaction/coordination/index.js +4 -3
  90. package/dist/transaction/coordination/locator.d.ts +23 -2
  91. package/dist/transaction/coordination/locator.js +22 -2
  92. package/dist/transaction/coordination/schema.d.ts +125 -62
  93. package/dist/transaction/coordination/schema.js +228 -64
  94. package/dist/transaction/coordination/targetConflict.js +32 -28
  95. package/dist/transaction/errorCodes.d.ts +2 -2
  96. package/dist/transaction/errorCodes.js +13 -9
  97. package/dist/transaction/plugin.d.ts +95 -2
  98. package/dist/transaction/plugin.js +21 -2
  99. package/dist/transaction/resources/httpResources.d.ts +57 -2
  100. package/dist/transaction/resources/modelOperations.d.ts +148 -40
  101. package/dist/transaction/resources/where.d.ts +16 -0
  102. package/dist/transaction/resources/where.js +45 -0
  103. package/dist/transaction/schema/field.d.ts +12 -18
  104. package/dist/transaction/schema/fieldRef.d.ts +38 -0
  105. package/dist/transaction/schema/fieldRef.js +11 -0
  106. package/dist/transaction/schema/openapi.d.ts +16 -15
  107. package/dist/transaction/schema/openapi.js +186 -25
  108. package/dist/transaction/schema/relation.d.ts +7 -2
  109. package/dist/transaction/schema/schema.d.ts +27 -0
  110. package/dist/transaction/schema/schema.js +20 -0
  111. package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +1 -1
  112. package/dist/transaction/transactions/settlement/pendingWrite.d.ts +1 -1
  113. package/dist/transaction/transport/httpClient.d.ts +9 -1
  114. package/dist/transaction/transport/httpClient.js +1 -0
  115. package/dist/transaction/transport/httpTransport.js +156 -44
  116. package/dist/transaction/transport/wsTransport.d.ts +2 -4
  117. package/dist/transaction/transport/wsTransport.js +16 -10
  118. package/dist/transaction/types/streams.d.ts +10 -0
  119. package/dist/transaction/utils/duration.d.ts +25 -0
  120. package/dist/transaction/utils/duration.js +32 -0
  121. package/dist/transaction/wire/accountResponses.d.ts +69 -0
  122. package/dist/transaction/wire/accountResponses.js +36 -1
  123. package/dist/transaction/wire/auth.d.ts +9 -2
  124. package/dist/transaction/wire/auth.js +7 -1
  125. package/dist/transaction/wire/claims.d.ts +164 -97
  126. package/dist/transaction/wire/claims.js +126 -28
  127. package/dist/transaction/wire/commit.d.ts +1 -1
  128. package/dist/transaction/wire/feedEvent.d.ts +27 -0
  129. package/dist/transaction/wire/feedEvent.js +27 -1
  130. package/dist/transaction/wire/frames.d.ts +2 -2
  131. package/dist/transaction/wire/inboundFrames.d.ts +12 -2
  132. package/dist/transaction/wire/index.d.ts +10 -6
  133. package/dist/transaction/wire/index.js +12 -3
  134. package/dist/transaction/wire/modelMutations.d.ts +31 -0
  135. package/dist/transaction/wire/modelMutations.js +52 -0
  136. package/dist/transaction/wire/modelShape.d.ts +78 -0
  137. package/dist/transaction/wire/modelShape.js +74 -0
  138. package/dist/transactions/mutations/MutationQueue.d.ts +6 -0
  139. package/dist/transactions/mutations/MutationQueue.js +55 -45
  140. package/dist/transactions/mutations/commitPayload.d.ts +3 -2
  141. package/dist/transactions/mutations/commitPayload.js +5 -5
  142. package/dist/transactions/mutations/deltaConfirmation.d.ts +4 -0
  143. package/dist/transactions/mutations/deltaConfirmation.js +10 -8
  144. package/dist/transactions/mutations/replayValidation.d.ts +2 -1
  145. package/dist/transactions/mutations/replayValidation.js +3 -2
  146. package/dist/{core → views}/QueryView.d.ts +1 -1
  147. package/dist/{core → views}/QueryView.js +1 -1
  148. package/dist/{core → views}/ViewRegistry.d.ts +1 -1
  149. package/dist/{core/queryUtils.d.ts → views/incrementalView.d.ts} +6 -6
  150. package/dist/{core/queryUtils.js → views/incrementalView.js} +6 -6
  151. package/docs/agents.md +1 -1
  152. package/docs/api-keys.md +6 -6
  153. package/docs/api.md +5 -43
  154. package/docs/audit.md +4 -3
  155. package/docs/cli.md +11 -11
  156. package/docs/client-behavior.md +4 -4
  157. package/docs/concurrency-convention.md +28 -42
  158. package/docs/coordination.md +235 -83
  159. package/docs/data-sources.md +4 -4
  160. package/docs/debugging.md +34 -12
  161. package/docs/deployment.md +8 -8
  162. package/docs/examples/scoped-agent.md +3 -3
  163. package/docs/groups.md +57 -3
  164. package/docs/guarantees.md +37 -10
  165. package/docs/how-it-works.md +29 -5
  166. package/docs/idempotency.md +6 -6
  167. package/docs/identity.md +22 -23
  168. package/docs/index.md +8 -8
  169. package/docs/integration-guide.md +14 -3
  170. package/docs/mcp.md +7 -7
  171. package/docs/migration.md +34 -15
  172. package/docs/projects.md +1 -1
  173. package/docs/react.md +19 -8
  174. package/docs/sessions.md +1 -1
  175. package/docs/webhooks.md +9 -9
  176. package/llms.txt +4 -4
  177. package/package.json +13 -20
  178. package/dist/cli.cjs +0 -288600
  179. package/dist/testing/fixtures/bootstrap.d.ts +0 -49
  180. package/dist/testing/fixtures/bootstrap.js +0 -59
  181. package/dist/testing/fixtures/deltas.d.ts +0 -83
  182. package/dist/testing/fixtures/deltas.js +0 -136
  183. package/dist/testing/fixtures/httpResponses.d.ts +0 -70
  184. package/dist/testing/fixtures/httpResponses.js +0 -90
  185. package/dist/testing/fixtures/models.d.ts +0 -83
  186. package/dist/testing/fixtures/models.js +0 -272
  187. package/dist/testing/helpers/reactWrapper.d.ts +0 -69
  188. package/dist/testing/helpers/reactWrapper.js +0 -67
  189. package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
  190. package/dist/testing/helpers/syncEngineHarness.js +0 -73
  191. package/dist/testing/helpers/wait.d.ts +0 -30
  192. package/dist/testing/helpers/wait.js +0 -49
  193. package/dist/testing/index.d.ts +0 -23
  194. package/dist/testing/index.js +0 -33
  195. package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
  196. package/dist/testing/mocks/FakeDatabase.js +0 -10
  197. package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
  198. package/dist/testing/mocks/MockMutationExecutor.js +0 -186
  199. package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
  200. package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
  201. package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
  202. package/dist/testing/mocks/MockSyncContext.js +0 -72
  203. package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
  204. package/dist/testing/mocks/MockSyncStore.js +0 -171
  205. package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
  206. package/dist/testing/mocks/MockWebSocket.js +0 -118
  207. package/docs/interaction-model.md +0 -99
  208. /package/dist/{core → query}/QueryProcessor.d.ts +0 -0
  209. /package/dist/{core → query}/QueryProcessor.js +0 -0
  210. /package/dist/{core/storeContract.js → storeContract.js} +0 -0
  211. /package/dist/{core → stores}/openIDBWithTimeout.d.ts +0 -0
  212. /package/dist/{core → stores}/openIDBWithTimeout.js +0 -0
  213. /package/dist/{source → transaction}/footprint.d.ts +0 -0
  214. /package/dist/{source → transaction}/footprint.js +0 -0
  215. /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