@byok-sdk/core 0.11.0 → 0.13.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/dist/errors.d.ts CHANGED
@@ -39,6 +39,7 @@ export declare const CORE_ERROR_CODES: {
39
39
  readonly assertion_envelope_invalid: 'assertion_envelope_invalid';
40
40
  readonly mailbox_message_not_found: 'mailbox_message_not_found';
41
41
  readonly mailbox_cursor_regression: 'mailbox_cursor_regression';
42
+ readonly mailbox_cursor_ahead_of_delivery: 'mailbox_cursor_ahead_of_delivery';
42
43
  readonly board_item_not_found: 'board_item_not_found';
43
44
  readonly board_item_exists: 'board_item_exists';
44
45
  readonly board_transition_invalid: 'board_transition_invalid';
@@ -1,4 +1,4 @@
1
- import type { MailboxAdvanceCursorInput, MailboxAppendInput, MailboxCursorState, MailboxMessage, MailboxPage, MailboxReadQuery, MailboxRetentionInput, MailboxRetentionResult, MailboxStore } from '../mailbox';
1
+ import type { MailboxAdvanceCursorInput, MailboxAppendInput, MailboxCursorState, MailboxMessage, MailboxPage, MailboxReadQuery, MailboxRecordDeliveryInput, MailboxRetentionInput, MailboxRetentionResult, MailboxStore } from '../mailbox';
2
2
  import type { Clock } from '../stores';
3
3
  import { type TenantId } from '../tenant';
4
4
  export declare class InMemoryMailboxStore implements MailboxStore {
@@ -7,6 +7,7 @@ export declare class InMemoryMailboxStore implements MailboxStore {
7
7
  append(tenant: TenantId, input: MailboxAppendInput): Promise<MailboxMessage>;
8
8
  readAfter(tenant: TenantId, query: MailboxReadQuery): Promise<MailboxPage>;
9
9
  advanceCursor(tenant: TenantId, input: MailboxAdvanceCursorInput): Promise<MailboxCursorState>;
10
+ recordDelivery(tenant: TenantId, input: MailboxRecordDeliveryInput): Promise<MailboxCursorState>;
10
11
  readCursor(tenant: TenantId, deviceId: string): Promise<MailboxCursorState>;
11
12
  collectRetired(tenant: TenantId, input: MailboxRetentionInput): Promise<MailboxRetentionResult>;
12
13
  }
package/dist/index.d.ts CHANGED
@@ -18,7 +18,7 @@ export { ByokCoreError, CoreConflictError, CORE_ERROR_CODES, isCoreError, isCore
18
18
  export type { CoreErrorCode } from './errors';
19
19
  export { CANONICAL_TIMESTAMP_PATTERN, assertCanonicalTimestamp, isCanonicalTimestamp, } from './time';
20
20
  export { MAILBOX_MESSAGE_STATES } from './mailbox';
21
- export type { MailboxAdvanceCursorInput, MailboxAppendInput, MailboxBody, MailboxCursorState, MailboxMessage, MailboxMessageState, MailboxPage, MailboxReadQuery, MailboxRetentionInput, MailboxRetentionResult, MailboxStore, } from './mailbox';
21
+ export type { MailboxAdvanceCursorInput, MailboxAppendInput, MailboxBody, MailboxCursorState, MailboxMessage, MailboxMessageState, MailboxPage, MailboxReadQuery, MailboxRecordDeliveryInput, MailboxRetentionInput, MailboxRetentionResult, MailboxStore, } from './mailbox';
22
22
  export { BOARD_STATUSES, BOARD_TRANSITIONS, isLegalBoardTransition } from './board';
23
23
  export type { BoardAssignee, BoardClaimInput, BoardItem, BoardItemInput, BoardListQuery, BoardPage, BoardStatus, BoardStatusUpdateInput, BoardStore, BoardUnclaimInput, } from './board';
24
24
  export { TRUTH_RECORD_KINDS } from './truth';
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ var CORE_ERROR_CODES = {
22
22
  // mailbox (§12.7.3)
23
23
  mailbox_message_not_found: "mailbox_message_not_found",
24
24
  mailbox_cursor_regression: "mailbox_cursor_regression",
25
+ mailbox_cursor_ahead_of_delivery: "mailbox_cursor_ahead_of_delivery",
25
26
  // board coordination (§12.3)
26
27
  board_item_not_found: "board_item_not_found",
27
28
  board_item_exists: "board_item_exists",
@@ -536,7 +537,7 @@ var CORE_STORE_NAMES = [
536
537
  // src/ports-contract.ts
537
538
  var CORE_NON_COMPOSITION_PORT_NAMES = [];
538
539
  var CORE_PORT_METHODS = {
539
- mailbox: ["append", "readAfter", "advanceCursor", "readCursor", "collectRetired"],
540
+ mailbox: ["append", "readAfter", "recordDelivery", "advanceCursor", "readCursor", "collectRetired"],
540
541
  board: ["create", "get", "list", "claim", "unclaim", "updateStatus"],
541
542
  truth: ["writeTerminal", "writeSnapshot", "getRecord", "listManifest"],
542
543
  presence: ["publish", "read", "list"],
@@ -1228,7 +1229,7 @@ var InMemoryMailboxStore = class {
1228
1229
  const device = this.#devices.get(tenantKey(tenant, query.deviceId));
1229
1230
  const limit = query.limit ?? DEFAULT_READ_LIMIT;
1230
1231
  if (device === void 0) {
1231
- return { messages: [], nextSeq: query.afterSeq, hasMore: false };
1232
+ return { messages: [], nextSeq: query.afterSeq, hasMore: false, recoverableFrom: 1 };
1232
1233
  }
1233
1234
  const pending = device.messages.filter((message) => message.state === "pending" && message.seq > query.afterSeq).sort((left, right) => left.seq - right.seq);
1234
1235
  const page = pending.slice(0, limit);
@@ -1238,7 +1239,11 @@ var InMemoryMailboxStore = class {
1238
1239
  // Reading is not acknowledging: the returned position is a *read* cursor.
1239
1240
  // Nothing above was mutated, so an identical call replays the same page.
1240
1241
  nextSeq: last?.seq ?? query.afterSeq,
1241
- hasMore: pending.length > page.length
1242
+ hasMore: pending.length > page.length,
1243
+ // Computed over the WHOLE mailbox, not the queried slice: the floor is a
1244
+ // property of the device's history, and reporting it relative to
1245
+ // `afterSeq` would make every cursor look recoverable from itself.
1246
+ recoverableFrom: this.#recoverableFrom(device)
1242
1247
  };
1243
1248
  }
1244
1249
  async advanceCursor(tenant, input) {
@@ -1252,6 +1257,14 @@ var InMemoryMailboxStore = class {
1252
1257
  this.#now()
1253
1258
  );
1254
1259
  }
1260
+ if (input.ackedSeq > device.deliveredSeq) {
1261
+ throw new CoreConflictError(
1262
+ "mailbox_cursor_ahead_of_delivery",
1263
+ `Cursor for device ${input.deviceId} was delivered through ${device.deliveredSeq}; refusing to acknowledge future cursor ${input.ackedSeq}.`,
1264
+ this.#cursorState(tenant, input.deviceId, device),
1265
+ this.#now()
1266
+ );
1267
+ }
1255
1268
  device.ackedSeq = input.ackedSeq;
1256
1269
  device.cursorUpdatedAt = this.#now();
1257
1270
  for (const [index, message] of device.messages.entries()) {
@@ -1268,10 +1281,24 @@ var InMemoryMailboxStore = class {
1268
1281
  );
1269
1282
  return advanced;
1270
1283
  }
1284
+ async recordDelivery(tenant, input) {
1285
+ const device = this.#device(tenant, input.deviceId);
1286
+ const recorded = device.mutationTail.then(() => {
1287
+ if (input.deliveredSeq > device.deliveredSeq) {
1288
+ device.deliveredSeq = input.deliveredSeq;
1289
+ }
1290
+ return this.#cursorState(tenant, input.deviceId, device);
1291
+ });
1292
+ device.mutationTail = recorded.then(
1293
+ () => void 0,
1294
+ () => void 0
1295
+ );
1296
+ return recorded;
1297
+ }
1271
1298
  async readCursor(tenant, deviceId) {
1272
1299
  const device = this.#devices.get(tenantKey(tenant, deviceId));
1273
1300
  if (device === void 0) {
1274
- return { tenantId: tenant, deviceId, ackedSeq: 0, updatedAt: this.#now() };
1301
+ return { tenantId: tenant, deviceId, deliveredSeq: 0, ackedSeq: 0, updatedAt: this.#now() };
1275
1302
  }
1276
1303
  return this.#cursorState(tenant, deviceId, device);
1277
1304
  }
@@ -1312,6 +1339,7 @@ var InMemoryMailboxStore = class {
1312
1339
  if (existing !== void 0) return existing;
1313
1340
  const created = {
1314
1341
  nextSeq: 1,
1342
+ deliveredSeq: 0,
1315
1343
  ackedSeq: 0,
1316
1344
  cursorUpdatedAt: this.#now(),
1317
1345
  mutationTail: Promise.resolve(),
@@ -1321,10 +1349,27 @@ var InMemoryMailboxStore = class {
1321
1349
  this.#devices.set(key, created);
1322
1350
  return created;
1323
1351
  }
1352
+ /**
1353
+ * One past the highest row this device lost, which is exactly the highest
1354
+ * `expired` seq: dead-lettered rows are kept rather than deleted (§12.7.5),
1355
+ * so the loss stays observable and needs no column of its own to remember it.
1356
+ *
1357
+ * `acked` is deliberately not counted. A consumed row is not a lost one, and
1358
+ * counting it would turn a daemon re-polling from an old cursor — which the
1359
+ * at-least-once contract explicitly allows — into a hard failure.
1360
+ */
1361
+ #recoverableFrom(device) {
1362
+ let lost = 0;
1363
+ for (const message of device.messages) {
1364
+ if (message.state === "expired" && message.seq > lost) lost = message.seq;
1365
+ }
1366
+ return lost + 1;
1367
+ }
1324
1368
  #cursorState(tenant, deviceId, device) {
1325
1369
  return {
1326
1370
  tenantId: tenant,
1327
1371
  deviceId,
1372
+ deliveredSeq: device.deliveredSeq,
1328
1373
  ackedSeq: device.ackedSeq,
1329
1374
  updatedAt: device.cursorUpdatedAt
1330
1375
  };