slotlock 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/ddl.js +3 -2
- package/event-feed.js +20 -8
- package/package.json +1 -1
- package/slotlock.manifest.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and semantic
|
|
4
4
|
versioning.
|
|
5
5
|
|
|
6
|
+
## 0.5.1 - 2026-10-28
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- Upgrading from 0.4, a feed cursor 0.4 gave no longer reads past changes 0.4 pruned: 0.4 pruned
|
|
11
|
+
by age, not in feed order, so the position 0.5.0 starts each tenant at did not cover every one it
|
|
12
|
+
took. A cursor 0.4 gave expires after 30 days, as it did in 0.4; one this version gives expires
|
|
13
|
+
only once a change after it is pruned.
|
|
14
|
+
- A reader starting the feed over reads from the pruned position on, so a change another server
|
|
15
|
+
left before it (pruning out of order) cannot end a page that is expired at once.
|
|
16
|
+
- The trigger that records how far the feed was pruned is replaced in place, never dropped first,
|
|
17
|
+
and the upgrade locks the feed while it reads where each tenant starts.
|
|
18
|
+
|
|
6
19
|
## 0.5.0 - 2026-10-28
|
|
7
20
|
|
|
8
21
|
### Documentation
|
package/ddl.js
CHANGED
|
@@ -1094,6 +1094,8 @@ CREATE INDEX IF NOT EXISTS slotlock_outbox_tenant_time_idx
|
|
|
1094
1094
|
DO $slotlock_outbox_pruned$
|
|
1095
1095
|
BEGIN
|
|
1096
1096
|
IF pg_catalog.to_regclass('slotlock.outbox_pruned') IS NULL THEN
|
|
1097
|
+
-- No change is deleted between reading what is kept and the mark it starts at.
|
|
1098
|
+
LOCK TABLE slotlock.outbox IN SHARE ROW EXCLUSIVE MODE;
|
|
1097
1099
|
CREATE TABLE IF NOT EXISTS slotlock.outbox_pruned (
|
|
1098
1100
|
tenant_ref text PRIMARY KEY,
|
|
1099
1101
|
txid xid8 NOT NULL,
|
|
@@ -1128,8 +1130,7 @@ BEGIN
|
|
|
1128
1130
|
RETURN NULL;
|
|
1129
1131
|
END
|
|
1130
1132
|
$slotlock_outbox_mark_pruned$;
|
|
1131
|
-
|
|
1132
|
-
CREATE TRIGGER slotlock_outbox_mark_pruned
|
|
1133
|
+
CREATE OR REPLACE TRIGGER slotlock_outbox_mark_pruned
|
|
1133
1134
|
AFTER DELETE ON slotlock.outbox REFERENCING OLD TABLE AS pruned
|
|
1134
1135
|
FOR EACH STATEMENT EXECUTE FUNCTION slotlock.outbox_mark_pruned();
|
|
1135
1136
|
|
package/event-feed.js
CHANGED
|
@@ -36,13 +36,18 @@ export const SLOTLOCK_MAX_WEBHOOK_ENDPOINTS = 10;
|
|
|
36
36
|
/** After this long failing, an endpoint is disabled and the feed says so (`webhook.disabled`). */
|
|
37
37
|
export const SLOTLOCK_WEBHOOK_FAILING_LIMIT_MS = 24 * 60 * 60 * 1_000;
|
|
38
38
|
function encodePosition(position) {
|
|
39
|
-
return Buffer.from(`
|
|
39
|
+
return Buffer.from(`2:${position.txid}:${position.id}:${position.at}`).toString('base64url');
|
|
40
40
|
}
|
|
41
41
|
function decodePosition(value) {
|
|
42
42
|
if (value.length > 200 || !/^[A-Za-z0-9_-]+$/.test(value))
|
|
43
43
|
return null;
|
|
44
|
-
const match = /^
|
|
45
|
-
|
|
44
|
+
const match = /^([12]):(\d{1,20}):(\d{1,19}):(\d{1,15})$/.exec(Buffer.from(value, 'base64url').toString());
|
|
45
|
+
if (!match)
|
|
46
|
+
return null;
|
|
47
|
+
const position = { txid: match[2], id: match[3], at: Number(match[4]) };
|
|
48
|
+
if (match[1] === '1')
|
|
49
|
+
position.dated = true;
|
|
50
|
+
return position;
|
|
46
51
|
}
|
|
47
52
|
function toChange(row) {
|
|
48
53
|
return {
|
|
@@ -131,6 +136,9 @@ export function slotlockEventFeedMethods(sql, retentionDays = SLOTLOCK_CHANGE_RE
|
|
|
131
136
|
AND txid < pg_snapshot_xmin(pg_current_snapshot())
|
|
132
137
|
AND (${after?.txid ?? null}::xid8 IS NULL
|
|
133
138
|
OR (txid, id) > (${after?.txid ?? null}::xid8, ${after?.id ?? null}::bigint))
|
|
139
|
+
-- Starting over: from the mark on. A change kept before it (one another server
|
|
140
|
+
-- left when it pruned past it) would end a page that is expired at once.
|
|
141
|
+
AND (mark.txid IS NULL OR (txid, id) > (mark.txid, mark.id))
|
|
134
142
|
ORDER BY txid, id
|
|
135
143
|
LIMIT ${limit}
|
|
136
144
|
) page ON true
|
|
@@ -139,11 +147,15 @@ export function slotlockEventFeedMethods(sql, retentionDays = SLOTLOCK_CHANGE_RE
|
|
|
139
147
|
if (!head)
|
|
140
148
|
throw new Error('slotlock: change page returned no row');
|
|
141
149
|
// Whichever server pruned it, and however long it keeps changes: reading on could skip one.
|
|
150
|
+
// A 0.4 cursor also by its date, as 0.4 did: 0.4 pruned by age, not in feed order, so the
|
|
151
|
+
// mark its changes started at does not cover every one it took.
|
|
142
152
|
if (after &&
|
|
143
|
-
head.pruned_txid !== null &&
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
153
|
+
((head.pruned_txid !== null &&
|
|
154
|
+
(BigInt(head.pruned_txid) > BigInt(after.txid) ||
|
|
155
|
+
(BigInt(head.pruned_txid) === BigInt(after.txid) &&
|
|
156
|
+
BigInt(head.pruned_id ?? 0) > BigInt(after.id)))) ||
|
|
157
|
+
(after.dated === true &&
|
|
158
|
+
after.at < Date.now() - SLOTLOCK_CHANGE_RETENTION_DAYS * 86_400_000))) {
|
|
147
159
|
return { ok: false, code: 'cursor_expired' };
|
|
148
160
|
}
|
|
149
161
|
const changes = rows.flatMap((row) => (row.id === null ? [] : [toChange(row)]));
|
|
@@ -154,7 +166,7 @@ export function slotlockEventFeedMethods(sql, retentionDays = SLOTLOCK_CHANGE_RE
|
|
|
154
166
|
return {
|
|
155
167
|
ok: true,
|
|
156
168
|
changes,
|
|
157
|
-
next: last?.position ?? encodePosition({
|
|
169
|
+
next: last?.position ?? encodePosition({ txid: here.txid, id: here.id, at: Date.now() }),
|
|
158
170
|
};
|
|
159
171
|
},
|
|
160
172
|
async listRecentChanges(params) {
|
package/package.json
CHANGED
package/slotlock.manifest.json
CHANGED