sphica 0.5.7 → 0.6.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/dist/deliver.js CHANGED
@@ -11086,7 +11086,7 @@ import os from "node:os";
11086
11086
  import path2 from "node:path";
11087
11087
  import { constants as C, DatabaseSync } from "node:sqlite";
11088
11088
  var SCHEMA_GENERATION = 2;
11089
- var SCHEMA_REVISION = 1;
11089
+ var SCHEMA_REVISION = 2;
11090
11090
  var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
11091
11091
  var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
11092
11092
  function requireRuntime() {
@@ -11110,7 +11110,7 @@ function prepare(raw, check) {
11110
11110
  const got = raw.prepare("pragma user_version").get()?.user_version;
11111
11111
  if (got === SCHEMA_REVISION)
11112
11112
  return;
11113
- throw new Error(`The database schema is revision ${got}, but this Sphica expects revision ${SCHEMA_REVISION}. ` + ((got ?? 0) < SCHEMA_REVISION ? "Move it aside (it is left unchanged), then run `sphica init`." : "Update sphica."));
11113
+ throw new Error(`The database schema is revision ${got}, but this Sphica expects revision ${SCHEMA_REVISION}. ` + ((got ?? 0) < SCHEMA_REVISION ? "Update the sphica CLI (`npm i -g sphica`), then run `sphica init` to migrate it (records are kept)." : "Update sphica."));
11114
11114
  }
11115
11115
  function generationOf(raw) {
11116
11116
  const has = raw.prepare("select 1 from sqlite_schema where type = 'table' and name = 'sphica_generation'").get();
@@ -11282,22 +11282,54 @@ function captureAuthorizer(own, action, p1, p2, triggerOrView) {
11282
11282
  return C2.SQLITE_DENY;
11283
11283
  }
11284
11284
  var writes = (action) => action === C2.SQLITE_INSERT || action === C2.SQLITE_UPDATE || action === C2.SQLITE_DELETE;
11285
+ var FORGET_ONLY = new Set(["forget_batch", "source_forgotten"]);
11286
+ var KEPT_BY_INGEST = new Set(["source", "unit_evidence", "unit_adoption"]);
11285
11287
  function ingestAuthorizer(action, p1, p2) {
11286
11288
  if (DDL().has(action))
11287
11289
  return C2.SQLITE_DENY;
11288
- if (writes(action) && p1 === "owner_identity")
11290
+ if (writes(action) && (p1 === "owner_identity" || FORGET_ONLY.has(p1 ?? "")))
11291
+ return C2.SQLITE_DENY;
11292
+ if (action === C2.SQLITE_DELETE && KEPT_BY_INGEST.has(p1 ?? ""))
11289
11293
  return C2.SQLITE_DENY;
11290
11294
  if (action === C2.SQLITE_PRAGMA)
11291
11295
  return readsDataVersion(p1, p2) ? C2.SQLITE_OK : C2.SQLITE_DENY;
11292
11296
  return C2.SQLITE_OK;
11293
11297
  }
11298
+ var FORGET_WRITES = {
11299
+ [C2.SQLITE_INSERT]: new Set(["forget_batch", "source_forgotten", "unit_state", "source_fts"]),
11300
+ [C2.SQLITE_DELETE]: new Set([
11301
+ "source",
11302
+ "external_reference",
11303
+ "unit_evidence",
11304
+ "unit_adoption",
11305
+ "source_processing",
11306
+ "source_fts"
11307
+ ])
11308
+ };
11309
+ var FORGET_UPDATES = {
11310
+ unit: new Set(["lifecycle", "revision"]),
11311
+ unit_state: new Set(["source_id"])
11312
+ };
11313
+ function forgetAuthorizer(action, p1, p2) {
11314
+ if (SHADOW.test(p1 ?? "") && writes(action))
11315
+ return C2.SQLITE_OK;
11316
+ if (action === C2.SQLITE_UPDATE)
11317
+ return FORGET_UPDATES[p1 ?? ""]?.has(p2 ?? "") ? C2.SQLITE_OK : C2.SQLITE_DENY;
11318
+ if (action === C2.SQLITE_INSERT || action === C2.SQLITE_DELETE)
11319
+ return FORGET_WRITES[action]?.has(p1 ?? "") ? C2.SQLITE_OK : C2.SQLITE_DENY;
11320
+ if (action === C2.SQLITE_PRAGMA)
11321
+ return readsDataVersion(p1, p2) || p1 === "secure_delete" && ["on", "1", "true"].includes((p2 ?? "").toLowerCase()) || p1 === "wal_checkpoint" ? C2.SQLITE_OK : C2.SQLITE_DENY;
11322
+ if (action === C2.SQLITE_READ || action === C2.SQLITE_SELECT || action === C2.SQLITE_FUNCTION || action === C2.SQLITE_TRANSACTION || action === C2.SQLITE_SAVEPOINT)
11323
+ return C2.SQLITE_OK;
11324
+ return C2.SQLITE_DENY;
11325
+ }
11294
11326
  function connectWriter(role, file = dbFile(), create = false) {
11295
11327
  requireRuntime();
11296
11328
  if (!create)
11297
11329
  requireFile(file);
11298
11330
  const raw = new DatabaseSync2(file);
11299
11331
  try {
11300
- prepare(raw, create ? "none" : role === "ingest" ? "revision" : "generation");
11332
+ prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation");
11301
11333
  raw.function("sphica_terms", { deterministic: true }, (text) => terms(String(text ?? "")).join(" "));
11302
11334
  } catch (e) {
11303
11335
  raw.close();
@@ -11305,6 +11337,8 @@ function connectWriter(role, file = dbFile(), create = false) {
11305
11337
  }
11306
11338
  if (role === "ingest")
11307
11339
  raw.setAuthorizer(ingestAuthorizer);
11340
+ else if (role === "forget")
11341
+ raw.setAuthorizer(forgetAuthorizer);
11308
11342
  else if (role === "capture") {
11309
11343
  let own = false;
11310
11344
  raw.setAuthorizer((action, p1, p2, _db, triggerOrView) => captureAuthorizer(own, action, p1, p2, triggerOrView));
@@ -11813,6 +11847,7 @@ function lock() {
11813
11847
  return null;
11814
11848
  }
11815
11849
  var BATCH = 500;
11850
+ var FLUSH_BUDGET_MS = 30000;
11816
11851
  var ROWS = 1000;
11817
11852
  var chunks = (xs, n = ROWS) => Array.from({ length: Math.ceil(xs.length / n) }, (_, i) => xs.slice(i * n, (i + 1) * n));
11818
11853
  async function write(db, batch, projects) {
@@ -11888,98 +11923,135 @@ async function write(db, batch, projects) {
11888
11923
  }
11889
11924
  var REJECTED = new Set([18, 19, 20, 25]);
11890
11925
  var rejected = (e) => REJECTED.has(sqliteCode(e) ?? -1);
11891
- async function flush(file = dbFile()) {
11892
- const unlock = lock();
11893
- if (!unlock)
11894
- return { sent: 0, deferred: 0, rejected: 0, busy: true };
11895
- const dir = spoolDir();
11896
- let client = null;
11926
+ var queued = (from) => {
11927
+ try {
11928
+ return fs4.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort().map((name) => ({ name, from }));
11929
+ } catch {
11930
+ return [];
11931
+ }
11932
+ };
11933
+ async function sendBatch(db, names) {
11934
+ const held = unregisteredDir();
11935
+ const records = [];
11936
+ for (const { name, from } of names) {
11937
+ let r;
11938
+ try {
11939
+ r = current(JSON.parse(fs4.readFileSync(path5.join(from, name), "utf8")));
11940
+ } catch {
11941
+ fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
11942
+ fs4.renameSync(path5.join(from, name), path5.join(rejectedDir(), name));
11943
+ continue;
11944
+ }
11945
+ if (r)
11946
+ records.push({ name, from, r });
11947
+ else
11948
+ fs4.rmSync(path5.join(from, name), { force: true });
11949
+ }
11950
+ const projects = new Map((await db.selectFrom("project").select(["id", "key", "name"]).where("key", "in", [...new Set(records.map((x) => x.r.project))]).execute()).map((p) => [p.key, { id: p.id, name: p.name }]));
11951
+ const known = records.filter((x) => projects.has(x.r.project));
11952
+ const strayed = records.filter((x) => !projects.has(x.r.project));
11953
+ let sent = 0;
11954
+ const bad = [];
11897
11955
  try {
11898
- const held = unregisteredDir();
11899
- const list = (from) => {
11956
+ sent = await write(db, known.map((x) => x.r), projects);
11957
+ } catch (e) {
11958
+ if (!rejected(e))
11959
+ throw e;
11960
+ for (const x of known) {
11900
11961
  try {
11901
- return fs4.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((name) => ({ name, from }));
11902
- } catch {
11903
- return [];
11962
+ sent += await write(db, [x.r], projects);
11963
+ } catch (e2) {
11964
+ if (!rejected(e2))
11965
+ throw e2;
11966
+ bad.push(x);
11904
11967
  }
11905
- };
11906
- const names = [...list(dir), ...list(held)].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).slice(0, BATCH);
11907
- if (names.length === 0)
11908
- return { sent: 0, deferred: 0, rejected: 0 };
11909
- const records = [];
11910
- for (const { name, from } of names) {
11911
- let r;
11968
+ }
11969
+ }
11970
+ if (bad.length) {
11971
+ fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
11972
+ for (const x of bad) {
11912
11973
  try {
11913
- r = current(JSON.parse(fs4.readFileSync(path5.join(from, name), "utf8")));
11914
- } catch {
11915
- fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
11916
- fs4.renameSync(path5.join(from, name), path5.join(rejectedDir(), name));
11917
- continue;
11918
- }
11919
- if (r)
11920
- records.push({ name, from, r });
11921
- else
11922
- fs4.rmSync(path5.join(from, name), { force: true });
11923
- }
11924
- const db = openWriter("capture", file);
11925
- client = db;
11926
- const projects = new Map((await db.selectFrom("project").select(["id", "key", "name"]).where("key", "in", [...new Set(records.map((x) => x.r.project))]).execute()).map((p) => [p.key, { id: p.id, name: p.name }]));
11927
- const known = records.filter((x) => projects.has(x.r.project));
11928
- const strayed = records.filter((x) => !projects.has(x.r.project));
11929
- let sent = 0;
11930
- const bad = [];
11931
- try {
11932
- sent = await write(db, known.map((x) => x.r), projects);
11933
- } catch (e) {
11934
- if (!rejected(e))
11935
- throw e;
11936
- for (const x of known) {
11937
- try {
11938
- sent += await write(db, [x.r], projects);
11939
- } catch (e2) {
11940
- if (!rejected(e2))
11941
- throw e2;
11942
- bad.push(x);
11943
- }
11974
+ fs4.renameSync(path5.join(x.from, x.name), path5.join(rejectedDir(), x.name));
11975
+ } catch (e) {
11976
+ if (e.code !== "ENOENT")
11977
+ throw e;
11944
11978
  }
11945
11979
  }
11946
- if (bad.length) {
11947
- fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
11948
- for (const x of bad) {
11949
- try {
11950
- fs4.renameSync(path5.join(x.from, x.name), path5.join(rejectedDir(), x.name));
11951
- } catch (e) {
11952
- if (e.code !== "ENOENT")
11953
- throw e;
11954
- }
11980
+ }
11981
+ if (strayed.length) {
11982
+ fs4.mkdirSync(held, { recursive: true, mode: 448 });
11983
+ for (const x of strayed) {
11984
+ if (x.from === held)
11985
+ continue;
11986
+ try {
11987
+ fs4.renameSync(path5.join(x.from, x.name), path5.join(held, x.name));
11988
+ } catch (e) {
11989
+ if (e.code !== "ENOENT")
11990
+ throw e;
11955
11991
  }
11956
11992
  }
11957
- if (strayed.length) {
11958
- fs4.mkdirSync(held, { recursive: true, mode: 448 });
11959
- for (const x of strayed) {
11960
- if (x.from === held)
11961
- continue;
11962
- try {
11963
- fs4.renameSync(path5.join(x.from, x.name), path5.join(held, x.name));
11964
- } catch (e) {
11965
- if (e.code !== "ENOENT")
11966
- throw e;
11993
+ }
11994
+ const moved = new Set([...bad, ...strayed].map((x) => x.name));
11995
+ for (const x of records)
11996
+ if (!moved.has(x.name))
11997
+ fs4.rmSync(path5.join(x.from, x.name), { force: true });
11998
+ return { sent, rejected: bad.length };
11999
+ }
12000
+ async function flush(file = dbFile(), budgetMs = FLUSH_BUDGET_MS) {
12001
+ const deadline = Date.now() + budgetMs;
12002
+ const dir = spoolDir();
12003
+ const held = unregisteredDir();
12004
+ const total = { sent: 0, deferred: 0, rejected: 0 };
12005
+ let batches = 0;
12006
+ let queueBatches = 0;
12007
+ for (let first = true;; first = false) {
12008
+ let unlock = lock();
12009
+ while (!unlock && Date.now() < deadline) {
12010
+ await new Promise((resolve) => setTimeout(resolve, Math.min(100, deadline - Date.now())));
12011
+ if (Date.now() < deadline)
12012
+ unlock = lock();
12013
+ }
12014
+ if (!unlock)
12015
+ return first ? { ...total, busy: true } : total;
12016
+ let client = null;
12017
+ const send = async (names) => {
12018
+ client ??= openWriter("capture", file);
12019
+ const r = await sendBatch(client, names);
12020
+ total.sent += r.sent;
12021
+ total.rejected += r.rejected;
12022
+ batches++;
12023
+ };
12024
+ const late = () => batches > 0 && Date.now() >= deadline;
12025
+ const sentBefore = batches;
12026
+ try {
12027
+ if (first) {
12028
+ prune(held);
12029
+ for (const part of chunks(queued(held), BATCH)) {
12030
+ if (late())
12031
+ break;
12032
+ await send(part);
11967
12033
  }
11968
12034
  }
12035
+ while (queueBatches === 0 || !late()) {
12036
+ const part = queued(dir).slice(0, BATCH);
12037
+ if (part.length === 0)
12038
+ break;
12039
+ await send(part);
12040
+ queueBatches++;
12041
+ }
12042
+ prune(held);
12043
+ total.deferred = queued(held).length;
12044
+ if (batches > sentBefore)
12045
+ writeState({ flushedAt: new Date().toISOString(), error: null, deferred: total.deferred });
12046
+ } catch (e) {
12047
+ writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
12048
+ throw e;
12049
+ } finally {
12050
+ await client?.destroy().catch(() => {});
12051
+ unlock();
11969
12052
  }
11970
- const moved = new Set([...bad, ...strayed].map((x) => x.name));
11971
- for (const x of records)
11972
- if (!moved.has(x.name))
11973
- fs4.rmSync(path5.join(x.from, x.name), { force: true });
11974
- prune(held);
11975
- writeState({ flushedAt: new Date().toISOString(), error: null, deferred: strayed.length });
11976
- return { sent, deferred: strayed.length, rejected: bad.length };
11977
- } catch (e) {
11978
- writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
11979
- throw e;
11980
- } finally {
11981
- await client?.destroy().catch(() => {});
11982
- unlock();
12053
+ if (Date.now() >= deadline || queued(dir).length === 0)
12054
+ return total;
11983
12055
  }
11984
12056
  }
11985
12057
  async function readInput(stream) {