@abloatai/transaction 0.51.0 → 0.52.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 (164) hide show
  1. package/dist/ablo.d.ts +1 -1
  2. package/dist/ablo.js +1 -1
  3. package/dist/ai-sdk/updateTool.d.ts +1 -1
  4. package/dist/ai-sdk/updateTool.js +1 -1
  5. package/dist/auth/capability.d.ts +2 -2
  6. package/dist/auth/capability.js +1 -1
  7. package/dist/auth/index.d.ts +6 -1
  8. package/dist/auth/index.d.ts.map +1 -1
  9. package/dist/auth/index.js +25 -20
  10. package/dist/auth/index.js.map +1 -1
  11. package/dist/auth/schemas.d.ts +24 -1
  12. package/dist/auth/schemas.d.ts.map +1 -1
  13. package/dist/auth/schemas.js +8 -2
  14. package/dist/auth/schemas.js.map +1 -1
  15. package/dist/auth/sessionIssuanceProtocol.d.ts +41 -0
  16. package/dist/auth/sessionIssuanceProtocol.d.ts.map +1 -0
  17. package/dist/auth/sessionIssuanceProtocol.js +48 -0
  18. package/dist/auth/sessionIssuanceProtocol.js.map +1 -0
  19. package/dist/auth/sessionMint.js +2 -2
  20. package/dist/branches.d.ts +4 -4
  21. package/dist/coordination/schema.d.ts +3 -2
  22. package/dist/coordination/schema.d.ts.map +1 -1
  23. package/dist/coordination/schema.js +7 -2
  24. package/dist/coordination/schema.js.map +1 -1
  25. package/dist/errorCodes.d.ts +3 -2
  26. package/dist/errorCodes.d.ts.map +1 -1
  27. package/dist/errorCodes.js +4 -3
  28. package/dist/errorCodes.js.map +1 -1
  29. package/dist/errors.d.ts +1 -1
  30. package/dist/errors.js +1 -1
  31. package/dist/resources/httpResources.d.ts +3 -2
  32. package/dist/resources/httpResources.d.ts.map +1 -1
  33. package/dist/resources/modelOperations.d.ts +6 -6
  34. package/dist/schema/ddl.d.ts.map +1 -1
  35. package/dist/schema/ddl.js +9 -12
  36. package/dist/schema/ddl.js.map +1 -1
  37. package/dist/schema/field.d.ts +3 -3
  38. package/dist/schema/field.js +2 -2
  39. package/dist/schema/fieldRef.d.ts +1 -1
  40. package/dist/schema/generate.d.ts +2 -3
  41. package/dist/schema/generate.d.ts.map +1 -1
  42. package/dist/schema/generate.js +2 -8
  43. package/dist/schema/generate.js.map +1 -1
  44. package/dist/schema/index.d.ts +2 -2
  45. package/dist/schema/index.js +3 -3
  46. package/dist/schema/model.d.ts +7 -7
  47. package/dist/schema/model.js +2 -2
  48. package/dist/schema/openapi.d.ts.map +1 -1
  49. package/dist/schema/openapi.js +4 -7
  50. package/dist/schema/openapi.js.map +1 -1
  51. package/dist/schema/queries.d.ts +1 -1
  52. package/dist/schema/queries.js +1 -1
  53. package/dist/schema/relation.d.ts +8 -8
  54. package/dist/schema/relation.js +5 -5
  55. package/dist/schema/schema.d.ts +24 -37
  56. package/dist/schema/schema.d.ts.map +1 -1
  57. package/dist/schema/schema.js +15 -28
  58. package/dist/schema/schema.js.map +1 -1
  59. package/dist/schema/sugar.d.ts +7 -7
  60. package/dist/schema/sugar.js +5 -5
  61. package/dist/server/commit.d.ts +5 -1
  62. package/dist/server/commit.d.ts.map +1 -1
  63. package/dist/source/adapters/drizzle.d.ts.map +1 -1
  64. package/dist/source/adapters/drizzle.js +5 -0
  65. package/dist/source/adapters/drizzle.js.map +1 -1
  66. package/dist/source/adapters/kysely.d.ts.map +1 -1
  67. package/dist/source/adapters/kysely.js +33 -8
  68. package/dist/source/adapters/kysely.js.map +1 -1
  69. package/dist/source/adapters/kyselyMutationCore.d.ts +1 -1
  70. package/dist/source/adapters/kyselyMutationCore.d.ts.map +1 -1
  71. package/dist/source/adapters/kyselyMutationCore.js +58 -20
  72. package/dist/source/adapters/kyselyMutationCore.js.map +1 -1
  73. package/dist/source/adapters/memory.d.ts.map +1 -1
  74. package/dist/source/adapters/memory.js +5 -0
  75. package/dist/source/adapters/memory.js.map +1 -1
  76. package/dist/source/adapters/prisma.d.ts.map +1 -1
  77. package/dist/source/adapters/prisma.js +6 -1
  78. package/dist/source/adapters/prisma.js.map +1 -1
  79. package/dist/source/conformance.js +21 -21
  80. package/dist/source/contract.d.ts +17 -0
  81. package/dist/source/contract.d.ts.map +1 -1
  82. package/dist/source/contract.js +31 -0
  83. package/dist/source/contract.js.map +1 -1
  84. package/dist/source/index.d.ts +1 -1
  85. package/dist/source/index.d.ts.map +1 -1
  86. package/dist/source/index.js +1 -1
  87. package/dist/source/index.js.map +1 -1
  88. package/dist/source/types.d.ts +1 -0
  89. package/dist/source/types.d.ts.map +1 -1
  90. package/dist/source/types.js.map +1 -1
  91. package/dist/transactionLayer.d.ts +3 -3
  92. package/dist/transactionLayer.js +3 -3
  93. package/dist/transport/httpClient.d.ts +6 -2
  94. package/dist/transport/httpClient.d.ts.map +1 -1
  95. package/dist/transport/httpClient.js.map +1 -1
  96. package/dist/transport/httpTransport.d.ts +1 -0
  97. package/dist/transport/httpTransport.d.ts.map +1 -1
  98. package/dist/transport/httpTransport.js +50 -4
  99. package/dist/transport/httpTransport.js.map +1 -1
  100. package/dist/transport/wsFrameHandlers.d.ts.map +1 -1
  101. package/dist/transport/wsFrameHandlers.js +3 -0
  102. package/dist/transport/wsFrameHandlers.js.map +1 -1
  103. package/dist/types/streams.d.ts +2 -2
  104. package/dist/wire/accountResponses.js +2 -2
  105. package/dist/wire/accountResponses.js.map +1 -1
  106. package/dist/wire/commit.d.ts +144 -0
  107. package/dist/wire/commit.d.ts.map +1 -1
  108. package/dist/wire/commit.js +22 -1
  109. package/dist/wire/commit.js.map +1 -1
  110. package/dist/wire/frames.d.ts +4 -0
  111. package/dist/wire/frames.d.ts.map +1 -1
  112. package/dist/wire/inboundFrames.d.ts +22 -0
  113. package/dist/wire/inboundFrames.d.ts.map +1 -1
  114. package/dist/wire/index.d.ts +2 -2
  115. package/dist/wire/index.d.ts.map +1 -1
  116. package/dist/wire/index.js +1 -1
  117. package/dist/wire/index.js.map +1 -1
  118. package/dist/wire/protocol.d.ts +1 -1
  119. package/dist/wire/protocol.js +1 -1
  120. package/package.json +1 -1
  121. package/src/ablo.ts +1 -1
  122. package/src/ai-sdk/updateTool.ts +1 -1
  123. package/src/auth/capability.ts +3 -3
  124. package/src/auth/index.ts +51 -20
  125. package/src/auth/schemas.ts +16 -2
  126. package/src/auth/sessionIssuanceProtocol.ts +57 -0
  127. package/src/auth/sessionMint.ts +2 -2
  128. package/src/coordination/schema.ts +7 -2
  129. package/src/errorCodes.ts +10 -4
  130. package/src/errors.ts +1 -1
  131. package/src/resources/httpResources.ts +3 -2
  132. package/src/resources/modelOperations.ts +6 -6
  133. package/src/schema/ddl.ts +9 -13
  134. package/src/schema/field.ts +3 -3
  135. package/src/schema/fieldRef.ts +1 -1
  136. package/src/schema/generate.ts +2 -8
  137. package/src/schema/index.ts +3 -3
  138. package/src/schema/model.ts +7 -7
  139. package/src/schema/openapi.ts +4 -7
  140. package/src/schema/queries.ts +1 -1
  141. package/src/schema/relation.ts +8 -8
  142. package/src/schema/residency.ts +0 -1
  143. package/src/schema/schema.ts +33 -58
  144. package/src/schema/sugar.ts +7 -7
  145. package/src/server/commit.ts +5 -1
  146. package/src/source/adapters/drizzle.ts +5 -0
  147. package/src/source/adapters/kysely.ts +48 -10
  148. package/src/source/adapters/kyselyMutationCore.ts +69 -22
  149. package/src/source/adapters/memory.ts +5 -0
  150. package/src/source/adapters/prisma.ts +6 -1
  151. package/src/source/conformance.ts +21 -21
  152. package/src/source/contract.ts +34 -0
  153. package/src/source/index.ts +2 -0
  154. package/src/source/types.ts +1 -0
  155. package/src/transactionLayer.ts +3 -3
  156. package/src/transport/httpClient.ts +6 -2
  157. package/src/transport/httpTransport.ts +59 -4
  158. package/src/transport/wsFrameHandlers.ts +3 -0
  159. package/src/types/global.ts +1 -1
  160. package/src/types/streams.ts +2 -2
  161. package/src/wire/accountResponses.ts +2 -2
  162. package/src/wire/commit.ts +31 -1
  163. package/src/wire/index.ts +3 -0
  164. package/src/wire/protocol.ts +1 -1
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Reusable Kysely row-mutation core.
3
3
  *
4
- * This is the single schema-field ↔ physical-column translation used by both
4
+ * This is the single schema-field ↔ database-column translation used by both
5
5
  * Kysely transports. It deliberately knows nothing about outboxes, logical
6
6
  * markers, or idempotency: wrappers compose those transaction policies around
7
7
  * the same `applyOperation` implementation, so direct and endpoint DML cannot
@@ -133,6 +133,11 @@ export function kyselyOperationRowId(operation: Operation): string {
133
133
  return id;
134
134
  }
135
135
 
136
+ function suppliedOperationRowId(operation: Operation): string | undefined {
137
+ const id = operation.id ?? operation.input?.id;
138
+ return typeof id === 'string' && id.length > 0 ? id : undefined;
139
+ }
140
+
136
141
  /** The transport-independent Kysely field/column mutation boundary. */
137
142
  export interface KyselyMutationCore {
138
143
  read(request: AdapterReadRequest): Promise<readonly Row[]>;
@@ -159,16 +164,23 @@ export function createKyselyMutationCore<S extends SchemaRecord>(
159
164
  columns.fieldToColumn.get(field) ?? camelToSnake(field);
160
165
  const fieldFor = (columns: ModelColumns, column: string): string =>
161
166
  columns.columnToField.get(column) ?? snakeToCamel(column);
162
-
163
167
  const toColumns = (columns: ModelColumns, row: Row): Row => {
164
168
  const out: Row = {};
165
- for (const key of Object.keys(row)) out[columnFor(columns, key)] = row[key];
169
+ for (const key of Object.keys(row)) {
170
+ out[columnFor(columns, key)] = row[key];
171
+ }
166
172
  return out;
167
173
  };
168
174
 
169
175
  const toFields = (columns: ModelColumns, row: Row): Row => {
170
176
  const out: Row = {};
171
- for (const key of Object.keys(row)) out[fieldFor(columns, key)] = row[key];
177
+ for (const key of Object.keys(row)) {
178
+ const field = fieldFor(columns, key);
179
+ const value = row[key];
180
+ out[field] = field === 'id' && value !== null && value !== undefined
181
+ ? String(value)
182
+ : value;
183
+ }
172
184
  return out;
173
185
  };
174
186
 
@@ -179,7 +191,7 @@ export function createKyselyMutationCore<S extends SchemaRecord>(
179
191
  const rows = await db
180
192
  .selectFrom(columns.table)
181
193
  .selectAll()
182
- .where('id', '=', request.id)
194
+ .where(columnFor(columns, 'id'), '=', request.id)
183
195
  .limit(1)
184
196
  .execute();
185
197
  return rows.map((row) => toFields(columns, row));
@@ -194,13 +206,53 @@ export function createKyselyMutationCore<S extends SchemaRecord>(
194
206
 
195
207
  async applyOperation(transaction, operation): Promise<Row> {
196
208
  const columns = modelColumns(operation.model);
197
- const id = kyselyOperationRowId(operation);
198
209
  const input = operation.input ?? {};
210
+ if (operation.where && operation.type !== 'UPDATE') {
211
+ throw new AbloValidationError('where is supported only for UPDATE operations', {
212
+ code: 'commit_operation_invalid',
213
+ param: 'where',
214
+ });
215
+ }
216
+
217
+ if (operation.type === 'CREATE') {
218
+ const suppliedId = suppliedOperationRowId(operation);
219
+ const createInput = operation.id == null
220
+ ? Object.fromEntries(Object.entries(input).filter(([field]) => field !== 'id'))
221
+ : input;
222
+ const inserted = await transaction
223
+ .insertInto(columns.table)
224
+ .values(toColumns(columns, {
225
+ ...createInput,
226
+ ...(operation.id == null
227
+ ? {}
228
+ : suppliedId
229
+ ? { id: suppliedId }
230
+ : {}),
231
+ }))
232
+ .returningAll()
233
+ .execute();
234
+ if (!inserted[0]) {
235
+ throw new AbloValidationError(
236
+ `${operation.type} on "${operation.model}" returned no source row`,
237
+ { code: 'source_adapter_misconfigured' },
238
+ );
239
+ }
240
+ const row = toFields(columns, inserted[0]);
241
+ if (typeof row.id !== 'string' || row.id.length === 0) {
242
+ throw new AbloValidationError(
243
+ `${operation.type} on "${operation.model}" did not return a canonical id`,
244
+ { code: 'source_adapter_misconfigured' },
245
+ );
246
+ }
247
+ return row;
248
+ }
249
+
250
+ const id = kyselyOperationRowId(operation);
199
251
 
200
252
  if (operation.type === 'DELETE') {
201
253
  const deleted = await transaction
202
254
  .deleteFrom(columns.table)
203
- .where('id', '=', id)
255
+ .where(columnFor(columns, 'id'), '=', id)
204
256
  .returningAll()
205
257
  .execute();
206
258
  if (!deleted[0]) {
@@ -212,30 +264,25 @@ export function createKyselyMutationCore<S extends SchemaRecord>(
212
264
  return toFields(columns, deleted[0]);
213
265
  }
214
266
 
215
- if (operation.type === 'CREATE') {
216
- const inserted = await transaction
217
- .insertInto(columns.table)
218
- .values(toColumns(columns, { id, ...input }))
219
- .returningAll()
220
- .execute();
221
- return inserted[0] ? toFields(columns, inserted[0]) : { id, ...input };
222
- }
223
-
224
267
  const patch = toColumns(columns, {
225
268
  ...input,
226
269
  ...(operation.type === 'ARCHIVE' ? { archivedAt: new Date() } : {}),
227
270
  ...(operation.type === 'UNARCHIVE' ? { archivedAt: null } : {}),
228
271
  });
229
- const updated = await transaction
272
+ let update = transaction
230
273
  .updateTable(columns.table)
231
274
  .set(patch)
232
- .where('id', '=', id)
233
- .returningAll()
234
- .execute();
275
+ .where(columnFor(columns, 'id'), '=', id);
276
+ for (const [column, value] of Object.entries(toColumns(columns, operation.where ?? {}))) {
277
+ update = update.where(column, '=', value);
278
+ }
279
+ const updated = await update.returningAll().execute();
235
280
  if (!updated[0]) {
236
281
  throw new AbloValidationError(
237
- `${operation.type} on "${operation.model}/${id}" matched no source row`,
238
- { code: 'mutate_update_entity_not_found' },
282
+ operation.where
283
+ ? `The conditional ${operation.type} on "${operation.model}/${id}" did not match`
284
+ : `${operation.type} on "${operation.model}/${id}" matched no source row`,
285
+ { code: operation.where ? 'precondition_failed' : 'mutate_update_entity_not_found' },
239
286
  );
240
287
  }
241
288
  return toFields(columns, updated[0]);
@@ -54,6 +54,11 @@ export function memoryDataSource(): DataSourceAdapter {
54
54
  };
55
55
 
56
56
  const applyOperation = (op: Operation): Row => {
57
+ if (op.where) {
58
+ throw new AbloValidationError('The memory adapter does not support conditional operations', {
59
+ code: 'source_adapter_misconfigured',
60
+ });
61
+ }
57
62
  const m = modelStore(op.model);
58
63
  const id = rowId(op);
59
64
  switch (op.type) {
@@ -80,7 +80,7 @@ const lowerFirst = (s: string): string => (s ? s.charAt(0).toLowerCase() + s.sli
80
80
  * the adapter, and it reflects a real limit of the type system rather than a
81
81
  * shortcut. Writes inside `prisma.$transaction(tx => …)` must go through the
82
82
  * transactional client `tx`, and the model is known only as a runtime string.
83
- * Prisma keys its client by fixed property names (`{ task: TaskDelegate; … }`), so
83
+ * Prisma keys its client by fixed property names (`{ item: ItemDelegate; … }`), so
84
84
  * a dynamic `tx[name]` lookup is `unknown` to the compiler: there is no static key
85
85
  * to infer from a string. The cast is checked at runtime immediately afterward by
86
86
  * confirming that `findMany` is a function on the resolved delegate.
@@ -149,6 +149,11 @@ export function prismaDataSource<S extends SchemaRecord>(
149
149
  void schema; // held for typed reads and model validation
150
150
 
151
151
  const applyOperation = async (tx: PrismaRaw, op: Operation): Promise<Row> => {
152
+ if (op.where) {
153
+ throw new AbloValidationError('The Prisma adapter does not support conditional operations', {
154
+ code: 'source_adapter_misconfigured',
155
+ });
156
+ }
152
157
  const delegate = delegateFor(tx, delegateName(op.model));
153
158
  const id = rowId(op);
154
159
  switch (op.type) {
@@ -75,7 +75,7 @@ export function mutationConformanceChecks(
75
75
  const adapter = await make();
76
76
  const result = await adapter.commit(
77
77
  request('corr_create', [
78
- { type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } },
78
+ { type: 'CREATE', model: 'item', id: 't1', input: { title: 'A' } },
79
79
  ]),
80
80
  );
81
81
  assert.equal(result.rows.length, 1, 'one row returned');
@@ -90,12 +90,12 @@ export function mutationConformanceChecks(
90
90
  run: async () => {
91
91
  const adapter = await make();
92
92
  await adapter.commit(request('corr_load', [
93
- { type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } },
93
+ { type: 'CREATE', model: 'item', id: 't1', input: { title: 'A' } },
94
94
  ]));
95
- const found = await adapter.read({ kind: 'load', model: 'task', id: 't1' });
95
+ const found = await adapter.read({ kind: 'load', model: 'item', id: 't1' });
96
96
  assert.equal(found.length, 1);
97
97
  assert.equal(found[0]?.title, 'A');
98
- const missing = await adapter.read({ kind: 'load', model: 'task', id: 'nope' });
98
+ const missing = await adapter.read({ kind: 'load', model: 'item', id: 'nope' });
99
99
  assert.equal(missing.length, 0, 'unknown id reads empty');
100
100
  },
101
101
  },
@@ -104,10 +104,10 @@ export function mutationConformanceChecks(
104
104
  run: async () => {
105
105
  const adapter = await make();
106
106
  await adapter.commit(request('corr_list', [
107
- { type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } },
108
- { type: 'CREATE', model: 'task', id: 't2', input: { title: 'B' } },
107
+ { type: 'CREATE', model: 'item', id: 't1', input: { title: 'A' } },
108
+ { type: 'CREATE', model: 'item', id: 't2', input: { title: 'B' } },
109
109
  ]));
110
- const rows = await adapter.read({ kind: 'list', model: 'task' });
110
+ const rows = await adapter.read({ kind: 'list', model: 'item' });
111
111
  assert.deepEqual(rows.map((row) => row.id).sort(), ['t1', 't2']);
112
112
  },
113
113
  },
@@ -116,12 +116,12 @@ export function mutationConformanceChecks(
116
116
  run: async () => {
117
117
  const adapter = await make();
118
118
  const commitRequest = request('corr_replay', [
119
- { type: 'CREATE', model: 'task', id: 't1', input: { title: 'A', n: 1 } },
119
+ { type: 'CREATE', model: 'item', id: 't1', input: { title: 'A', n: 1 } },
120
120
  ]);
121
121
  const first = await adapter.commit(commitRequest);
122
122
  const second = await adapter.commit(commitRequest);
123
123
  assert.deepEqual(second.rows, first.rows, 'replay returns the original rows');
124
- const rows = await adapter.read({ kind: 'list', model: 'task' });
124
+ const rows = await adapter.read({ kind: 'list', model: 'item' });
125
125
  assert.equal(rows.length, 1, 'no duplicate row');
126
126
  },
127
127
  },
@@ -131,13 +131,13 @@ export function mutationConformanceChecks(
131
131
  const adapter = await make();
132
132
  await adapter.commit(request(
133
133
  'corr_conflict',
134
- [{ type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } }],
134
+ [{ type: 'CREATE', model: 'item', id: 't1', input: { title: 'A' } }],
135
135
  'a'.repeat(64),
136
136
  ));
137
137
  await assert.rejects(
138
138
  adapter.commit(request(
139
139
  'corr_conflict',
140
- [{ type: 'UPDATE', model: 'task', id: 't1', input: { title: 'B' } }],
140
+ [{ type: 'UPDATE', model: 'item', id: 't1', input: { title: 'B' } }],
141
141
  'b'.repeat(64),
142
142
  )),
143
143
  (error: unknown) =>
@@ -153,14 +153,14 @@ export function mutationConformanceChecks(
153
153
  run: async () => {
154
154
  const adapter = await make();
155
155
  const commitRequest = request('corr_concurrent', [
156
- { type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } },
156
+ { type: 'CREATE', model: 'item', id: 't1', input: { title: 'A' } },
157
157
  ]);
158
158
  const [first, second] = await Promise.all([
159
159
  adapter.commit(commitRequest),
160
160
  adapter.commit(commitRequest),
161
161
  ]);
162
162
  assert.deepEqual(second.rows, first.rows);
163
- const rows = await adapter.read({ kind: 'list', model: 'task' });
163
+ const rows = await adapter.read({ kind: 'list', model: 'item' });
164
164
  assert.equal(rows.length, 1, 'concurrent retries produced one row');
165
165
  },
166
166
  },
@@ -169,12 +169,12 @@ export function mutationConformanceChecks(
169
169
  run: async () => {
170
170
  const adapter = await make();
171
171
  await adapter.commit(request('corr_create_2', [
172
- { type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } },
172
+ { type: 'CREATE', model: 'item', id: 't1', input: { title: 'A' } },
173
173
  ]));
174
174
  await adapter.commit(request('corr_update_2', [
175
- { type: 'UPDATE', model: 'task', id: 't1', input: { title: 'B' } },
175
+ { type: 'UPDATE', model: 'item', id: 't1', input: { title: 'B' } },
176
176
  ]));
177
- const found = await adapter.read({ kind: 'load', model: 'task', id: 't1' });
177
+ const found = await adapter.read({ kind: 'load', model: 'item', id: 't1' });
178
178
  assert.equal(found[0]?.title, 'B', 'update applied');
179
179
  },
180
180
  },
@@ -200,7 +200,7 @@ export function endpointConformanceChecks(make: MakeAdapter): ConformanceCheck[]
200
200
  const request = change('corr_outbox_replay', [
201
201
  {
202
202
  type: 'CREATE',
203
- model: 'task',
203
+ model: 'item',
204
204
  id: 't1',
205
205
  input: { title: 'A' },
206
206
  transactionId: 'op_outbox_replay',
@@ -223,7 +223,7 @@ export function endpointConformanceChecks(make: MakeAdapter): ConformanceCheck[]
223
223
  await adapter.commit(change('corr_event', [
224
224
  {
225
225
  type: 'CREATE',
226
- model: 'task',
226
+ model: 'item',
227
227
  id: 't1',
228
228
  input: { title: 'A' },
229
229
  transactionId: 'op_event',
@@ -232,7 +232,7 @@ export function endpointConformanceChecks(make: MakeAdapter): ConformanceCheck[]
232
232
  const page = await adapter.events(null, 100);
233
233
  const event = page.events.find((candidate) => candidate.entityId === 't1');
234
234
  assert.ok(event, 'event for the committed row');
235
- assert.equal(event.model, 'task');
235
+ assert.equal(event.model, 'item');
236
236
  assert.equal(event.type, 'CREATE');
237
237
  assert.equal(event.correlationId, 'corr_event');
238
238
  assert.equal(event.transactionId, 'op_event');
@@ -244,10 +244,10 @@ export function endpointConformanceChecks(make: MakeAdapter): ConformanceCheck[]
244
244
  run: async () => {
245
245
  const adapter = await make();
246
246
  await adapter.commit(change('corr_page_a', [
247
- { type: 'CREATE', model: 'task', id: 't1', input: {} },
247
+ { type: 'CREATE', model: 'item', id: 't1', input: {} },
248
248
  ]));
249
249
  await adapter.commit(change('corr_page_b', [
250
- { type: 'CREATE', model: 'task', id: 't2', input: {} },
250
+ { type: 'CREATE', model: 'item', id: 't2', input: {} },
251
251
  ]));
252
252
 
253
253
  const first = await adapter.events(null, 1);
@@ -79,6 +79,40 @@ export const sourceCommitEchoOperationSchema = z.strictObject({
79
79
  transactionId: z.string().min(1).max(ABLO_SOURCE_CLIENT_TX_ID_MAX_LENGTH),
80
80
  });
81
81
 
82
+ /**
83
+ * Pre-write correlation evidence. A database-generated CREATE has no row id
84
+ * until its INSERT returns, so the adapter receives the same operation shape
85
+ * with that one value absent and finalizes it inside the writing transaction.
86
+ */
87
+ const sourceCommitEchoIntentOperationSchema = sourceCommitEchoOperationSchema.extend({
88
+ id: z.string().min(1).nullish(),
89
+ });
90
+
91
+ export const sourceCommitEchoIntentSchema = z
92
+ .strictObject({
93
+ version: z.literal(1),
94
+ correlationId: correlationIdSchema,
95
+ operations: z
96
+ .array(sourceCommitEchoIntentOperationSchema)
97
+ .min(1)
98
+ .max(ABLO_SOURCE_ECHO_MAX_OPERATIONS)
99
+ .readonly(),
100
+ })
101
+ .superRefine(({ operations }, ctx) => {
102
+ const seen = new Set<string>();
103
+ for (const [index, operation] of operations.entries()) {
104
+ if (seen.has(operation.transactionId)) {
105
+ ctx.addIssue({
106
+ code: 'custom',
107
+ path: ['operations', index, 'transactionId'],
108
+ message: 'WAL echo operation transactionIds must be unique',
109
+ });
110
+ }
111
+ seen.add(operation.transactionId);
112
+ }
113
+ });
114
+ export type SourceCommitEchoIntent = z.infer<typeof sourceCommitEchoIntentSchema>;
115
+
82
116
  export const sourceCommitEchoMarkerSchema = z
83
117
  .strictObject({
84
118
  version: z.literal(1),
@@ -129,6 +129,7 @@ export {
129
129
  sourceCommitEchoSchema,
130
130
  sourceCommitEchoOperationSchema,
131
131
  sourceCommitEchoMarkerSchema,
132
+ sourceCommitEchoIntentSchema,
132
133
  changeSetSchema,
133
134
  outboxEventSchema,
134
135
  eventsPageSchema,
@@ -141,6 +142,7 @@ export {
141
142
  type Migration,
142
143
  type AdapterCapabilities,
143
144
  type SourceCommitEchoMarkerWire,
145
+ type SourceCommitEchoIntent,
144
146
  } from './contract.js';
145
147
  export { prismaDataSource, type PrismaLike, type PrismaDataSourceOptions } from './adapters/prisma.js';
146
148
  export {
@@ -105,6 +105,7 @@ export interface SourceOperation {
105
105
  readonly model: string;
106
106
  readonly id?: string | null;
107
107
  readonly input?: Record<string, unknown> | null;
108
+ readonly where?: Record<string, unknown> | null;
108
109
  readonly transactionId?: string | null;
109
110
  readonly readAt?: number | null;
110
111
  readonly onStale?: OnStaleMode | null;
@@ -17,9 +17,9 @@
17
17
  * over a socket, and it joins the seam when its core carrier type exists —
18
18
  * see docs/plans/transaction-core-language.md, Tier 3.
19
19
  *
20
- * This is the contract, not a runtime: nothing in this package implements it
21
- * yet. The first explicit implementation (a headless client, a test harness)
22
- * gains a compile-time conformance pin against this interface.
20
+ * This is the contract, not a runtime. The headless HTTP client implements it
21
+ * as a proxy to the hosted authority; other compositions gain the same
22
+ * compile-time conformance pin against this interface.
23
23
  */
24
24
 
25
25
  import type { CommitMessage } from './wire/frames.js';
@@ -166,10 +166,10 @@ export interface HttpModelClient<T, C = T> {
166
166
  * duplicated.
167
167
  *
168
168
  * ```ts
169
- * await ablo.tasks.track({ id: 'task_42' });
169
+ * await ablo.items.track({ id: 'item_42' });
170
170
  * // …minutes of other work later, on the next write…
171
171
  * const res = await ablo.commits.create({ operations: [ … ] });
172
- * res.notifications; // populated if task_42 moved in the meantime
172
+ * res.notifications; // populated if item_42 moved in the meantime
173
173
  * ```
174
174
  *
175
175
  * The returned `notifications` are only the tracks that had ALREADY fired at
@@ -209,6 +209,10 @@ export type AbloHttpClient<S extends SchemaRecord> = {
209
209
  readonly logs: HttpLogsResource;
210
210
  /** Server-confirmed authority of the credential, populated by `ready()`. */
211
211
  readonly identity: EffectiveAuthority | null;
212
+ /**
213
+ * Waits for requests already using this client and commits already scheduled
214
+ * on its commit lane. Stop accepting application work before calling it.
215
+ */
212
216
  dispose(): Promise<void>;
213
217
  /** Resolves the bearer credential this client authenticates with, or `null` if none is set. */
214
218
  getAuthToken(): Promise<string | null>;
@@ -203,6 +203,7 @@ const DEFAULT_CLAIM_TTL_MS = 60_000;
203
203
  export interface HttpTransport {
204
204
  ready(): Promise<void>;
205
205
  waitForFlush(): Promise<void>;
206
+ /** Drains scheduled commits and active requests. */
206
207
  dispose(): Promise<void>;
207
208
  purge(): Promise<void>;
208
209
  readonly commits: CommitResource;
@@ -361,6 +362,26 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
361
362
  let readyPromise: Promise<void> | null = null;
362
363
  let effectiveAuthority: EffectiveAuthority | null = null;
363
364
  let httpCommitLane: Promise<void> = Promise.resolve();
365
+ let activeRequests = 0;
366
+ let requestIdleWaiters: Array<() => void> = [];
367
+ let disposePromise: Promise<void> | null = null;
368
+
369
+ function requestStarted(): void {
370
+ activeRequests += 1;
371
+ }
372
+
373
+ function requestFinished(): void {
374
+ activeRequests -= 1;
375
+ if (activeRequests !== 0) return;
376
+ const waiters = requestIdleWaiters;
377
+ requestIdleWaiters = [];
378
+ for (const resolve of waiters) resolve();
379
+ }
380
+
381
+ function waitForRequestIdle(): Promise<void> {
382
+ if (activeRequests === 0) return Promise.resolve();
383
+ return new Promise((resolve) => requestIdleWaiters.push(resolve));
384
+ }
364
385
 
365
386
  function runInHttpCommitLane<T>(work: () => Promise<T>): Promise<T> {
366
387
  const result = httpCommitLane.then(work);
@@ -475,7 +496,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
475
496
  * the commit paths do with their receipt schema. Everywhere else, go through
476
497
  * {@link requestJson}, which will not let a response past unvalidated.
477
498
  */
478
- async function requestRaw(
499
+ async function performRequest(
479
500
  path: string,
480
501
  init: RequestInit & {
481
502
  readonly idempotencyKey?: string | null;
@@ -557,6 +578,22 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
557
578
  return body;
558
579
  }
559
580
 
581
+ async function requestRaw(
582
+ path: string,
583
+ init: RequestInit & {
584
+ readonly idempotencyKey?: string | null;
585
+ readonly sealedProtocolVersion?: number;
586
+ },
587
+ skipReady = false
588
+ ): Promise<unknown> {
589
+ requestStarted();
590
+ try {
591
+ return await performRequest(path, init, skipReady);
592
+ } finally {
593
+ requestFinished();
594
+ }
595
+ }
596
+
560
597
  /**
561
598
  * Issues one request and validates its body against the route's schema.
562
599
  *
@@ -729,6 +766,11 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
729
766
  initial: CommitResponse
730
767
  ): Promise<CommitResponse> {
731
768
  let current = initial;
769
+ const operationResults = initial.operationResults;
770
+ const withOperationResults = (receipt: CommitResponse): CommitResponse =>
771
+ operationResults?.length
772
+ ? { ...receipt, operationResults }
773
+ : receipt;
732
774
  const correlationId = initial.correlationId;
733
775
  const deadlineAt = requestTimeoutMs > 0 ? Date.now() + requestTimeoutMs : null;
734
776
 
@@ -780,7 +822,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
780
822
  if (confirmationDeadline) clearTimeout(confirmationDeadline);
781
823
  }
782
824
 
783
- if (current.status === 'confirmed') return current;
825
+ if (current.status === 'confirmed') return withOperationResults(current);
784
826
  const delayMs =
785
827
  deadlineAt === null
786
828
  ? HTTP_CONFIRMATION_POLL_INTERVAL_MS
@@ -791,7 +833,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
791
833
  });
792
834
  }
793
835
  }
794
- return current;
836
+ return withOperationResults(current);
795
837
  }
796
838
 
797
839
  async function replayHttpCommitOutbox(): Promise<Map<string, ReplayedHttpCommit>> {
@@ -1111,6 +1153,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1111
1153
  model: op.model,
1112
1154
  id: op.id ?? null,
1113
1155
  data: op.data ?? null,
1156
+ where: op.where ?? null,
1114
1157
  transactionId: op.transactionId ?? null,
1115
1158
  claimId:
1116
1159
  op.claimId ?? claimIdFor(claim?.target, claim?.id, op.model, op.id ?? null),
@@ -1392,6 +1435,9 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1392
1435
  ? { notifications: body.notifications }
1393
1436
  : {}),
1394
1437
  ...(body.missingIds && body.missingIds.length > 0 ? { missingIds: body.missingIds } : {}),
1438
+ ...(body.operationResults && body.operationResults.length > 0
1439
+ ? { operationResults: body.operationResults }
1440
+ : {}),
1395
1441
  };
1396
1442
  },
1397
1443
  get({ id }) {
@@ -2053,7 +2099,16 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
2053
2099
  const replayed = await replayHttpCommitOutbox();
2054
2100
  await confirmReplayedHttpCommits(replayed);
2055
2101
  }),
2056
- async dispose() {},
2102
+ dispose() {
2103
+ if (!disposePromise) {
2104
+ const scheduledCommits = httpCommitLane;
2105
+ disposePromise = (async () => {
2106
+ await scheduledCommits;
2107
+ await waitForRequestIdle();
2108
+ })();
2109
+ }
2110
+ return disposePromise;
2111
+ },
2057
2112
  async purge() {},
2058
2113
  commits,
2059
2114
  claims,
@@ -222,6 +222,9 @@ const handleMutationResult: WsFrameHandler = (session, message) => {
222
222
  ...(receipt.missingIds && receipt.missingIds.length > 0
223
223
  ? { missingIds: receipt.missingIds }
224
224
  : {}),
225
+ ...(receipt.operationResults && receipt.operationResults.length > 0
226
+ ? { operationResults: receipt.operationResults }
227
+ : {}),
225
228
  }),
226
229
  );
227
230
  } else {
@@ -51,7 +51,7 @@ export interface DefaultSyncShape {
51
51
  // `models` is a `SchemaRecord` (not `Record<string, unknown>`) so that
52
52
  // `ResolveSchema['models']` still satisfies the `R extends SchemaRecord`
53
53
  // bound on the SDK hooks when no `Register` is present — e.g. a shared
54
- // package (`@ablo/documents`, `@ablo/teams`) typechecked standalone, with no
54
+ // package (`feature packages`) typechecked standalone, with no
55
55
  // app registration in scope. Without this the fallback wouldn't type-check
56
56
  // against `useAblo<R>()`/`AbloProvider<R>` and every such package would need
57
57
  // its own ambient registration.
@@ -629,12 +629,12 @@ export interface Claim<T = Record<string, unknown>, M = ResolveClaimMeta> {
629
629
  revoke?: () => void;
630
630
  /**
631
631
  * Extend the lease past the liveness window — the "still working" signal
632
- * for long-running tasks. Resolving means the lease is still yours, now
632
+ * for long-running items. Resolving means the lease is still yours, now
633
633
  * good until the returned `expiresAt`; a lease that lapsed (and may have
634
634
  * been granted to the next in line) rejects with an
635
635
  * {@link ../errors.js AbloClaimedError} carrying code `claim_lost`, so the
636
636
  * failed beat doubles as the loss notification. Each beat's extension is
637
- * clamped server-side — hold a long task by beating on a cadence (pass
637
+ * clamped server-side — hold a long item by beating on a cadence (pass
638
638
  * `heartbeat` when claiming to have the SDK do this for you), not by
639
639
  * asking once for a huge window. Present only on a claim you hold.
640
640
  */
@@ -101,7 +101,7 @@ export const conflictAxisWireSchema = z.object({
101
101
 
102
102
  /** One model in the deployed schema, as the schema read reports it. */
103
103
  export const schemaModelResponseSchema = z.object({
104
- /** The key local code addresses (`ablo.documents`). */
104
+ /** The key local code addresses (`ablo.records`). */
105
105
  key: z.string(),
106
106
  /** The wire typename the engine routes and gates on. */
107
107
  typename: z.string(),
@@ -122,7 +122,7 @@ export const schemaModelResponseSchema = z.object({
122
122
  *
123
123
  * The schema artifact has always carried these and this response dropped
124
124
  * them, so a caller with no local schema declaration — which is every caller
125
- * that is not TypeScript — had no way to learn that `task.title` is a
125
+ * that is not TypeScript — had no way to learn that `item.title` is a
126
126
  * required string. It found out from a 400 at the end of a round trip.
127
127
  *
128
128
  * For an agent that is not merely slow, it is a reasoning detour: a typo