@byok-sdk/cloud-dataplane 0.5.0 → 0.6.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/README.md +74 -5
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1041 -289
- package/dist/index.js.map +1 -1
- package/dist/migrate.d.ts +42 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +444 -247
- package/dist/runtime.js.map +1 -1
- package/dist/sql/0009_task_cancellation.sql +6 -0
- package/dist/sql/0010_tenant_readiness.sql +21 -0
- package/dist/sql/0011_tenant_erasure.sql +46 -0
- package/dist/stores/core/mailbox.d.ts +13 -0
- package/dist/stores/devices.d.ts +3 -2
- package/dist/stores/index.d.ts +1 -0
- package/dist/stores/r2-blobs.d.ts +1 -1
- package/dist/stores/task-attempts.d.ts +14 -0
- package/dist/stores/task-cancellations.d.ts +9 -0
- package/dist/tenant-erasure.d.ts +77 -0
- package/package.json +4 -4
package/dist/runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import pg from 'pg';
|
|
2
2
|
import { DEDUP_RING_CAPACITY, NONCE_TTL_MS, validateActivityAppend, projectTimelineEvents, ByokCloudError, validateApprovalTimelineAppend, ApprovalObservationSchema, AllowAllRateLimiter, TruthCommitError, TruthCommitResponseSchema, truthRecordMetadata, TRUTH_REQUEST_ID_MAX_LENGTH, parseTimelineEvents, activityCursor, parseApprovalObservations, approvalTimelineCursor } from '@byok-sdk/cloud';
|
|
3
|
-
import { ByokCoreError, assertCanonicalTimestamp,
|
|
3
|
+
import { contentHash, ByokCoreError, assertCanonicalTimestamp, isContentHash, tenantObjectKey, objectKeyPrefix, CoreConflictError, isLegalBoardTransition, checkSkillPackManifest, checkSkillPackEntry, SKILL_PACK_ENTRY_PATH, SKILL_PACK_MANIFEST_SCHEMA_ID } from '@byok-sdk/core';
|
|
4
4
|
import { AwsClient } from 'aws4fetch';
|
|
5
5
|
import { XMLParser } from 'fast-xml-parser';
|
|
6
6
|
|
|
@@ -591,7 +591,7 @@ var R2ObjectMaintenanceStore = class {
|
|
|
591
591
|
`A ListObjectsV2 page limit of ${String(limit)} is not a whole number in [1, 1000].`
|
|
592
592
|
);
|
|
593
593
|
}
|
|
594
|
-
const prefix =
|
|
594
|
+
const prefix = tenantObjectKey(tenant, LIST_PREFIX_HASH, this.#keyPrefix).slice(0, -LIST_HASH_HEX_LENGTH);
|
|
595
595
|
const url = new URL(`${this.#origin}/${this.#bucket}`);
|
|
596
596
|
url.searchParams.set("list-type", "2");
|
|
597
597
|
url.searchParams.set("prefix", prefix);
|
|
@@ -775,6 +775,8 @@ function parseListObjectsV2(xml, prefix, attempts) {
|
|
|
775
775
|
return { objects, nextContinuationToken };
|
|
776
776
|
}
|
|
777
777
|
var HASH_KEY_SUFFIX = /^[0-9a-f]{64}$/;
|
|
778
|
+
var LIST_HASH_HEX_LENGTH = 64;
|
|
779
|
+
var LIST_PREFIX_HASH = contentHash(`sha256:${"0".repeat(LIST_HASH_HEX_LENGTH)}`);
|
|
778
780
|
function asRecord(value) {
|
|
779
781
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
780
782
|
}
|
|
@@ -786,8 +788,6 @@ function requiredText(record, field, attempts) {
|
|
|
786
788
|
attempts
|
|
787
789
|
);
|
|
788
790
|
}
|
|
789
|
-
|
|
790
|
-
// src/stores/devices.ts
|
|
791
791
|
function toRecord(row) {
|
|
792
792
|
return {
|
|
793
793
|
tenantId: row.tenant_id,
|
|
@@ -803,8 +803,10 @@ function toRecord(row) {
|
|
|
803
803
|
var SELECT_COLUMNS = "tenant_id, device_id, product_id, device_name, device_public_key, proof_key_id, proof_key_epoch, revoked";
|
|
804
804
|
var PostgresDeviceDirectory = class {
|
|
805
805
|
#pool;
|
|
806
|
-
|
|
806
|
+
#clock;
|
|
807
|
+
constructor(pool, clock) {
|
|
807
808
|
this.#pool = pool;
|
|
809
|
+
this.#clock = clock;
|
|
808
810
|
}
|
|
809
811
|
async register(tenant, input) {
|
|
810
812
|
const result = await this.#pool.query(
|
|
@@ -854,6 +856,73 @@ var PostgresDeviceDirectory = class {
|
|
|
854
856
|
);
|
|
855
857
|
return result.rows.map(toRecord);
|
|
856
858
|
}
|
|
859
|
+
async readiness(tenant, _presence) {
|
|
860
|
+
const result = await this.#pool.query(
|
|
861
|
+
`SELECT
|
|
862
|
+
d.device_id,
|
|
863
|
+
d.product_id,
|
|
864
|
+
d.device_name,
|
|
865
|
+
d.revoked,
|
|
866
|
+
CASE WHEN NOT d.revoked THEN p.level END AS presence_level,
|
|
867
|
+
CASE WHEN NOT d.revoked THEN p.detail END AS presence_detail,
|
|
868
|
+
CASE WHEN NOT d.revoked THEN p.configured_toolsets END AS presence_configured_toolsets,
|
|
869
|
+
CASE WHEN NOT d.revoked THEN p.client_version END AS presence_client_version,
|
|
870
|
+
CASE WHEN NOT d.revoked THEN p.protocol_versions END AS presence_protocol_versions,
|
|
871
|
+
CASE WHEN NOT d.revoked THEN p.runtimes END AS presence_runtimes,
|
|
872
|
+
CASE WHEN NOT d.revoked THEN p.observed_at END AS presence_observed_at,
|
|
873
|
+
CASE WHEN NOT d.revoked THEN p.expires_at END AS presence_expires_at,
|
|
874
|
+
(COUNT(*) FILTER (WHERE NOT d.revoked) OVER ())::int AS active_paired_device_count,
|
|
875
|
+
(COUNT(*) FILTER (WHERE d.revoked) OVER ())::int AS revoked_device_count,
|
|
876
|
+
(COUNT(*) FILTER (WHERE NOT d.revoked AND p.device_id IS NOT NULL) OVER ())::int AS observed_presence_count,
|
|
877
|
+
(COUNT(*) FILTER (WHERE NOT d.revoked AND p.level = 'online') OVER ())::int AS observed_online_count,
|
|
878
|
+
(COUNT(*) FILTER (WHERE NOT d.revoked AND p.level = 'thinking') OVER ())::int AS observed_thinking_count,
|
|
879
|
+
(COUNT(*) FILTER (WHERE NOT d.revoked AND p.level = 'working') OVER ())::int AS observed_working_count,
|
|
880
|
+
(COUNT(*) FILTER (WHERE NOT d.revoked AND p.level = 'error') OVER ())::int AS observed_error_count,
|
|
881
|
+
(COUNT(*) FILTER (WHERE NOT d.revoked AND p.level = 'offline') OVER ())::int AS observed_offline_count
|
|
882
|
+
FROM device d
|
|
883
|
+
LEFT JOIN device_presence p
|
|
884
|
+
ON p.tenant_id = d.tenant_id
|
|
885
|
+
AND p.device_id = d.device_id
|
|
886
|
+
AND p.expires_at > $2
|
|
887
|
+
WHERE d.tenant_id = $1
|
|
888
|
+
ORDER BY d.device_id`,
|
|
889
|
+
[tenant, this.#clock?.now().toISOString() ?? (/* @__PURE__ */ new Date()).toISOString()]
|
|
890
|
+
);
|
|
891
|
+
const row = result.rows[0];
|
|
892
|
+
const count = (value) => Number(value);
|
|
893
|
+
const devices = result.rows.map((device) => ({
|
|
894
|
+
deviceId: device.device_id,
|
|
895
|
+
productId: device.product_id,
|
|
896
|
+
deviceName: device.device_name,
|
|
897
|
+
revoked: device.revoked,
|
|
898
|
+
...device.presence_level === null ? {} : {
|
|
899
|
+
presence: {
|
|
900
|
+
level: device.presence_level,
|
|
901
|
+
...device.presence_detail === null ? {} : { detail: device.presence_detail },
|
|
902
|
+
...device.presence_configured_toolsets === null ? {} : { configuredToolsets: Object.freeze([...device.presence_configured_toolsets]) },
|
|
903
|
+
...device.presence_client_version === null ? {} : { clientVersion: device.presence_client_version },
|
|
904
|
+
...device.presence_protocol_versions === null ? {} : { protocolVersions: Object.freeze([...device.presence_protocol_versions]) },
|
|
905
|
+
...device.presence_runtimes === null ? {} : { runtimes: Object.freeze(device.presence_runtimes.map((runtime) => Object.freeze({ ...runtime }))) },
|
|
906
|
+
observedAt: device.presence_observed_at,
|
|
907
|
+
expiresAt: device.presence_expires_at
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}));
|
|
911
|
+
return {
|
|
912
|
+
tenantId: tenant,
|
|
913
|
+
activePairedDeviceCount: count(row?.active_paired_device_count ?? 0),
|
|
914
|
+
revokedDeviceCount: count(row?.revoked_device_count ?? 0),
|
|
915
|
+
observedPresenceCount: count(row?.observed_presence_count ?? 0),
|
|
916
|
+
observedPresenceByLevel: {
|
|
917
|
+
online: count(row?.observed_online_count ?? 0),
|
|
918
|
+
thinking: count(row?.observed_thinking_count ?? 0),
|
|
919
|
+
working: count(row?.observed_working_count ?? 0),
|
|
920
|
+
error: count(row?.observed_error_count ?? 0),
|
|
921
|
+
offline: count(row?.observed_offline_count ?? 0)
|
|
922
|
+
},
|
|
923
|
+
devices
|
|
924
|
+
};
|
|
925
|
+
}
|
|
857
926
|
async resolveByDeviceId(deviceId) {
|
|
858
927
|
const result = await this.#pool.query(
|
|
859
928
|
`SELECT ${SELECT_COLUMNS} FROM device WHERE device_id = $1`,
|
|
@@ -1077,8 +1146,8 @@ var PostgresProofRequestReceiptStore = class {
|
|
|
1077
1146
|
};
|
|
1078
1147
|
|
|
1079
1148
|
// src/stores/task-attempts.ts
|
|
1080
|
-
var
|
|
1081
|
-
function
|
|
1149
|
+
var TASK_SELECT_COLUMNS = "tenant_id, task_id, device_id, owner_device_id, status, cancel_requested_at, cancel_reason, cancel_message_id, updated_at";
|
|
1150
|
+
function taskRowToAttempt(row) {
|
|
1082
1151
|
return {
|
|
1083
1152
|
tenantId: row.tenant_id,
|
|
1084
1153
|
taskId: row.task_id,
|
|
@@ -1089,6 +1158,12 @@ function toAttempt(row) {
|
|
|
1089
1158
|
// former.
|
|
1090
1159
|
...row.owner_device_id === null ? {} : { ownerDeviceId: row.owner_device_id },
|
|
1091
1160
|
status: row.status,
|
|
1161
|
+
...row.cancel_requested_at === null ? {} : {
|
|
1162
|
+
cancellation: {
|
|
1163
|
+
requestedAt: row.cancel_requested_at.toISOString(),
|
|
1164
|
+
...row.cancel_reason === null ? {} : { reason: row.cancel_reason }
|
|
1165
|
+
}
|
|
1166
|
+
},
|
|
1092
1167
|
updatedAt: row.updated_at.toISOString()
|
|
1093
1168
|
};
|
|
1094
1169
|
}
|
|
@@ -1104,33 +1179,42 @@ var PostgresTaskAttemptStore = class {
|
|
|
1104
1179
|
`INSERT INTO task (tenant_id, task_id, device_id, owner_device_id, status, updated_at)
|
|
1105
1180
|
VALUES ($1, $2, $3, NULL, 'offered', $4)
|
|
1106
1181
|
ON CONFLICT (tenant_id, task_id) DO NOTHING
|
|
1107
|
-
RETURNING ${
|
|
1182
|
+
RETURNING ${TASK_SELECT_COLUMNS}`,
|
|
1108
1183
|
[tenant, input.taskId, input.deviceId, this.#now()]
|
|
1109
1184
|
);
|
|
1110
1185
|
const created = inserted.rows[0];
|
|
1111
|
-
if (created !== void 0) return
|
|
1186
|
+
if (created !== void 0) return taskRowToAttempt(created);
|
|
1112
1187
|
const existing = await this.get(tenant, input.taskId);
|
|
1113
1188
|
if (existing === void 0) throw new Error(`task ${input.taskId} vanished during open`);
|
|
1114
1189
|
return existing;
|
|
1115
1190
|
}
|
|
1116
1191
|
async get(tenant, taskId) {
|
|
1117
1192
|
const result = await this.#pool.query(
|
|
1118
|
-
`SELECT ${
|
|
1193
|
+
`SELECT ${TASK_SELECT_COLUMNS} FROM task WHERE tenant_id = $1 AND task_id = $2`,
|
|
1119
1194
|
[tenant, taskId]
|
|
1120
1195
|
);
|
|
1121
1196
|
const row = result.rows[0];
|
|
1122
|
-
return row === void 0 ? void 0 :
|
|
1197
|
+
return row === void 0 ? void 0 : taskRowToAttempt(row);
|
|
1198
|
+
}
|
|
1199
|
+
async getMany(tenant, taskIds) {
|
|
1200
|
+
if (taskIds.length === 0) return [];
|
|
1201
|
+
const result = await this.#pool.query(
|
|
1202
|
+
`SELECT ${TASK_SELECT_COLUMNS} FROM task WHERE tenant_id = $1 AND task_id = ANY($2::text[])`,
|
|
1203
|
+
[tenant, [...new Set(taskIds)]]
|
|
1204
|
+
);
|
|
1205
|
+
return result.rows.map(taskRowToAttempt);
|
|
1123
1206
|
}
|
|
1124
1207
|
async claim(tenant, input) {
|
|
1125
1208
|
const claimed = await this.#pool.query(
|
|
1126
1209
|
`UPDATE task
|
|
1127
1210
|
SET owner_device_id = $3, status = 'claimed', updated_at = $4
|
|
1128
1211
|
WHERE tenant_id = $1 AND task_id = $2 AND owner_device_id IS NULL
|
|
1129
|
-
|
|
1212
|
+
AND cancel_requested_at IS NULL AND status = 'offered'
|
|
1213
|
+
RETURNING ${TASK_SELECT_COLUMNS}`,
|
|
1130
1214
|
[tenant, input.taskId, input.deviceId, this.#now()]
|
|
1131
1215
|
);
|
|
1132
1216
|
const won = claimed.rows[0];
|
|
1133
|
-
if (won !== void 0) return
|
|
1217
|
+
if (won !== void 0) return taskRowToAttempt(won);
|
|
1134
1218
|
return this.get(tenant, input.taskId);
|
|
1135
1219
|
}
|
|
1136
1220
|
async recordStatus(tenant, input) {
|
|
@@ -1138,16 +1222,336 @@ var PostgresTaskAttemptStore = class {
|
|
|
1138
1222
|
`UPDATE task
|
|
1139
1223
|
SET status = $3, updated_at = $4
|
|
1140
1224
|
WHERE tenant_id = $1 AND task_id = $2
|
|
1141
|
-
|
|
1225
|
+
AND (
|
|
1226
|
+
(cancel_requested_at IS NULL AND status NOT IN ('complete', 'failed', 'cancelled'))
|
|
1227
|
+
OR (cancel_requested_at IS NOT NULL AND $3 = 'cancelled' AND status <> 'cancelled')
|
|
1228
|
+
)
|
|
1229
|
+
RETURNING ${TASK_SELECT_COLUMNS}`,
|
|
1142
1230
|
[tenant, input.taskId, input.status, this.#now()]
|
|
1143
1231
|
);
|
|
1144
1232
|
const row = result.rows[0];
|
|
1145
|
-
return row === void 0 ?
|
|
1233
|
+
return row === void 0 ? this.get(tenant, input.taskId) : taskRowToAttempt(row);
|
|
1146
1234
|
}
|
|
1147
1235
|
#now() {
|
|
1148
1236
|
return this.#clock.now().toISOString();
|
|
1149
1237
|
}
|
|
1150
1238
|
};
|
|
1239
|
+
|
|
1240
|
+
// src/stores/core/mailbox-sequence.ts
|
|
1241
|
+
async function allocateMailboxSequence(client, tenant, deviceId, now) {
|
|
1242
|
+
const allocation = await client.query(
|
|
1243
|
+
`INSERT INTO device_stream (tenant_id, device_id, next_seq, acked_seq, acked_at)
|
|
1244
|
+
VALUES ($1, $2, 2, 0, $3)
|
|
1245
|
+
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
1246
|
+
SET next_seq = device_stream.next_seq + 1
|
|
1247
|
+
RETURNING next_seq - 1 AS seq`,
|
|
1248
|
+
[tenant, deviceId, now]
|
|
1249
|
+
);
|
|
1250
|
+
return Number(allocation.rows[0].seq);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// src/stores/core/mailbox.ts
|
|
1254
|
+
var DEFAULT_READ_LIMIT = 50;
|
|
1255
|
+
var OUTBOX_COLUMNS = "tenant_id, device_id, seq, message_id, body, body_hash, byte_size, state, appended_at";
|
|
1256
|
+
function toMailboxMessage(row) {
|
|
1257
|
+
return {
|
|
1258
|
+
tenantId: row.tenant_id,
|
|
1259
|
+
deviceId: row.device_id,
|
|
1260
|
+
// `seq` is bigint in the column and `number` on the port, because it is the
|
|
1261
|
+
// envelope `seq` on the wire. The column is wide so the counter cannot wrap
|
|
1262
|
+
// into a redelivery bug; the narrowing happens once, here.
|
|
1263
|
+
seq: Number(row.seq),
|
|
1264
|
+
messageId: row.message_id,
|
|
1265
|
+
body: row.body,
|
|
1266
|
+
bodyHash: row.body_hash,
|
|
1267
|
+
byteSize: row.byte_size,
|
|
1268
|
+
state: row.state,
|
|
1269
|
+
appendedAt: row.appended_at
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
var PostgresMailboxStore = class {
|
|
1273
|
+
#pool;
|
|
1274
|
+
#clock;
|
|
1275
|
+
constructor(pool, clock) {
|
|
1276
|
+
this.#pool = pool;
|
|
1277
|
+
this.#clock = clock;
|
|
1278
|
+
}
|
|
1279
|
+
async append(tenant, input) {
|
|
1280
|
+
this.#requireDeviceId(input.deviceId);
|
|
1281
|
+
const client = await this.#pool.connect();
|
|
1282
|
+
try {
|
|
1283
|
+
await client.query("BEGIN");
|
|
1284
|
+
const existing = await client.query(
|
|
1285
|
+
`SELECT ${OUTBOX_COLUMNS} FROM outbox
|
|
1286
|
+
WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
|
|
1287
|
+
[tenant, input.deviceId, input.messageId]
|
|
1288
|
+
);
|
|
1289
|
+
const replayed = existing.rows[0];
|
|
1290
|
+
if (replayed !== void 0) {
|
|
1291
|
+
await client.query("COMMIT");
|
|
1292
|
+
return toMailboxMessage(replayed);
|
|
1293
|
+
}
|
|
1294
|
+
const seq = await allocateMailboxSequence(client, tenant, input.deviceId, this.#now());
|
|
1295
|
+
const serializedExisting = await client.query(
|
|
1296
|
+
`SELECT ${OUTBOX_COLUMNS} FROM outbox
|
|
1297
|
+
WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
|
|
1298
|
+
[tenant, input.deviceId, input.messageId]
|
|
1299
|
+
);
|
|
1300
|
+
const winnerAfterLock = serializedExisting.rows[0];
|
|
1301
|
+
if (winnerAfterLock !== void 0) {
|
|
1302
|
+
await client.query("ROLLBACK");
|
|
1303
|
+
return toMailboxMessage(winnerAfterLock);
|
|
1304
|
+
}
|
|
1305
|
+
const materialized = await input.materialize(seq);
|
|
1306
|
+
const now = this.#now();
|
|
1307
|
+
const inserted = await client.query(
|
|
1308
|
+
`INSERT INTO outbox (${OUTBOX_COLUMNS})
|
|
1309
|
+
VALUES ($1, $2, $3::bigint, $4, $5, $6, $7::bigint, 'pending', $8)
|
|
1310
|
+
ON CONFLICT (tenant_id, device_id, message_id) DO NOTHING
|
|
1311
|
+
RETURNING ${OUTBOX_COLUMNS}`,
|
|
1312
|
+
[
|
|
1313
|
+
tenant,
|
|
1314
|
+
input.deviceId,
|
|
1315
|
+
seq,
|
|
1316
|
+
input.messageId,
|
|
1317
|
+
materialized.body,
|
|
1318
|
+
materialized.bodyHash,
|
|
1319
|
+
materialized.byteSize,
|
|
1320
|
+
now
|
|
1321
|
+
]
|
|
1322
|
+
);
|
|
1323
|
+
const row = inserted.rows[0];
|
|
1324
|
+
if (row !== void 0) {
|
|
1325
|
+
await client.query("COMMIT");
|
|
1326
|
+
return toMailboxMessage(row);
|
|
1327
|
+
}
|
|
1328
|
+
const winner = await client.query(
|
|
1329
|
+
`SELECT ${OUTBOX_COLUMNS} FROM outbox
|
|
1330
|
+
WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
|
|
1331
|
+
[tenant, input.deviceId, input.messageId]
|
|
1332
|
+
);
|
|
1333
|
+
await client.query("ROLLBACK");
|
|
1334
|
+
const won = winner.rows[0];
|
|
1335
|
+
if (won === void 0) {
|
|
1336
|
+
throw new ByokCoreError(
|
|
1337
|
+
"mailbox_message_not_found",
|
|
1338
|
+
`Message ${input.messageId} vanished during an idempotent append.`
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
return toMailboxMessage(won);
|
|
1342
|
+
} catch (cause) {
|
|
1343
|
+
await client.query("ROLLBACK").catch(() => {
|
|
1344
|
+
});
|
|
1345
|
+
throw cause;
|
|
1346
|
+
} finally {
|
|
1347
|
+
client.release();
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
async readAfter(tenant, query) {
|
|
1351
|
+
const limit = query.limit ?? DEFAULT_READ_LIMIT;
|
|
1352
|
+
const result = await this.#pool.query(
|
|
1353
|
+
`SELECT ${OUTBOX_COLUMNS} FROM outbox
|
|
1354
|
+
WHERE tenant_id = $1 AND device_id = $2 AND state = 'pending' AND seq > $3::bigint
|
|
1355
|
+
ORDER BY seq
|
|
1356
|
+
LIMIT $4`,
|
|
1357
|
+
[tenant, query.deviceId, query.afterSeq, limit + 1]
|
|
1358
|
+
);
|
|
1359
|
+
const page = result.rows.slice(0, limit).map(toMailboxMessage);
|
|
1360
|
+
return {
|
|
1361
|
+
messages: page,
|
|
1362
|
+
// Nothing above was mutated, so an identical call replays the same page.
|
|
1363
|
+
// The returned position is a READ cursor and moves no ack.
|
|
1364
|
+
nextSeq: page.at(-1)?.seq ?? query.afterSeq,
|
|
1365
|
+
hasMore: result.rows.length > page.length
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
async advanceCursor(tenant, input) {
|
|
1369
|
+
this.#requireDeviceId(input.deviceId);
|
|
1370
|
+
const now = this.#now();
|
|
1371
|
+
const moved = await this.#pool.query(
|
|
1372
|
+
`WITH moved AS (
|
|
1373
|
+
INSERT INTO device_stream (tenant_id, device_id, next_seq, acked_seq, acked_at)
|
|
1374
|
+
VALUES ($1, $2, 1, $3::bigint, $4)
|
|
1375
|
+
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
1376
|
+
SET acked_seq = EXCLUDED.acked_seq, acked_at = EXCLUDED.acked_at
|
|
1377
|
+
WHERE device_stream.acked_seq <= EXCLUDED.acked_seq
|
|
1378
|
+
RETURNING acked_seq, acked_at
|
|
1379
|
+
), marked AS (
|
|
1380
|
+
UPDATE outbox
|
|
1381
|
+
SET state = 'acked'
|
|
1382
|
+
WHERE tenant_id = $1 AND device_id = $2 AND state = 'pending'
|
|
1383
|
+
AND seq <= (SELECT acked_seq FROM moved)
|
|
1384
|
+
RETURNING 1
|
|
1385
|
+
)
|
|
1386
|
+
SELECT acked_seq, acked_at FROM moved`,
|
|
1387
|
+
[tenant, input.deviceId, input.ackedSeq, now]
|
|
1388
|
+
);
|
|
1389
|
+
const row = moved.rows[0];
|
|
1390
|
+
if (row !== void 0) {
|
|
1391
|
+
return {
|
|
1392
|
+
tenantId: tenant,
|
|
1393
|
+
deviceId: input.deviceId,
|
|
1394
|
+
ackedSeq: Number(row.acked_seq),
|
|
1395
|
+
updatedAt: row.acked_at ?? now
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
const current = await this.readCursor(tenant, input.deviceId);
|
|
1399
|
+
throw new CoreConflictError(
|
|
1400
|
+
"mailbox_cursor_regression",
|
|
1401
|
+
`Cursor for device ${input.deviceId} is at ${current.ackedSeq}; refusing to move it back to ${input.ackedSeq}.`,
|
|
1402
|
+
current,
|
|
1403
|
+
this.#now()
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
async readCursor(tenant, deviceId) {
|
|
1407
|
+
const result = await this.#pool.query(
|
|
1408
|
+
`SELECT acked_seq, acked_at FROM device_stream
|
|
1409
|
+
WHERE tenant_id = $1 AND device_id = $2`,
|
|
1410
|
+
[tenant, deviceId]
|
|
1411
|
+
);
|
|
1412
|
+
const row = result.rows[0];
|
|
1413
|
+
return {
|
|
1414
|
+
tenantId: tenant,
|
|
1415
|
+
deviceId,
|
|
1416
|
+
ackedSeq: row === void 0 ? 0 : Number(row.acked_seq),
|
|
1417
|
+
updatedAt: row?.acked_at ?? this.#now()
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
async collectRetired(tenant, input) {
|
|
1421
|
+
assertCanonicalTimestamp(input.ackedBefore, "ackedBefore");
|
|
1422
|
+
assertCanonicalTimestamp(input.expireUnackedBefore, "expireUnackedBefore");
|
|
1423
|
+
const swept = await this.#pool.query(
|
|
1424
|
+
`WITH deleted AS (
|
|
1425
|
+
DELETE FROM outbox
|
|
1426
|
+
WHERE tenant_id = $1
|
|
1427
|
+
AND ($2::text IS NULL OR device_id = $2::text)
|
|
1428
|
+
AND state = 'acked'
|
|
1429
|
+
AND appended_at < $3
|
|
1430
|
+
RETURNING byte_size
|
|
1431
|
+
), expired AS (
|
|
1432
|
+
UPDATE outbox
|
|
1433
|
+
SET state = 'expired'
|
|
1434
|
+
WHERE tenant_id = $1
|
|
1435
|
+
AND ($2::text IS NULL OR device_id = $2::text)
|
|
1436
|
+
AND state = 'pending'
|
|
1437
|
+
AND appended_at < $4
|
|
1438
|
+
RETURNING 1
|
|
1439
|
+
)
|
|
1440
|
+
SELECT (SELECT count(*) FROM deleted) AS deleted_count,
|
|
1441
|
+
(SELECT count(*) FROM expired) AS expired_count,
|
|
1442
|
+
(SELECT COALESCE(SUM(byte_size), 0) FROM deleted)::bigint AS released_bytes`,
|
|
1443
|
+
[tenant, input.deviceId ?? null, input.ackedBefore, input.expireUnackedBefore]
|
|
1444
|
+
);
|
|
1445
|
+
const row = swept.rows[0];
|
|
1446
|
+
return {
|
|
1447
|
+
deletedCount: Number(row.deleted_count),
|
|
1448
|
+
expiredCount: Number(row.expired_count),
|
|
1449
|
+
releasedBytes: row.released_bytes
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* The in-memory reference refuses an empty device id rather than opening a
|
|
1454
|
+
* mailbox nothing can address. Kept here so the two compositions answer the
|
|
1455
|
+
* same way; the table itself would happily store the row.
|
|
1456
|
+
*/
|
|
1457
|
+
#requireDeviceId(deviceId) {
|
|
1458
|
+
if (deviceId.length === 0) {
|
|
1459
|
+
throw new ByokCoreError("mailbox_message_not_found", "Device id must not be empty.");
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
#now() {
|
|
1463
|
+
return this.#clock.now().toISOString();
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1467
|
+
// src/stores/task-cancellations.ts
|
|
1468
|
+
var PostgresTaskCancellationStore = class {
|
|
1469
|
+
#pool;
|
|
1470
|
+
#clock;
|
|
1471
|
+
constructor(pool, clock) {
|
|
1472
|
+
this.#pool = pool;
|
|
1473
|
+
this.#clock = clock;
|
|
1474
|
+
}
|
|
1475
|
+
async request(tenant, input) {
|
|
1476
|
+
const client = await this.#pool.connect();
|
|
1477
|
+
try {
|
|
1478
|
+
await client.query("BEGIN");
|
|
1479
|
+
const selected = await client.query(
|
|
1480
|
+
`SELECT ${TASK_SELECT_COLUMNS} FROM task
|
|
1481
|
+
WHERE tenant_id = $1 AND task_id = $2
|
|
1482
|
+
FOR UPDATE`,
|
|
1483
|
+
[tenant, input.taskId]
|
|
1484
|
+
);
|
|
1485
|
+
const current = selected.rows[0];
|
|
1486
|
+
if (current === void 0) {
|
|
1487
|
+
await client.query("ROLLBACK");
|
|
1488
|
+
return void 0;
|
|
1489
|
+
}
|
|
1490
|
+
if (current.cancel_requested_at === null && (current.status === "complete" || current.status === "failed" || current.status === "cancelled")) {
|
|
1491
|
+
await client.query("COMMIT");
|
|
1492
|
+
return { attempt: taskRowToAttempt(current) };
|
|
1493
|
+
}
|
|
1494
|
+
if (current.cancel_message_id !== null) {
|
|
1495
|
+
const replayed = await client.query(
|
|
1496
|
+
`SELECT ${OUTBOX_COLUMNS} FROM outbox
|
|
1497
|
+
WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
|
|
1498
|
+
[tenant, current.device_id, current.cancel_message_id]
|
|
1499
|
+
);
|
|
1500
|
+
const message = replayed.rows[0];
|
|
1501
|
+
if (message === void 0) {
|
|
1502
|
+
if (current.status === "cancelled") {
|
|
1503
|
+
await client.query("COMMIT");
|
|
1504
|
+
return { attempt: taskRowToAttempt(current) };
|
|
1505
|
+
}
|
|
1506
|
+
throw new Error(`Cancellation delivery ${current.cancel_message_id} is missing for task ${input.taskId}`);
|
|
1507
|
+
}
|
|
1508
|
+
await client.query("COMMIT");
|
|
1509
|
+
return { attempt: taskRowToAttempt(current), message: toMailboxMessage(message) };
|
|
1510
|
+
}
|
|
1511
|
+
const now = this.#clock.now().toISOString();
|
|
1512
|
+
const messageId = input.proposedMessageId;
|
|
1513
|
+
const seq = await allocateMailboxSequence(client, tenant, current.device_id, now);
|
|
1514
|
+
const materialized = await input.materialize(seq, messageId);
|
|
1515
|
+
const insertedMessage = await client.query(
|
|
1516
|
+
`INSERT INTO outbox (${OUTBOX_COLUMNS})
|
|
1517
|
+
VALUES ($1, $2, $3::bigint, $4, $5, $6, $7::bigint, 'pending', $8)
|
|
1518
|
+
RETURNING ${OUTBOX_COLUMNS}`,
|
|
1519
|
+
[
|
|
1520
|
+
tenant,
|
|
1521
|
+
current.device_id,
|
|
1522
|
+
seq,
|
|
1523
|
+
messageId,
|
|
1524
|
+
materialized.body,
|
|
1525
|
+
materialized.bodyHash,
|
|
1526
|
+
materialized.byteSize,
|
|
1527
|
+
now
|
|
1528
|
+
]
|
|
1529
|
+
);
|
|
1530
|
+
const updated = await client.query(
|
|
1531
|
+
`UPDATE task
|
|
1532
|
+
SET status = CASE WHEN owner_device_id IS NULL THEN 'cancelled' ELSE 'cancel_requested' END,
|
|
1533
|
+
cancel_requested_at = $3,
|
|
1534
|
+
cancel_reason = $4,
|
|
1535
|
+
cancel_message_id = $5,
|
|
1536
|
+
updated_at = $3
|
|
1537
|
+
WHERE tenant_id = $1 AND task_id = $2
|
|
1538
|
+
RETURNING ${TASK_SELECT_COLUMNS}`,
|
|
1539
|
+
[tenant, input.taskId, now, input.reason ?? null, messageId]
|
|
1540
|
+
);
|
|
1541
|
+
await client.query("COMMIT");
|
|
1542
|
+
return {
|
|
1543
|
+
attempt: taskRowToAttempt(updated.rows[0]),
|
|
1544
|
+
message: toMailboxMessage(insertedMessage.rows[0])
|
|
1545
|
+
};
|
|
1546
|
+
} catch (cause) {
|
|
1547
|
+
await client.query("ROLLBACK").catch(() => {
|
|
1548
|
+
});
|
|
1549
|
+
throw cause;
|
|
1550
|
+
} finally {
|
|
1551
|
+
client.release();
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1151
1555
|
function toTail(row) {
|
|
1152
1556
|
const entries = parseTimelineEvents(row.entries);
|
|
1153
1557
|
const cursor = activityCursor(entries);
|
|
@@ -1437,11 +1841,12 @@ function createPostgresCloudStores(options) {
|
|
|
1437
1841
|
return {
|
|
1438
1842
|
activity: new PostgresActivityStore(pool, clock),
|
|
1439
1843
|
approvals: new PostgresApprovalTimelineStore(pool, clock),
|
|
1440
|
-
devices: new PostgresDeviceDirectory(pool),
|
|
1844
|
+
devices: new PostgresDeviceDirectory(pool, clock),
|
|
1441
1845
|
pairingCodes: new PostgresPairingCodeStore(pool, clock),
|
|
1442
1846
|
nonces: new PostgresNonceStore(pool, clock, crypto),
|
|
1443
1847
|
dedup: new PostgresInboundDedupStore(pool),
|
|
1444
1848
|
tasks: new PostgresTaskAttemptStore(pool, clock),
|
|
1849
|
+
cancellations: new PostgresTaskCancellationStore(pool, clock),
|
|
1445
1850
|
receipts: new PostgresRequestReceiptStore(pool, clock),
|
|
1446
1851
|
proofReceipts: new PostgresProofRequestReceiptStore(pool, clock),
|
|
1447
1852
|
// A second `PostgresObjectStore` instance, not a shared one: it is a
|
|
@@ -1455,7 +1860,7 @@ function createPostgresCloudStores(options) {
|
|
|
1455
1860
|
rateLimiter: new AllowAllRateLimiter()
|
|
1456
1861
|
};
|
|
1457
1862
|
}
|
|
1458
|
-
var PRESENCE_COLUMNS = "tenant_id, device_id, level, detail, configured_toolsets, observed_at, expires_at";
|
|
1863
|
+
var PRESENCE_COLUMNS = "tenant_id, device_id, level, detail, configured_toolsets, client_version, protocol_versions, runtimes, observed_at, expires_at";
|
|
1459
1864
|
function toHint(row) {
|
|
1460
1865
|
return {
|
|
1461
1866
|
tenantId: row.tenant_id,
|
|
@@ -1463,6 +1868,19 @@ function toHint(row) {
|
|
|
1463
1868
|
level: row.level,
|
|
1464
1869
|
...row.detail === null ? {} : { detail: row.detail },
|
|
1465
1870
|
...row.configured_toolsets === null ? {} : { configuredToolsets: Object.freeze([...row.configured_toolsets]) },
|
|
1871
|
+
...row.client_version === null ? {} : { clientVersion: row.client_version },
|
|
1872
|
+
...row.protocol_versions === null ? {} : { protocolVersions: Object.freeze([...row.protocol_versions]) },
|
|
1873
|
+
...row.runtimes === null ? {} : {
|
|
1874
|
+
runtimes: Object.freeze(
|
|
1875
|
+
row.runtimes.map(
|
|
1876
|
+
(runtime) => Object.freeze({
|
|
1877
|
+
id: runtime.id,
|
|
1878
|
+
...runtime.version === void 0 ? {} : { version: runtime.version },
|
|
1879
|
+
...runtime.authPresent === void 0 ? {} : { authPresent: runtime.authPresent }
|
|
1880
|
+
})
|
|
1881
|
+
)
|
|
1882
|
+
)
|
|
1883
|
+
},
|
|
1466
1884
|
observedAt: row.observed_at,
|
|
1467
1885
|
expiresAt: row.expires_at
|
|
1468
1886
|
};
|
|
@@ -1498,15 +1916,18 @@ var PostgresPresenceStore = class {
|
|
|
1498
1916
|
const allowedBefore = new Date(now.getTime() - input.minimumIntervalMs).toISOString();
|
|
1499
1917
|
const result = await this.#pool.query(
|
|
1500
1918
|
`INSERT INTO device_presence (${PRESENCE_COLUMNS})
|
|
1501
|
-
VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7)
|
|
1919
|
+
VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7::jsonb, $8::jsonb, $9, $10)
|
|
1502
1920
|
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
1503
1921
|
SET level = EXCLUDED.level,
|
|
1504
1922
|
detail = EXCLUDED.detail,
|
|
1505
1923
|
configured_toolsets = EXCLUDED.configured_toolsets,
|
|
1924
|
+
client_version = EXCLUDED.client_version,
|
|
1925
|
+
protocol_versions = EXCLUDED.protocol_versions,
|
|
1926
|
+
runtimes = EXCLUDED.runtimes,
|
|
1506
1927
|
observed_at = EXCLUDED.observed_at,
|
|
1507
1928
|
expires_at = EXCLUDED.expires_at
|
|
1508
1929
|
WHERE device_presence.expires_at <= EXCLUDED.observed_at
|
|
1509
|
-
OR device_presence.observed_at <= $
|
|
1930
|
+
OR device_presence.observed_at <= $11
|
|
1510
1931
|
RETURNING ${PRESENCE_COLUMNS}`,
|
|
1511
1932
|
[
|
|
1512
1933
|
tenant,
|
|
@@ -1514,6 +1935,9 @@ var PostgresPresenceStore = class {
|
|
|
1514
1935
|
input.level,
|
|
1515
1936
|
input.detail ?? null,
|
|
1516
1937
|
input.configuredToolsets === void 0 ? null : JSON.stringify(input.configuredToolsets),
|
|
1938
|
+
input.clientVersion ?? null,
|
|
1939
|
+
input.protocolVersions === void 0 ? null : JSON.stringify(input.protocolVersions),
|
|
1940
|
+
input.runtimes === void 0 ? null : JSON.stringify(input.runtimes),
|
|
1517
1941
|
observedAt,
|
|
1518
1942
|
new Date(now.getTime() + input.ttlMs).toISOString(),
|
|
1519
1943
|
allowedBefore
|
|
@@ -1781,233 +2205,6 @@ var PostgresBoardStore = class {
|
|
|
1781
2205
|
return this.#clock.now().toISOString();
|
|
1782
2206
|
}
|
|
1783
2207
|
};
|
|
1784
|
-
|
|
1785
|
-
// src/stores/core/mailbox-sequence.ts
|
|
1786
|
-
async function allocateMailboxSequence(client, tenant, deviceId, now) {
|
|
1787
|
-
const allocation = await client.query(
|
|
1788
|
-
`INSERT INTO device_stream (tenant_id, device_id, next_seq, acked_seq, acked_at)
|
|
1789
|
-
VALUES ($1, $2, 2, 0, $3)
|
|
1790
|
-
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
1791
|
-
SET next_seq = device_stream.next_seq + 1
|
|
1792
|
-
RETURNING next_seq - 1 AS seq`,
|
|
1793
|
-
[tenant, deviceId, now]
|
|
1794
|
-
);
|
|
1795
|
-
return Number(allocation.rows[0].seq);
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
// src/stores/core/mailbox.ts
|
|
1799
|
-
var DEFAULT_READ_LIMIT = 50;
|
|
1800
|
-
var OUTBOX_COLUMNS = "tenant_id, device_id, seq, message_id, body, body_hash, byte_size, state, appended_at";
|
|
1801
|
-
function toMessage(row) {
|
|
1802
|
-
return {
|
|
1803
|
-
tenantId: row.tenant_id,
|
|
1804
|
-
deviceId: row.device_id,
|
|
1805
|
-
// `seq` is bigint in the column and `number` on the port, because it is the
|
|
1806
|
-
// envelope `seq` on the wire. The column is wide so the counter cannot wrap
|
|
1807
|
-
// into a redelivery bug; the narrowing happens once, here.
|
|
1808
|
-
seq: Number(row.seq),
|
|
1809
|
-
messageId: row.message_id,
|
|
1810
|
-
body: row.body,
|
|
1811
|
-
bodyHash: row.body_hash,
|
|
1812
|
-
byteSize: row.byte_size,
|
|
1813
|
-
state: row.state,
|
|
1814
|
-
appendedAt: row.appended_at
|
|
1815
|
-
};
|
|
1816
|
-
}
|
|
1817
|
-
var PostgresMailboxStore = class {
|
|
1818
|
-
#pool;
|
|
1819
|
-
#clock;
|
|
1820
|
-
constructor(pool, clock) {
|
|
1821
|
-
this.#pool = pool;
|
|
1822
|
-
this.#clock = clock;
|
|
1823
|
-
}
|
|
1824
|
-
async append(tenant, input) {
|
|
1825
|
-
this.#requireDeviceId(input.deviceId);
|
|
1826
|
-
const client = await this.#pool.connect();
|
|
1827
|
-
try {
|
|
1828
|
-
await client.query("BEGIN");
|
|
1829
|
-
const existing = await client.query(
|
|
1830
|
-
`SELECT ${OUTBOX_COLUMNS} FROM outbox
|
|
1831
|
-
WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
|
|
1832
|
-
[tenant, input.deviceId, input.messageId]
|
|
1833
|
-
);
|
|
1834
|
-
const replayed = existing.rows[0];
|
|
1835
|
-
if (replayed !== void 0) {
|
|
1836
|
-
await client.query("COMMIT");
|
|
1837
|
-
return toMessage(replayed);
|
|
1838
|
-
}
|
|
1839
|
-
const seq = await allocateMailboxSequence(client, tenant, input.deviceId, this.#now());
|
|
1840
|
-
const serializedExisting = await client.query(
|
|
1841
|
-
`SELECT ${OUTBOX_COLUMNS} FROM outbox
|
|
1842
|
-
WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
|
|
1843
|
-
[tenant, input.deviceId, input.messageId]
|
|
1844
|
-
);
|
|
1845
|
-
const winnerAfterLock = serializedExisting.rows[0];
|
|
1846
|
-
if (winnerAfterLock !== void 0) {
|
|
1847
|
-
await client.query("ROLLBACK");
|
|
1848
|
-
return toMessage(winnerAfterLock);
|
|
1849
|
-
}
|
|
1850
|
-
const materialized = await input.materialize(seq);
|
|
1851
|
-
const now = this.#now();
|
|
1852
|
-
const inserted = await client.query(
|
|
1853
|
-
`INSERT INTO outbox (${OUTBOX_COLUMNS})
|
|
1854
|
-
VALUES ($1, $2, $3::bigint, $4, $5, $6, $7::bigint, 'pending', $8)
|
|
1855
|
-
ON CONFLICT (tenant_id, device_id, message_id) DO NOTHING
|
|
1856
|
-
RETURNING ${OUTBOX_COLUMNS}`,
|
|
1857
|
-
[
|
|
1858
|
-
tenant,
|
|
1859
|
-
input.deviceId,
|
|
1860
|
-
seq,
|
|
1861
|
-
input.messageId,
|
|
1862
|
-
materialized.body,
|
|
1863
|
-
materialized.bodyHash,
|
|
1864
|
-
materialized.byteSize,
|
|
1865
|
-
now
|
|
1866
|
-
]
|
|
1867
|
-
);
|
|
1868
|
-
const row = inserted.rows[0];
|
|
1869
|
-
if (row !== void 0) {
|
|
1870
|
-
await client.query("COMMIT");
|
|
1871
|
-
return toMessage(row);
|
|
1872
|
-
}
|
|
1873
|
-
const winner = await client.query(
|
|
1874
|
-
`SELECT ${OUTBOX_COLUMNS} FROM outbox
|
|
1875
|
-
WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
|
|
1876
|
-
[tenant, input.deviceId, input.messageId]
|
|
1877
|
-
);
|
|
1878
|
-
await client.query("ROLLBACK");
|
|
1879
|
-
const won = winner.rows[0];
|
|
1880
|
-
if (won === void 0) {
|
|
1881
|
-
throw new ByokCoreError(
|
|
1882
|
-
"mailbox_message_not_found",
|
|
1883
|
-
`Message ${input.messageId} vanished during an idempotent append.`
|
|
1884
|
-
);
|
|
1885
|
-
}
|
|
1886
|
-
return toMessage(won);
|
|
1887
|
-
} catch (cause) {
|
|
1888
|
-
await client.query("ROLLBACK").catch(() => {
|
|
1889
|
-
});
|
|
1890
|
-
throw cause;
|
|
1891
|
-
} finally {
|
|
1892
|
-
client.release();
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
async readAfter(tenant, query) {
|
|
1896
|
-
const limit = query.limit ?? DEFAULT_READ_LIMIT;
|
|
1897
|
-
const result = await this.#pool.query(
|
|
1898
|
-
`SELECT ${OUTBOX_COLUMNS} FROM outbox
|
|
1899
|
-
WHERE tenant_id = $1 AND device_id = $2 AND state = 'pending' AND seq > $3::bigint
|
|
1900
|
-
ORDER BY seq
|
|
1901
|
-
LIMIT $4`,
|
|
1902
|
-
[tenant, query.deviceId, query.afterSeq, limit + 1]
|
|
1903
|
-
);
|
|
1904
|
-
const page = result.rows.slice(0, limit).map(toMessage);
|
|
1905
|
-
return {
|
|
1906
|
-
messages: page,
|
|
1907
|
-
// Nothing above was mutated, so an identical call replays the same page.
|
|
1908
|
-
// The returned position is a READ cursor and moves no ack.
|
|
1909
|
-
nextSeq: page.at(-1)?.seq ?? query.afterSeq,
|
|
1910
|
-
hasMore: result.rows.length > page.length
|
|
1911
|
-
};
|
|
1912
|
-
}
|
|
1913
|
-
async advanceCursor(tenant, input) {
|
|
1914
|
-
this.#requireDeviceId(input.deviceId);
|
|
1915
|
-
const now = this.#now();
|
|
1916
|
-
const moved = await this.#pool.query(
|
|
1917
|
-
`WITH moved AS (
|
|
1918
|
-
INSERT INTO device_stream (tenant_id, device_id, next_seq, acked_seq, acked_at)
|
|
1919
|
-
VALUES ($1, $2, 1, $3::bigint, $4)
|
|
1920
|
-
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
1921
|
-
SET acked_seq = EXCLUDED.acked_seq, acked_at = EXCLUDED.acked_at
|
|
1922
|
-
WHERE device_stream.acked_seq <= EXCLUDED.acked_seq
|
|
1923
|
-
RETURNING acked_seq, acked_at
|
|
1924
|
-
), marked AS (
|
|
1925
|
-
UPDATE outbox
|
|
1926
|
-
SET state = 'acked'
|
|
1927
|
-
WHERE tenant_id = $1 AND device_id = $2 AND state = 'pending'
|
|
1928
|
-
AND seq <= (SELECT acked_seq FROM moved)
|
|
1929
|
-
RETURNING 1
|
|
1930
|
-
)
|
|
1931
|
-
SELECT acked_seq, acked_at FROM moved`,
|
|
1932
|
-
[tenant, input.deviceId, input.ackedSeq, now]
|
|
1933
|
-
);
|
|
1934
|
-
const row = moved.rows[0];
|
|
1935
|
-
if (row !== void 0) {
|
|
1936
|
-
return {
|
|
1937
|
-
tenantId: tenant,
|
|
1938
|
-
deviceId: input.deviceId,
|
|
1939
|
-
ackedSeq: Number(row.acked_seq),
|
|
1940
|
-
updatedAt: row.acked_at ?? now
|
|
1941
|
-
};
|
|
1942
|
-
}
|
|
1943
|
-
const current = await this.readCursor(tenant, input.deviceId);
|
|
1944
|
-
throw new CoreConflictError(
|
|
1945
|
-
"mailbox_cursor_regression",
|
|
1946
|
-
`Cursor for device ${input.deviceId} is at ${current.ackedSeq}; refusing to move it back to ${input.ackedSeq}.`,
|
|
1947
|
-
current,
|
|
1948
|
-
this.#now()
|
|
1949
|
-
);
|
|
1950
|
-
}
|
|
1951
|
-
async readCursor(tenant, deviceId) {
|
|
1952
|
-
const result = await this.#pool.query(
|
|
1953
|
-
`SELECT acked_seq, acked_at FROM device_stream
|
|
1954
|
-
WHERE tenant_id = $1 AND device_id = $2`,
|
|
1955
|
-
[tenant, deviceId]
|
|
1956
|
-
);
|
|
1957
|
-
const row = result.rows[0];
|
|
1958
|
-
return {
|
|
1959
|
-
tenantId: tenant,
|
|
1960
|
-
deviceId,
|
|
1961
|
-
ackedSeq: row === void 0 ? 0 : Number(row.acked_seq),
|
|
1962
|
-
updatedAt: row?.acked_at ?? this.#now()
|
|
1963
|
-
};
|
|
1964
|
-
}
|
|
1965
|
-
async collectRetired(tenant, input) {
|
|
1966
|
-
assertCanonicalTimestamp(input.ackedBefore, "ackedBefore");
|
|
1967
|
-
assertCanonicalTimestamp(input.expireUnackedBefore, "expireUnackedBefore");
|
|
1968
|
-
const swept = await this.#pool.query(
|
|
1969
|
-
`WITH deleted AS (
|
|
1970
|
-
DELETE FROM outbox
|
|
1971
|
-
WHERE tenant_id = $1
|
|
1972
|
-
AND ($2::text IS NULL OR device_id = $2::text)
|
|
1973
|
-
AND state = 'acked'
|
|
1974
|
-
AND appended_at < $3
|
|
1975
|
-
RETURNING byte_size
|
|
1976
|
-
), expired AS (
|
|
1977
|
-
UPDATE outbox
|
|
1978
|
-
SET state = 'expired'
|
|
1979
|
-
WHERE tenant_id = $1
|
|
1980
|
-
AND ($2::text IS NULL OR device_id = $2::text)
|
|
1981
|
-
AND state = 'pending'
|
|
1982
|
-
AND appended_at < $4
|
|
1983
|
-
RETURNING 1
|
|
1984
|
-
)
|
|
1985
|
-
SELECT (SELECT count(*) FROM deleted) AS deleted_count,
|
|
1986
|
-
(SELECT count(*) FROM expired) AS expired_count,
|
|
1987
|
-
(SELECT COALESCE(SUM(byte_size), 0) FROM deleted)::bigint AS released_bytes`,
|
|
1988
|
-
[tenant, input.deviceId ?? null, input.ackedBefore, input.expireUnackedBefore]
|
|
1989
|
-
);
|
|
1990
|
-
const row = swept.rows[0];
|
|
1991
|
-
return {
|
|
1992
|
-
deletedCount: Number(row.deleted_count),
|
|
1993
|
-
expiredCount: Number(row.expired_count),
|
|
1994
|
-
releasedBytes: row.released_bytes
|
|
1995
|
-
};
|
|
1996
|
-
}
|
|
1997
|
-
/**
|
|
1998
|
-
* The in-memory reference refuses an empty device id rather than opening a
|
|
1999
|
-
* mailbox nothing can address. Kept here so the two compositions answer the
|
|
2000
|
-
* same way; the table itself would happily store the row.
|
|
2001
|
-
*/
|
|
2002
|
-
#requireDeviceId(deviceId) {
|
|
2003
|
-
if (deviceId.length === 0) {
|
|
2004
|
-
throw new ByokCoreError("mailbox_message_not_found", "Device id must not be empty.");
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
#now() {
|
|
2008
|
-
return this.#clock.now().toISOString();
|
|
2009
|
-
}
|
|
2010
|
-
};
|
|
2011
2208
|
var WARNING_NUMERATOR = 80n;
|
|
2012
2209
|
var WARNING_DENOMINATOR = 100n;
|
|
2013
2210
|
var ENTITLEMENT_COLUMNS = "tenant_id, version, hard_limit_bytes, max_object_bytes, max_inline_bytes, mailbox_limit_bytes, retention_policy_id, downgrade_grace_until";
|
|
@@ -3353,6 +3550,6 @@ var PostgresTruthCommitter = class {
|
|
|
3353
3550
|
}
|
|
3354
3551
|
};
|
|
3355
3552
|
|
|
3356
|
-
export { DEFAULT_MAX_ATTEMPTS, DEFAULT_PRESIGN_TTL_SECONDS, DEFAULT_RETRY_DELAY_MS, MAX_PRESIGN_TTL_SECONDS, MIN_PRESIGN_TTL_SECONDS, ObjectStoreRequestError, PostgresActivityStore, PostgresApprovalTimelineStore, PostgresBoardStore, PostgresDeviceAssertionReplayAuthority, PostgresDeviceDirectory, PostgresInboundDedupStore, PostgresMailboxStore, PostgresNonceStore, PostgresObjectStore, PostgresPairingCodeStore, PostgresPresenceStore, PostgresProofRequestReceiptStore, PostgresQuotaStore, PostgresRequestReceiptStore, PostgresSkillPackStore, PostgresTaskAttemptStore, PostgresTruthCommitter, PostgresTruthStore, R2BlobStoreError, R2CloudBlobStore, R2ObjectMaintenanceStore, R2_BLOB_ERROR_CODES, createByokPool, createPostgresCloudStores, createPostgresCoreStores };
|
|
3553
|
+
export { DEFAULT_MAX_ATTEMPTS, DEFAULT_PRESIGN_TTL_SECONDS, DEFAULT_RETRY_DELAY_MS, MAX_PRESIGN_TTL_SECONDS, MIN_PRESIGN_TTL_SECONDS, ObjectStoreRequestError, PostgresActivityStore, PostgresApprovalTimelineStore, PostgresBoardStore, PostgresDeviceAssertionReplayAuthority, PostgresDeviceDirectory, PostgresInboundDedupStore, PostgresMailboxStore, PostgresNonceStore, PostgresObjectStore, PostgresPairingCodeStore, PostgresPresenceStore, PostgresProofRequestReceiptStore, PostgresQuotaStore, PostgresRequestReceiptStore, PostgresSkillPackStore, PostgresTaskAttemptStore, PostgresTaskCancellationStore, PostgresTruthCommitter, PostgresTruthStore, R2BlobStoreError, R2CloudBlobStore, R2ObjectMaintenanceStore, R2_BLOB_ERROR_CODES, createByokPool, createPostgresCloudStores, createPostgresCoreStores };
|
|
3357
3554
|
//# sourceMappingURL=runtime.js.map
|
|
3358
3555
|
//# sourceMappingURL=runtime.js.map
|