@volter/teams 0.1.56 → 0.1.58

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/teams",
3
- "version": "0.1.56",
3
+ "version": "0.1.58",
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/core.mjs CHANGED
@@ -69,6 +69,21 @@ export async function maintain(api){const now=api.clock.nowMs();
69
69
  // they go (a backlog of them filled the database).
70
70
  const parts=await api.store.query(`SELECT p.team_id,p.capture_id,p.part_index FROM capture_parts p JOIN captures c ON c.team_id=p.team_id AND c.id=p.capture_id WHERE c.state='committed' LIMIT ${PART_PRUNE_BATCH}`,[]);
71
71
  for(let at=0;at<parts.length;at+=50)await api.store.transaction(parts.slice(at,at+50).map(({team_id,capture_id,part_index})=>({sql:'DELETE FROM capture_parts WHERE team_id=? AND capture_id=? AND part_index=?',params:[team_id,capture_id,part_index]})));
72
+ // Retired versions release their native bundles. Keep every bundle still named by a version,
73
+ // continuation (including completed receipts), or in-flight capture. Never-committed uploads retain their one-day grace.
74
+ const orphaned=await api.store.query(`SELECT u.team_id,u.id FROM uploads u WHERE u.committed=1
75
+ AND NOT EXISTS (SELECT 1 FROM session_artifacts a WHERE a.team_id=u.team_id AND a.upload_id=u.id)
76
+ AND NOT EXISTS (SELECT 1 FROM continuations c WHERE c.team_id=u.team_id AND instr(c.value_json,u.id)>0)
77
+ AND NOT EXISTS (SELECT 1 FROM captures c WHERE c.team_id=u.team_id AND c.state='staging' AND instr(c.begin_json,u.id)>0)
78
+ LIMIT 10`,[]);
79
+ for(const upload of orphaned)await api.store.transaction([{sql:`DELETE FROM uploads AS u WHERE team_id=? AND id=? AND committed=1
80
+ AND NOT EXISTS (SELECT 1 FROM session_artifacts a WHERE a.team_id=u.team_id AND a.upload_id=u.id)
81
+ AND NOT EXISTS (SELECT 1 FROM continuations c WHERE c.team_id=u.team_id AND instr(c.value_json,u.id)>0)
82
+ AND NOT EXISTS (SELECT 1 FROM captures c WHERE c.team_id=u.team_id AND c.state='staging' AND instr(c.begin_json,u.id)>0)`,params:[upload.team_id,upload.id]}]);
83
+ const unusedBlobs = await api.store.query(`SELECT team_id,sha256 FROM artifact_blobs b
84
+ WHERE NOT EXISTS (SELECT 1 FROM artifact_chunk_refs r WHERE r.team_id=b.team_id AND r.sha256=b.sha256) LIMIT 50`);
85
+ for (const blob of unusedBlobs) await api.store.transaction([{sql:`DELETE FROM artifact_blobs AS b WHERE team_id=? AND sha256=?
86
+ AND NOT EXISTS (SELECT 1 FROM artifact_chunk_refs r WHERE r.team_id=b.team_id AND r.sha256=b.sha256)`,params:[blob.team_id,blob.sha256]}]);
72
87
  // Every capture writes its session a new version (its messages are carried, not copied: schema 9), so versions pile
73
88
  // up (hundreds per session) until listing them outgrew a Worker's memory. Kept: each session's current version, the newest
74
89
  // version from each of its sources (the next capture and a correlation read that one), and every version an
@@ -76,6 +91,10 @@ export async function maintain(api){const now=api.clock.nowMs();
76
91
  const stale=await api.store.query(`SELECT v.team_id,v.session_id,v.revision FROM session_versions v JOIN sessions s ON s.team_id=v.team_id AND s.id=v.session_id
77
92
  WHERE v.revision<>s.revision
78
93
  AND EXISTS (SELECT 1 FROM session_versions n WHERE n.team_id=v.team_id AND n.session_id=v.session_id AND n.revision>v.revision AND json_extract(n.source_projection_json,'$.source_id') IS json_extract(v.source_projection_json,'$.source_id'))
94
+ AND (NOT EXISTS (SELECT 1 FROM session_artifacts a WHERE a.team_id=v.team_id AND a.session_id=v.session_id AND a.revision=v.revision AND a.provenance='capture')
95
+ OR EXISTS (SELECT 1 FROM session_artifacts a JOIN session_versions n ON n.team_id=a.team_id AND n.session_id=a.session_id AND n.revision=a.revision
96
+ WHERE a.team_id=v.team_id AND a.session_id=v.session_id AND a.revision>v.revision AND a.provenance='capture'
97
+ AND json_extract(n.source_projection_json,'$.source_id') IS json_extract(v.source_projection_json,'$.source_id')))
79
98
  AND NOT EXISTS (SELECT 1 FROM cursors c, json_each(c.snapshot_json) j WHERE c.team_id=v.team_id AND c.kind IN ('list','search') AND c.expires_at>? AND json_extract(j.value,'$[0]')=v.session_id AND json_extract(j.value,'$[1]')=v.revision)
80
99
  AND NOT EXISTS (SELECT 1 FROM cursors c WHERE c.team_id=v.team_id AND c.kind='history' AND c.expires_at>? AND json_extract(c.snapshot_json,'$.session_id')=v.session_id AND json_extract(c.snapshot_json,'$.revision')=v.revision)
81
100
  LIMIT ${VERSION_PRUNE_BATCH}`,[now,now]);
@@ -85,14 +104,6 @@ export async function maintain(api){const now=api.clock.nowMs();
85
104
  {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 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]},
86
105
  ...['messages','session_artifacts','session_versions'].map(table=>({sql:`DELETE FROM ${table} WHERE team_id=? AND session_id=? AND revision=?`,params:[team_id,session_id,revision]})),
87
106
  ]);
88
- // Retired versions release their native bundles. Keep every bundle still named by a version,
89
- // continuation (including completed receipts), or in-flight capture, and leave a full day for uploads.
90
- const orphaned=await api.store.query(`SELECT u.team_id,u.id FROM uploads u WHERE u.committed=1 AND u.created_at+86400000<=?
91
- AND NOT EXISTS (SELECT 1 FROM session_artifacts a WHERE a.team_id=u.team_id AND a.upload_id=u.id)
92
- AND NOT EXISTS (SELECT 1 FROM continuations c WHERE c.team_id=u.team_id AND instr(c.value_json,u.id)>0)
93
- AND NOT EXISTS (SELECT 1 FROM captures c WHERE c.team_id=u.team_id AND c.state='staging' AND instr(c.begin_json,u.id)>0)
94
- LIMIT 10`,[now]);
95
- for(const upload of orphaned)await api.store.transaction([{sql:'DELETE FROM uploads WHERE team_id=? AND id=?',params:[upload.team_id,upload.id]}]);
96
107
  await api.store.transaction([
97
108
  {sql:'DELETE FROM cursors WHERE expires_at<=?',params:[now]},
98
109
  {sql:'DELETE FROM global_cursors WHERE expires_at<=?',params:[now]},
@@ -108,7 +119,7 @@ export async function maintain(api){const now=api.clock.nowMs();
108
119
  // Presence reconciliation can INSERT. Never put it ahead of the deletes
109
120
  // that make room when the database is full.
110
121
  await reconcileMachinePresence(api);
111
- const result={pruned_versions:stale.length,pruned_parts:parts.length,pruned_idempotency:expired.length,pruned_uploads:orphaned.length,backlog:orphaned.length>=10||expired.length>=1000||stale.length>=VERSION_PRUNE_BATCH||parts.length>=PART_PRUNE_BATCH};
122
+ const result={pruned_versions:stale.length,pruned_parts:parts.length,pruned_idempotency:expired.length,pruned_uploads:orphaned.length,pruned_blobs:unusedBlobs.length,backlog:unusedBlobs.length>=50||orphaned.length>=10||expired.length>=1000||stale.length>=VERSION_PRUNE_BATCH||parts.length>=PART_PRUNE_BATCH};
112
123
  if(result.pruned_versions||result.pruned_parts||result.pruned_idempotency||result.pruned_uploads)console.log(`maintain: pruned ${result.pruned_uploads} unreachable bundles, ${result.pruned_idempotency} expired replay records, ${result.pruned_parts} parts, ${result.pruned_versions} versions${result.backlog?' (backlog)':''}`);
113
124
  return result;}
114
125
  /** Session versions `maintain` removes per run: small enough for one Durable Object alarm; a backlog runs again in seconds. */
@@ -0,0 +1,20 @@
1
+ // New captures share immutable chunk contents. Legacy uploads stay readable
2
+ // without copying their bytes during migration; ordinary retention retires them.
3
+ export default `
4
+ CREATE TABLE artifact_blobs (
5
+ team_id TEXT NOT NULL, sha256 TEXT NOT NULL, bytes BLOB NOT NULL,
6
+ PRIMARY KEY(team_id,sha256), CHECK(length(bytes)<=1048576)
7
+ ) STRICT;
8
+ CREATE TABLE artifact_chunk_refs (
9
+ team_id TEXT NOT NULL, upload_id TEXT NOT NULL, chunk_index INTEGER NOT NULL,
10
+ sha256 TEXT NOT NULL, PRIMARY KEY(team_id,upload_id,chunk_index),
11
+ FOREIGN KEY(team_id,upload_id) REFERENCES uploads(team_id,id) ON DELETE CASCADE,
12
+ FOREIGN KEY(team_id,sha256) REFERENCES artifact_blobs(team_id,sha256)
13
+ ) STRICT;
14
+ CREATE INDEX artifact_chunk_refs_blob ON artifact_chunk_refs(team_id,sha256);
15
+ CREATE VIEW artifact_chunk_data AS
16
+ SELECT team_id,upload_id,chunk_index,sha256,bytes FROM artifact_chunks
17
+ UNION ALL
18
+ SELECT r.team_id,r.upload_id,r.chunk_index,r.sha256,b.bytes
19
+ FROM artifact_chunk_refs r JOIN artifact_blobs b ON b.team_id=r.team_id AND b.sha256=r.sha256;
20
+ `;
@@ -9,7 +9,8 @@ import m007 from './007.mjs';
9
9
  import m008 from './008.mjs';
10
10
  import m009 from './009.mjs';
11
11
  import m010 from './010.mjs';
12
- export const MIGRATIONS = [m001, m002, m003, m004, m005, m006, m007, m008, m009, m010];
12
+ import m011 from './011.mjs';
13
+ export const MIGRATIONS = [m001, m002, m003, m004, m005, m006, m007, m008, m009, m010, m011];
13
14
  /** A message row's `until_revision` while its session's current version still holds it (schema 9). */
14
15
  export const OPEN_REVISION = Number.MAX_SAFE_INTEGER;
15
16
  /** SQL: message row `m` belongs to the version at revision ?1 (bind the revision twice). */