@volter/teams 0.2.26 → 0.2.27
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/package.json +1 -1
- package/server/apps.mjs +5 -7
- package/server/core.mjs +42 -10
- package/server/migrations/024.mjs +14 -0
- package/server/migrations/index.mjs +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.27",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Volter Teams, the neutral team plane: machine identity and daemon core, the local door and server link, admission from machine grants, product doors, and the Teams server's neutral routes (company RFC 0008).",
|
|
6
6
|
"license": "MIT",
|
package/server/apps.mjs
CHANGED
|
@@ -397,12 +397,10 @@ export async function deliverAppWebhooks(api) {
|
|
|
397
397
|
const state = delivered ? 'delivered' : row.attempts >= 8 ? 'failed' : 'pending';
|
|
398
398
|
await api.store.transaction([statement("UPDATE app_webhook_deliveries SET state=?,last_status=?,lease_until=NULL,next_at=? WHERE id=? AND lease_until=? AND state='pending'", state, status, api.clock.nowMs() + Math.min(3600000, 1000 * 2 ** row.attempts), id, lease)]);
|
|
399
399
|
}
|
|
400
|
-
// Week-old settled deliveries go
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (through != null) await api.store.transaction([statement('DELETE FROM app_webhook_deliveries WHERE rowid<=? AND state<>\'pending\'', through)]);
|
|
406
|
-
if (through != null && front.length >= 500 && front.at(-1).id === through) return true;
|
|
400
|
+
// Week-old settled deliveries go through their queue (migration 024: queued when they settled), oldest first, a
|
|
401
|
+
// bounded batch per run; a pending delivery is never in it, so none stands in front of them (t_6c01c966).
|
|
402
|
+
const cut = api.clock.nowMs() - 7 * 86400000;
|
|
403
|
+
await api.store.transaction([statement("DELETE FROM app_webhook_deliveries WHERE id IN (SELECT id FROM delivery_settled WHERE created_at<? ORDER BY created_at LIMIT 500) AND state<>'pending'", cut)]);
|
|
404
|
+
if (await api.store.one('SELECT 1 AS more FROM delivery_settled WHERE created_at<? LIMIT 1', [cut])) return true;
|
|
407
405
|
return due.length >= 10;
|
|
408
406
|
}
|
package/server/core.mjs
CHANGED
|
@@ -73,7 +73,8 @@ export async function maintain(api,{budgetMs=MAINTAIN_BUDGET_MS}={}){const now=a
|
|
|
73
73
|
for(const [table,column,cut] of [['idempotency','expires_at',now],['global_idempotency','expires_at',now],['journal','created_at',now-86400000],['app_install_codes','expires_at',now]]){
|
|
74
74
|
const front=await q(`SELECT rowid AS id,${column} AS at FROM ${table} ORDER BY rowid LIMIT ${EXPIRY_BATCH}`);
|
|
75
75
|
let through=null;for(const row of front){if(row.at>cut)break;through=row.id;}
|
|
76
|
-
|
|
76
|
+
// fenced by its own expiry too: a row written since the read that took a reused rowid is never removed
|
|
77
|
+
if(through!=null){await tx([{sql:`DELETE FROM ${table} WHERE rowid<=? AND ${column}<=?`,params:[through,cut]}]);count(`expired ${table}`,front.findIndex(row=>row.id===through)+1);}
|
|
77
78
|
if(through!=null&&front.length>=EXPIRY_BATCH&&front.at(-1).id===through)more=true;
|
|
78
79
|
}
|
|
79
80
|
// cursors expire through their expiry index
|
|
@@ -135,8 +136,11 @@ export async function maintain(api,{budgetMs=MAINTAIN_BUDGET_MS}={}){const now=a
|
|
|
135
136
|
// session's current version, the newest version from each of its sources, the newest one from each source that a
|
|
136
137
|
// capture's artifact names (the next capture and a correlation read those), and every version a live cursor pins.
|
|
137
138
|
await step('versions',async()=>{
|
|
138
|
-
|
|
139
|
+
// every cursor made since migration 023 pins through its trigger; one made before it is waited out (cursors live
|
|
140
|
+
// 15 minutes), never unpacked
|
|
141
|
+
if(!await walked(api,['version_sources'])||!await cursorsBeforePinsGone(api,now)){more=true;return;}
|
|
139
142
|
const due=await q(`SELECT team_id,session_id,generation FROM version_prune_queue WHERE due_at<=? ORDER BY due_at LIMIT ${VERSION_SESSION_BATCH}`,[now]);
|
|
143
|
+
let messageBudget=MESSAGE_BUDGET;
|
|
140
144
|
for(const {team_id,session_id,generation} of due){
|
|
141
145
|
if(!left()){more=true;break;}
|
|
142
146
|
const session=await one('SELECT revision FROM sessions WHERE team_id=? AND id=?',[team_id,session_id]);
|
|
@@ -162,11 +166,27 @@ export async function maintain(api,{budgetMs=MAINTAIN_BUDGET_MS}={}){const now=a
|
|
|
162
166
|
ORDER BY vs.revision LIMIT ${budget}`,[team_id,session_id,source,newest,session.revision,now,capture]);
|
|
163
167
|
// A message row lives from the version that wrote it until one that changed it (schema 9), so a row a later
|
|
164
168
|
// version still holds moves to the next version that holds it before its own goes; the rest go with the version.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
// A long session's version holds many rows: they are worked a bounded chunk at a time (by ordinal, on the key),
|
|
170
|
+
// and the version goes once none is left; one not finished keeps its session due.
|
|
171
|
+
let removed=0;
|
|
172
|
+
for(const {revision} of free){
|
|
173
|
+
for(;;){
|
|
174
|
+
if(messageBudget<=0||!left()){full=true;break;}
|
|
175
|
+
const edge=await one(`SELECT ordinal FROM messages WHERE team_id=? AND session_id=? AND revision=? ORDER BY ordinal LIMIT 1 OFFSET ${MESSAGE_CHUNK-1}`,[team_id,session_id,revision]);
|
|
176
|
+
const upTo=edge?.ordinal??Number.MAX_SAFE_INTEGER;
|
|
177
|
+
await tx([
|
|
178
|
+
{sql:'UPDATE messages SET revision=(SELECT MIN(k.revision) FROM session_versions k WHERE k.team_id=messages.team_id AND k.session_id=messages.session_id AND k.revision>messages.revision AND k.revision<COALESCE(messages.until_revision,messages.revision+1)) WHERE team_id=? AND session_id=? AND revision=? AND ordinal<=? AND EXISTS (SELECT 1 FROM session_versions k WHERE k.team_id=messages.team_id AND k.session_id=messages.session_id AND k.revision>messages.revision AND k.revision<COALESCE(messages.until_revision,messages.revision+1))',params:[team_id,session_id,revision,upTo]},
|
|
179
|
+
{sql:'DELETE FROM messages WHERE team_id=? AND session_id=? AND revision=? AND ordinal<=?',params:[team_id,session_id,revision,upTo]},
|
|
180
|
+
]);
|
|
181
|
+
messageBudget-=MESSAGE_CHUNK;
|
|
182
|
+
if(!edge)break;
|
|
183
|
+
}
|
|
184
|
+
if(await one('SELECT 1 AS left FROM messages WHERE team_id=? AND session_id=? AND revision=? LIMIT 1',[team_id,session_id,revision]))break;
|
|
185
|
+
await tx(['session_artifacts','session_versions'].map(table=>({sql:`DELETE FROM ${table} WHERE team_id=? AND session_id=? AND revision=?`,params:[team_id,session_id,revision]})));
|
|
186
|
+
removed++;
|
|
187
|
+
}
|
|
188
|
+
count('versions',removed);budget-=free.length;if(budget<=0)full=true;
|
|
189
|
+
if(full)break;
|
|
170
190
|
}
|
|
171
191
|
// more of this session: due now; else due when its first live pin expires (what it pins may then go); else done
|
|
172
192
|
const pin=full?null:await one('SELECT MIN(expires_at) AS at FROM cursor_pins WHERE team_id=? AND session_id=? AND expires_at>?',[team_id,session_id,now]);
|
|
@@ -209,6 +229,17 @@ export async function maintain(api,{budgetMs=MAINTAIN_BUDGET_MS}={}){const now=a
|
|
|
209
229
|
if(Object.keys(counts).length)console.log(`maintain: removed ${Object.entries(counts).map(([name,n])=>`${n} ${name}`).join(', ')}${more?' (backlog)':''}`);
|
|
210
230
|
return result;}
|
|
211
231
|
|
|
232
|
+
/** Whether every cursor made before migration 023 (whose versions it pins are not pin rows) has expired: the latest
|
|
233
|
+
* expiry among the cursors there when this was first asked, kept, then waited out (cursors live 15 minutes). */
|
|
234
|
+
async function cursorsBeforePinsGone(api,now){
|
|
235
|
+
let row=await api.store.one("SELECT after_rowid AS until FROM maintain_backfill WHERE name='cursors_before_pins'");
|
|
236
|
+
if(!row){
|
|
237
|
+
const latest=await api.store.one('SELECT expires_at FROM cursors ORDER BY expires_at DESC LIMIT 1');
|
|
238
|
+
row={until:latest?.expires_at??0};
|
|
239
|
+
await api.store.transaction([{sql:"INSERT OR IGNORE INTO maintain_backfill(name,after_rowid,done) VALUES ('cursors_before_pins',?,1)",params:[row.until]}]);
|
|
240
|
+
}
|
|
241
|
+
return now>row.until;
|
|
242
|
+
}
|
|
212
243
|
/** Whether each named walk of migration 023's backfill is done. */
|
|
213
244
|
async function walked(api,names){
|
|
214
245
|
for(const name of names){const row=await api.store.one('SELECT done FROM maintain_backfill WHERE name=?',[name]);if(!row?.done)return false;}
|
|
@@ -220,9 +251,6 @@ const BACKFILLS=[
|
|
|
220
251
|
['version_sources','session_versions',[
|
|
221
252
|
"INSERT OR IGNORE INTO version_sources SELECT team_id,session_id,COALESCE(json_extract(source_projection_json,'$.source_id'),''),revision FROM session_versions WHERE rowid>? AND rowid<=?",
|
|
222
253
|
'INSERT INTO version_prune_queue(team_id,session_id,due_at,generation) SELECT DISTINCT team_id,session_id,0,1 FROM session_versions WHERE rowid>? AND rowid<=? ON CONFLICT(team_id,session_id) DO UPDATE SET due_at=0,generation=generation+1']],
|
|
223
|
-
['cursor_pins','cursors',[
|
|
224
|
-
"INSERT INTO cursor_pins SELECT c.id,c.team_id,json_extract(j.value,'$[0]'),json_extract(j.value,'$[1]'),c.expires_at FROM cursors c, json_each(c.snapshot_json) j WHERE c.rowid>? AND c.rowid<=? AND c.expires_at>NOWMS AND c.kind IN ('list','search') AND c.snapshot_json IS NOT NULL AND json_extract(j.value,'$[0]') IS NOT NULL AND json_extract(j.value,'$[1]') IS NOT NULL AND NOT EXISTS (SELECT 1 FROM cursor_pins p WHERE p.cursor_id=c.id)",
|
|
225
|
-
"INSERT INTO cursor_pins SELECT c.id,c.team_id,json_extract(c.snapshot_json,'$.session_id'),json_extract(c.snapshot_json,'$.revision'),c.expires_at FROM cursors c WHERE c.rowid>? AND c.rowid<=? AND c.expires_at>NOWMS AND c.kind='history' AND json_extract(c.snapshot_json,'$.session_id') IS NOT NULL AND json_extract(c.snapshot_json,'$.revision') IS NOT NULL AND NOT EXISTS (SELECT 1 FROM cursor_pins p WHERE p.cursor_id=c.id)"]],
|
|
226
254
|
['artifact_refs','session_artifacts',[
|
|
227
255
|
"INSERT OR IGNORE INTO upload_refs SELECT team_id,upload_id,'a:'||session_id||':'||revision||':'||bundle_id FROM session_artifacts WHERE rowid>? AND rowid<=?"]],
|
|
228
256
|
['continuation_refs','continuations',[
|
|
@@ -234,6 +262,8 @@ const BACKFILLS=[
|
|
|
234
262
|
['uploads','uploads',[
|
|
235
263
|
'INSERT INTO upload_prune_queue SELECT team_id,id,1 FROM uploads WHERE rowid>? AND rowid<=? AND committed=1 ON CONFLICT(team_id,upload_id) DO UPDATE SET generation=generation+1',
|
|
236
264
|
'INSERT OR IGNORE INTO pending_uploads SELECT team_id,id,source_id,created_at FROM uploads WHERE rowid>? AND rowid<=? AND committed=0']],
|
|
265
|
+
['deliveries','app_webhook_deliveries',[
|
|
266
|
+
"INSERT OR IGNORE INTO delivery_settled SELECT id,created_at FROM app_webhook_deliveries WHERE rowid>? AND rowid<=? AND state<>'pending'"]],
|
|
237
267
|
['blobs','artifact_blobs',[
|
|
238
268
|
'INSERT INTO blob_prune_queue SELECT team_id,sha256,1 FROM artifact_blobs WHERE rowid>? AND rowid<=? ON CONFLICT(team_id,sha256) DO UPDATE SET generation=generation+1']],
|
|
239
269
|
];
|
|
@@ -270,6 +300,8 @@ const MAINTAIN_MS=60_000,MAINTAIN_BUDGET_MS=250;
|
|
|
270
300
|
/** Sessions whose versions a run reads, and versions it removes from each; how far it walks down a source to find the
|
|
271
301
|
* newest version a capture names. */
|
|
272
302
|
const VERSION_SESSION_BATCH=10,VERSION_PRUNE_BATCH=20,CAPTURE_WALK=500;
|
|
303
|
+
/** Message rows a run moves or removes in all (a long session's version is worked across runs), and per statement. */
|
|
304
|
+
const MESSAGE_BUDGET=2000,MESSAGE_CHUNK=500;
|
|
273
305
|
/** Committed captures whose parts a run removes, and parts per capture per run. */
|
|
274
306
|
const PARTS_CAPTURE_BATCH=20,PART_PRUNE_BATCH=50;
|
|
275
307
|
/** Rows of one expiry a run removes; captures and uploads in flight a run expires. */
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// A delivery that settled (delivered, failed, cancelled) is queued with its age, so retention removes week-old
|
|
2
|
+
// settled deliveries through that queue: a pending delivery never stands in front of them (t_6c01c966, #1120 review
|
|
3
|
+
// DO7). Constant cost at activation: a table, its index and two triggers; rows from before it are walked into the
|
|
4
|
+
// queue by `maintain` (its `deliveries` walk).
|
|
5
|
+
export default `
|
|
6
|
+
CREATE TABLE delivery_settled (id TEXT PRIMARY KEY, created_at INTEGER NOT NULL) STRICT;
|
|
7
|
+
CREATE INDEX delivery_settled_created ON delivery_settled(created_at);
|
|
8
|
+
CREATE TRIGGER delivery_settled_on_state AFTER UPDATE OF state ON app_webhook_deliveries WHEN NEW.state<>'pending' BEGIN
|
|
9
|
+
INSERT OR IGNORE INTO delivery_settled VALUES (NEW.id, NEW.created_at);
|
|
10
|
+
END;
|
|
11
|
+
CREATE TRIGGER delivery_settled_on_delete AFTER DELETE ON app_webhook_deliveries BEGIN
|
|
12
|
+
DELETE FROM delivery_settled WHERE id=OLD.id;
|
|
13
|
+
END;
|
|
14
|
+
`;
|
|
@@ -22,7 +22,8 @@ import m020 from './020.mjs';
|
|
|
22
22
|
import m021 from './021.mjs';
|
|
23
23
|
import m022 from './022.mjs';
|
|
24
24
|
import m023 from './023.mjs';
|
|
25
|
-
|
|
25
|
+
import m024 from './024.mjs';
|
|
26
|
+
export const MIGRATIONS = [m001, m002, m003, m004, m005, m006, m007, m008, m009, m010, m011, m012, m013, m014, m015, m016, m017, m018, m019, m020, m021, m022, m023, m024];
|
|
26
27
|
/** A message row's `until_revision` while its session's current version still holds it (schema 9). */
|
|
27
28
|
export const OPEN_REVISION = Number.MAX_SAFE_INTEGER;
|
|
28
29
|
/** SQL: message row `m` belongs to the version at revision ?1 (bind the revision twice). */
|