@alma-harness/runtime 0.11.0 → 0.12.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.
@@ -0,0 +1,118 @@
1
+ import {
2
+ createMaintenance,
3
+ scopeRotation
4
+ } from "./chunk-6MFSZ4VS.js";
5
+ import {
6
+ MAINTENANCE_ROLE,
7
+ createPostgresRuntime
8
+ } from "./chunk-EGQL2P7U.js";
9
+ import {
10
+ namespace
11
+ } from "./chunk-II7NDZRL.js";
12
+
13
+ // src/maintenance.ts
14
+ import pg from "pg";
15
+ var DUE = `with due as (
16
+ select org, uid from alma_operation_admissions where status='active' and deadline_at<=clock_timestamp()
17
+ union select org, uid from alma_operation_roots where status='active' and deadline_at<=clock_timestamp()
18
+ union select org, uid from alma_executions where state in ('prepared','dispatching') and deadline_at<=clock_timestamp()
19
+ union select org, uid from alma_executions where state in ('result_received','settled') and pricing->>'status'='ready'
20
+ union select e.org, e.uid from alma_executions e join alma_operation_calls c on c.org=e.org and c.uid=e.uid and c.operation_key=e.operation_key
21
+ where e.state='completed' and not exists(select from alma_operation_financial_calls f where f.org=c.org and f.uid=c.uid and f.root_id=c.root_id and f.call_id=c.call_id)
22
+ union select org, uid from alma_usage_inbox_pending)
23
+ select org collate "C" as org, uid collate "C" as uid from due`;
24
+ var ORDER = `order by org collate "C", uid collate "C"`;
25
+ var ELIGIBLE = `from alma_executions e
26
+ left join alma_operation_calls c on c.org=e.org and c.uid=e.uid and c.operation_key=e.operation_key
27
+ left join alma_operation_roots r on r.org=c.org and r.uid=c.uid and r.id=c.root_id
28
+ where e.org=$1 and e.uid=$2 and ((e.state in ('result_received','settled') and e.pricing->>'status'='ready')
29
+ or (e.state='completed' and c.call_id is not null and not exists(select from alma_operation_financial_calls f where f.org=c.org and f.uid=c.uid and f.root_id=c.root_id and f.call_id=c.call_id)))`;
30
+ var json = (v) => v === null || v === void 0 ? null : JSON.stringify(v);
31
+ function createPostgresMaintenanceDiscovery(pool, workerURL) {
32
+ const rotation = scopeRotation(pool, { role: MAINTENANCE_ROLE, lockPrefix: "alma-maintenance", workerURL }), tx = rotation.tx;
33
+ return {
34
+ verify: rotation.verify,
35
+ claim: rotation.claim,
36
+ nextScopes(limit) {
37
+ return tx(async (c) => {
38
+ const cur = (await c.query("select org, uid from alma_maintenance_cursor where id=1 for update")).rows[0];
39
+ if (!cur) throw new Error("Maintenance cursor missing: rerun migratePostgresRuntime");
40
+ let rows = (await c.query(`${DUE} where $1::text is null or (org collate "C", uid collate "C") > ($1::text collate "C", $2::text collate "C") ${ORDER} limit $3`, [cur.org, cur.uid, limit])).rows;
41
+ if (rows.length < limit && cur.org !== null)
42
+ rows = rows.concat((await c.query(`${DUE} where (org collate "C", uid collate "C") <= ($1::text collate "C", $2::text collate "C") ${ORDER} limit $3`, [cur.org, cur.uid, limit - rows.length])).rows);
43
+ const last = rows.at(-1);
44
+ if (last) await c.query("update alma_maintenance_cursor set org=$1, uid=$2, updated_at=clock_timestamp() where id=1", [last.org, last.uid]);
45
+ return rows.map((r) => ({ org: r.org, uid: r.uid }));
46
+ });
47
+ },
48
+ executionLap(scope) {
49
+ return tx(async (c) => {
50
+ const r = (await c.query(`select e.created_at::text as created_at, e.operation_key, count(*) over ()::int as n
51
+ ${ELIGIBLE} order by e.created_at desc, e.operation_key desc limit 1`, [scope.org, scope.uid])).rows[0];
52
+ return r ? { bound: { createdAt: r.created_at, operationKey: r.operation_key }, budget: r.n } : null;
53
+ });
54
+ },
55
+ usageLap(scope) {
56
+ return tx(async (c) => {
57
+ const r = (await c.query(`select received_at, id, count(*) over ()::int as n from alma_usage_inbox_pending
58
+ where org=$1 and uid=$2 order by received_at desc, id collate "C" desc limit 1`, [scope.org, scope.uid])).rows[0];
59
+ return r ? { bound: { receivedAt: r.received_at.toISOString(), id: r.id }, budget: r.n } : null;
60
+ });
61
+ },
62
+ recoverable(scope, after, bound, limit) {
63
+ return tx(async (c) => (await c.query(`select e.operation_key, e.created_at::text as created_at, r.key as root_key
64
+ ${ELIGIBLE} and ($3::timestamptz is null or (e.created_at, e.operation_key) > ($3::timestamptz, $4::text))
65
+ and (e.created_at, e.operation_key) <= ($5::timestamptz, $6::text)
66
+ order by e.created_at, e.operation_key limit $7`, [scope.org, scope.uid, after?.createdAt ?? null, after?.operationKey ?? null, bound.createdAt, bound.operationKey, limit])).rows.map((r) => ({ operationKey: r.operation_key, createdAt: r.created_at, rootKey: r.root_key })));
67
+ },
68
+ members(scope, keys) {
69
+ return tx(async (c) => new Map((await c.query(
70
+ `select c.operation_key, r.key from alma_operation_calls c
71
+ join alma_operation_roots r on r.org=c.org and r.uid=c.uid and r.id=c.root_id where c.org=$1 and c.uid=$2 and c.operation_key=any($3::text[])`,
72
+ [scope.org, scope.uid, keys]
73
+ )).rows.map((r) => [r.operation_key, r.key])));
74
+ },
75
+ cursors(scope) {
76
+ return tx(async (c) => {
77
+ const row = (await c.query(`select executions_after, executions_bound, executions_budget, usage_after, usage_bound, usage_budget
78
+ from alma_maintenance_scopes where org=$1 and uid=$2`, [scope.org, scope.uid])).rows[0];
79
+ const lap = (after, bound, budget) => after || bound ? { after: after ?? null, bound: bound ?? null, budget: budget ?? null } : null;
80
+ return { executions: lap(row?.executions_after, row?.executions_bound, row?.executions_budget), usage: lap(row?.usage_after, row?.usage_bound, row?.usage_budget) };
81
+ });
82
+ },
83
+ async saveCursors(scope, { executions: e, usage: u }) {
84
+ await tx((c) => c.query(
85
+ `insert into alma_maintenance_scopes (org, uid, executions_after, executions_bound, executions_budget, usage_after, usage_bound, usage_budget)
86
+ values ($1, $2, $3::jsonb, $4::jsonb, $5, $6::jsonb, $7::jsonb, $8)
87
+ on conflict (org, uid) do update set executions_after=excluded.executions_after, executions_bound=excluded.executions_bound, executions_budget=excluded.executions_budget,
88
+ usage_after=excluded.usage_after, usage_bound=excluded.usage_bound, usage_budget=excluded.usage_budget, updated_at=clock_timestamp()`,
89
+ [scope.org, scope.uid, json(e?.after), json(e?.bound), e?.budget ?? null, json(u?.after), json(u?.bound), u?.budget ?? null]
90
+ ));
91
+ }
92
+ };
93
+ }
94
+ function createPostgresMaintenance(value) {
95
+ if (!value || typeof value !== "object" || !value.discovery || !value.worker) throw new TypeError("Maintenance requires discovery and worker logins");
96
+ const base = { schema: value.schema, rootSchema: value.rootSchema, onPoolError: value.onPoolError };
97
+ const d = namespace({ ...base, connectionString: value.discovery.connectionString }), workerURL = namespace({ ...base, connectionString: value.worker.connectionString }).connectionString;
98
+ const runtime = createPostgresRuntime({ ...base, connectionString: workerURL, stepResultPolicy: value.stepResultPolicy, rootResultPolicy: value.rootResultPolicy, poolMax: { execution: 2, roots: 1 } });
99
+ const pool = new pg.Pool({ connectionString: d.connectionString, options: `-c search_path=${d.schema}`, max: 2, connectionTimeoutMillis: 5e3, idleTimeoutMillis: 1e4 });
100
+ pool.on("error", (error) => d.onPoolError(error, d.schema));
101
+ const maintenance = createMaintenance({ stores: runtime.stores, discovery: createPostgresMaintenanceDiscovery(pool, workerURL) });
102
+ let closing;
103
+ return {
104
+ maintain: maintenance.maintain,
105
+ close() {
106
+ return closing ??= Promise.allSettled([runtime.close(), pool.end()]).then((results) => {
107
+ const errors = results.flatMap((r) => r.status === "rejected" ? [r.reason] : []);
108
+ if (errors.length) throw new AggregateError(errors, "Maintenance pool cleanup failed");
109
+ });
110
+ }
111
+ };
112
+ }
113
+ export {
114
+ createMaintenance,
115
+ createPostgresMaintenance,
116
+ createPostgresMaintenanceDiscovery
117
+ };
118
+ //# sourceMappingURL=maintenance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/maintenance.ts"],"sourcesContent":["import pg from \"pg\";\nimport type { Scope } from \"@alma-harness/core\";\nimport type { RuntimePolicies } from \"./index\";\nimport { createMaintenance, type ExecutionCursor, type MaintenanceDiscovery, type UsageCursor } from \"./maintenance-core\";\nimport { scopeRotation } from \"./scope-rotation\";\nimport { MAINTENANCE_ROLE } from \"./maintenance-schema\";\nimport { namespace } from \"./policies\";\nimport { createPostgresRuntime } from \"./postgres\";\nexport { createMaintenance } from \"./maintenance-core\";\nexport type { MaintenanceDiscovery, MaintenanceReport, RecoverableExecution, ScopeClaim, ExecutionCursor, UsageCursor, Lap, MaintenanceCursors } from \"./maintenance-core\";\n\nexport interface PostgresMaintenanceOptions extends RuntimePolicies {\n /** A login that is a member of alma_maintenance only. */\n discovery: { connectionString: string };\n /** A login that is a member of alma_app only, never the request path's. */\n worker: { connectionString: string };\n schema: string; rootSchema: string;\n onPoolError: (error: Error, schema: string) => void;\n}\n// Every kind of due work, as scopes only. Column grants restrict this role to the columns named here.\nconst DUE = `with due as (\n select org, uid from alma_operation_admissions where status='active' and deadline_at<=clock_timestamp()\n union select org, uid from alma_operation_roots where status='active' and deadline_at<=clock_timestamp()\n union select org, uid from alma_executions where state in ('prepared','dispatching') and deadline_at<=clock_timestamp()\n union select org, uid from alma_executions where state in ('result_received','settled') and pricing->>'status'='ready'\n union select e.org, e.uid from alma_executions e join alma_operation_calls c on c.org=e.org and c.uid=e.uid and c.operation_key=e.operation_key\n where e.state='completed' and not exists(select from alma_operation_financial_calls f where f.org=c.org and f.uid=c.uid and f.root_id=c.root_id and f.call_id=c.call_id)\n union select org, uid from alma_usage_inbox_pending)\nselect org collate \"C\" as org, uid collate \"C\" as uid from due`;\nconst ORDER = `order by org collate \"C\", uid collate \"C\"`;\n// Recoverable executions of one scope ($1, $2), with the root key of a member.\nconst ELIGIBLE = `from alma_executions e\n left join alma_operation_calls c on c.org=e.org and c.uid=e.uid and c.operation_key=e.operation_key\n left join alma_operation_roots r on r.org=c.org and r.uid=c.uid and r.id=c.root_id\n where e.org=$1 and e.uid=$2 and ((e.state in ('result_received','settled') and e.pricing->>'status'='ready')\n or (e.state='completed' and c.call_id is not null and not exists(select from alma_operation_financial_calls f where f.org=c.org and f.uid=c.uid and f.root_id=c.root_id and f.call_id=c.call_id)))`;\ntype Row = { executions_after: ExecutionCursor | null; executions_bound: ExecutionCursor | null; executions_budget: number | null;\n usage_after: UsageCursor | null; usage_bound: UsageCursor | null; usage_budget: number | null };\nconst json = (v: unknown) => v === null || v === undefined ? null : JSON.stringify(v);\n\n/** The PostgreSQL discovery on a pool of the discovery login; `workerURL` is only used to verify the worker login. */\nexport function createPostgresMaintenanceDiscovery(pool: pg.Pool, workerURL: string): MaintenanceDiscovery {\n const rotation = scopeRotation(pool, { role: MAINTENANCE_ROLE, lockPrefix: \"alma-maintenance\", workerURL }), tx = rotation.tx;\n return {\n verify: rotation.verify,\n claim: rotation.claim,\n nextScopes(limit) {\n return tx(async c => {\n const cur = (await c.query<{ org: string | null; uid: string | null }>(\"select org, uid from alma_maintenance_cursor where id=1 for update\")).rows[0];\n if (!cur) throw new Error(\"Maintenance cursor missing: rerun migratePostgresRuntime\");\n let rows = (await c.query<Scope>(`${DUE} where $1::text is null or (org collate \"C\", uid collate \"C\") > ($1::text collate \"C\", $2::text collate \"C\") ${ORDER} limit $3`, [cur.org, cur.uid, limit])).rows;\n if (rows.length < limit && cur.org !== null)\n rows = rows.concat((await c.query<Scope>(`${DUE} where (org collate \"C\", uid collate \"C\") <= ($1::text collate \"C\", $2::text collate \"C\") ${ORDER} limit $3`, [cur.org, cur.uid, limit - rows.length])).rows);\n const last = rows.at(-1);\n if (last) await c.query(\"update alma_maintenance_cursor set org=$1, uid=$2, updated_at=clock_timestamp() where id=1\", [last.org, last.uid]);\n return rows.map(r => ({ org: r.org, uid: r.uid }));\n });\n },\n executionLap(scope) {\n return tx(async c => {\n const r = (await c.query<{ created_at: string; operation_key: string; n: number }>(`select e.created_at::text as created_at, e.operation_key, count(*) over ()::int as n\n ${ELIGIBLE} order by e.created_at desc, e.operation_key desc limit 1`, [scope.org, scope.uid])).rows[0];\n return r ? { bound: { createdAt: r.created_at, operationKey: r.operation_key }, budget: r.n } : null;\n });\n },\n usageLap(scope) {\n return tx(async c => {\n const r = (await c.query<{ received_at: Date; id: string; n: number }>(`select received_at, id, count(*) over ()::int as n from alma_usage_inbox_pending\n where org=$1 and uid=$2 order by received_at desc, id collate \"C\" desc limit 1`, [scope.org, scope.uid])).rows[0];\n return r ? { bound: { receivedAt: r.received_at.toISOString(), id: r.id }, budget: r.n } : null;\n });\n },\n recoverable(scope, after, bound, limit) {\n return tx(async c => (await c.query<{ operation_key: string; created_at: string; root_key: string | null }>(`select e.operation_key, e.created_at::text as created_at, r.key as root_key\n ${ELIGIBLE} and ($3::timestamptz is null or (e.created_at, e.operation_key) > ($3::timestamptz, $4::text))\n and (e.created_at, e.operation_key) <= ($5::timestamptz, $6::text)\n order by e.created_at, e.operation_key limit $7`, [scope.org, scope.uid, after?.createdAt ?? null, after?.operationKey ?? null, bound.createdAt, bound.operationKey, limit])).rows\n .map(r => ({ operationKey: r.operation_key, createdAt: r.created_at, rootKey: r.root_key })));\n },\n members(scope, keys) {\n return tx(async c => new Map((await c.query<{ operation_key: string; key: string }>(`select c.operation_key, r.key from alma_operation_calls c\n join alma_operation_roots r on r.org=c.org and r.uid=c.uid and r.id=c.root_id where c.org=$1 and c.uid=$2 and c.operation_key=any($3::text[])`,\n [scope.org, scope.uid, keys])).rows.map(r => [r.operation_key, r.key])));\n },\n cursors(scope) {\n return tx(async c => {\n const row = (await c.query<Row>(`select executions_after, executions_bound, executions_budget, usage_after, usage_bound, usage_budget\n from alma_maintenance_scopes where org=$1 and uid=$2`, [scope.org, scope.uid])).rows[0];\n const lap = <C>(after: C | null | undefined, bound: C | null | undefined, budget: number | null | undefined) =>\n after || bound ? { after: after ?? null, bound: bound ?? null, budget: budget ?? null } : null;\n return { executions: lap(row?.executions_after, row?.executions_bound, row?.executions_budget), usage: lap(row?.usage_after, row?.usage_bound, row?.usage_budget) };\n });\n },\n async saveCursors(scope, { executions: e, usage: u }) {\n await tx(c => c.query(`insert into alma_maintenance_scopes (org, uid, executions_after, executions_bound, executions_budget, usage_after, usage_bound, usage_budget)\n values ($1, $2, $3::jsonb, $4::jsonb, $5, $6::jsonb, $7::jsonb, $8)\n on conflict (org, uid) do update set executions_after=excluded.executions_after, executions_bound=excluded.executions_bound, executions_budget=excluded.executions_budget,\n usage_after=excluded.usage_after, usage_bound=excluded.usage_bound, usage_budget=excluded.usage_budget, updated_at=clock_timestamp()`,\n [scope.org, scope.uid, json(e?.after), json(e?.bound), e?.budget ?? null, json(u?.after), json(u?.bound), u?.budget ?? null]));\n },\n };\n}\n\n/** Its own logins and pools, never the request path's. Construction opens no connection (spec: runtime-maintenance). */\nexport function createPostgresMaintenance(value: PostgresMaintenanceOptions) {\n if (!value || typeof value !== \"object\" || !value.discovery || !value.worker) throw new TypeError(\"Maintenance requires discovery and worker logins\");\n const base = { schema: value.schema, rootSchema: value.rootSchema, onPoolError: value.onPoolError };\n const d = namespace({ ...base, connectionString: value.discovery.connectionString }), workerURL = namespace({ ...base, connectionString: value.worker.connectionString }).connectionString;\n const runtime = createPostgresRuntime({ ...base, connectionString: workerURL, stepResultPolicy: value.stepResultPolicy, rootResultPolicy: value.rootResultPolicy, poolMax: { execution: 2, roots: 1 } });\n const pool = new pg.Pool({ connectionString: d.connectionString, options: `-c search_path=${d.schema}`, max: 2, connectionTimeoutMillis: 5000, idleTimeoutMillis: 10000 });\n pool.on(\"error\", error => d.onPoolError(error, d.schema));\n const maintenance = createMaintenance({ stores: runtime.stores, discovery: createPostgresMaintenanceDiscovery(pool, workerURL) });\n let closing: Promise<void> | undefined;\n return {\n maintain: maintenance.maintain,\n close() {\n return closing ??= Promise.allSettled([runtime.close(), pool.end()]).then(results => {\n const errors = results.flatMap(r => r.status === \"rejected\" ? [r.reason] : []);\n if (errors.length) throw new AggregateError(errors, \"Maintenance pool cleanup failed\");\n });\n },\n };\n}\n"],"mappings":"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","names":[]}
package/dist/memory.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  policies
3
- } from "./chunk-3OEFEOI4.js";
3
+ } from "./chunk-II7NDZRL.js";
4
4
 
5
5
  // src/memory.ts
6
- import { InMemoryExecutionStore, InMemorySessionStore } from "@alma-harness/core/testing";
6
+ import { InMemoryExecutionStore, InMemorySessionLabelStore, InMemorySessionStore, RecordingAuditLog } from "@alma-harness/core/testing";
7
7
  import { InMemoryOperationSessionStore, InMemoryOperationTreeStore, InMemoryOperationAccountingStore, InMemoryUsageInbox, InMemoryGovernedCostSettlementStore } from "@alma-harness/execution/testing";
8
8
  import { InMemoryExecutionResultStore } from "@alma-harness/memory/testing";
9
9
  function createMemoryRuntime(value) {
@@ -21,7 +21,9 @@ function createMemoryRuntime(value) {
21
21
  spend: settlements,
22
22
  trees,
23
23
  accounting: new InMemoryOperationAccountingStore(trees, settlements)
24
- }
24
+ },
25
+ labels: new InMemorySessionLabelStore(),
26
+ audit: new RecordingAuditLog()
25
27
  },
26
28
  async close() {
27
29
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/memory.ts"],"sourcesContent":["import { InMemoryExecutionStore, InMemorySessionStore } from \"@alma-harness/core/testing\";\nimport { InMemoryOperationSessionStore, InMemoryOperationTreeStore, InMemoryOperationAccountingStore, InMemoryUsageInbox, InMemoryGovernedCostSettlementStore } from \"@alma-harness/execution/testing\";\nimport { InMemoryExecutionResultStore } from \"@alma-harness/memory/testing\";\nimport type { Runtime, RuntimePolicies } from \"./index\";\nimport { policies } from \"./policies\";\n/** Ephemeral reference composition. Reconstructing it loses all data and execution history. */\nexport function createMemoryRuntime(value: RuntimePolicies): Runtime {\n const p = policies(value), settlements = new InMemoryGovernedCostSettlementStore(), trees = new InMemoryOperationTreeStore();\n return {\n stores: {\n sessions: new InMemorySessionStore(), admissions: new InMemoryOperationSessionStore(),\n rootResults: new InMemoryExecutionResultStore(p.rootResultPolicy),\n step: { executions: new InMemoryExecutionStore(), results: new InMemoryExecutionResultStore(p.stepResultPolicy), inbox: new InMemoryUsageInbox(),\n settlements, spend: settlements, trees, accounting: new InMemoryOperationAccountingStore(trees, settlements) },\n },\n async close() {},\n };\n}\n"],"mappings":";;;;;AAAA,SAAS,wBAAwB,4BAA4B;AAC7D,SAAS,+BAA+B,4BAA4B,kCAAkC,oBAAoB,2CAA2C;AACrK,SAAS,oCAAoC;AAItC,SAAS,oBAAoB,OAAiC;AACnE,QAAM,IAAI,SAAS,KAAK,GAAG,cAAc,IAAI,oCAAoC,GAAG,QAAQ,IAAI,2BAA2B;AAC3H,SAAO;AAAA,IACL,QAAQ;AAAA,MACN,UAAU,IAAI,qBAAqB;AAAA,MAAG,YAAY,IAAI,8BAA8B;AAAA,MACpF,aAAa,IAAI,6BAA6B,EAAE,gBAAgB;AAAA,MAChE,MAAM;AAAA,QAAE,YAAY,IAAI,uBAAuB;AAAA,QAAG,SAAS,IAAI,6BAA6B,EAAE,gBAAgB;AAAA,QAAG,OAAO,IAAI,mBAAmB;AAAA,QAC7I;AAAA,QAAa,OAAO;AAAA,QAAa;AAAA,QAAO,YAAY,IAAI,iCAAiC,OAAO,WAAW;AAAA,MAAE;AAAA,IACjH;AAAA,IACA,MAAM,QAAQ;AAAA,IAAC;AAAA,EACjB;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/memory.ts"],"sourcesContent":["import { InMemoryExecutionStore, InMemorySessionLabelStore, InMemorySessionStore, RecordingAuditLog } from \"@alma-harness/core/testing\";\nimport { InMemoryOperationSessionStore, InMemoryOperationTreeStore, InMemoryOperationAccountingStore, InMemoryUsageInbox, InMemoryGovernedCostSettlementStore } from \"@alma-harness/execution/testing\";\nimport { InMemoryExecutionResultStore } from \"@alma-harness/memory/testing\";\nimport type { Runtime, RuntimePolicies } from \"./index\";\nimport { policies } from \"./policies\";\n/** Ephemeral reference composition. Reconstructing it loses all data and execution history. */\nexport function createMemoryRuntime(value: RuntimePolicies): Runtime {\n const p = policies(value), settlements = new InMemoryGovernedCostSettlementStore(), trees = new InMemoryOperationTreeStore();\n return {\n stores: {\n sessions: new InMemorySessionStore(), admissions: new InMemoryOperationSessionStore(),\n rootResults: new InMemoryExecutionResultStore(p.rootResultPolicy),\n step: { executions: new InMemoryExecutionStore(), results: new InMemoryExecutionResultStore(p.stepResultPolicy), inbox: new InMemoryUsageInbox(),\n settlements, spend: settlements, trees, accounting: new InMemoryOperationAccountingStore(trees, settlements) },\n labels: new InMemorySessionLabelStore(), audit: new RecordingAuditLog(),\n },\n async close() {},\n };\n}\n"],"mappings":";;;;;AAAA,SAAS,wBAAwB,2BAA2B,sBAAsB,yBAAyB;AAC3G,SAAS,+BAA+B,4BAA4B,kCAAkC,oBAAoB,2CAA2C;AACrK,SAAS,oCAAoC;AAItC,SAAS,oBAAoB,OAAiC;AACnE,QAAM,IAAI,SAAS,KAAK,GAAG,cAAc,IAAI,oCAAoC,GAAG,QAAQ,IAAI,2BAA2B;AAC3H,SAAO;AAAA,IACL,QAAQ;AAAA,MACN,UAAU,IAAI,qBAAqB;AAAA,MAAG,YAAY,IAAI,8BAA8B;AAAA,MACpF,aAAa,IAAI,6BAA6B,EAAE,gBAAgB;AAAA,MAChE,MAAM;AAAA,QAAE,YAAY,IAAI,uBAAuB;AAAA,QAAG,SAAS,IAAI,6BAA6B,EAAE,gBAAgB;AAAA,QAAG,OAAO,IAAI,mBAAmB;AAAA,QAC7I;AAAA,QAAa,OAAO;AAAA,QAAa;AAAA,QAAO,YAAY,IAAI,iCAAiC,OAAO,WAAW;AAAA,MAAE;AAAA,MAC/G,QAAQ,IAAI,0BAA0B;AAAA,MAAG,OAAO,IAAI,kBAAkB;AAAA,IACxE;AAAA,IACA,MAAM,QAAQ;AAAA,IAAC;AAAA,EACjB;AACF;","names":[]}
package/dist/postgres.js CHANGED
@@ -1,104 +1,8 @@
1
1
  import {
2
- policies
3
- } from "./chunk-3OEFEOI4.js";
4
-
5
- // src/postgres.ts
6
- import pg from "pg";
7
- import {
8
- PostgresSessionStore,
9
- PostgresExecutionStore,
10
- PostgresExecutionResultStore,
11
- PostgresUsageInbox,
12
- PostgresCostSettlementStore,
13
- PostgresSpendStore,
14
- migrateSessionStore,
15
- migrateExecutionStore,
16
- migrateExecutionResultStore,
17
- migrateUsageInbox
18
- } from "@alma-harness/postgres";
19
- import { PostgresOperationSessionStore, PostgresOperationTreeStore, PostgresOperationAccountingStore, migrateOperationAccountingStore, migrateOperationSessionStore } from "@alma-harness/postgres-execution";
20
- function limits(value) {
21
- if (value === void 0) return { execution: 5, roots: 5 };
22
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("Invalid runtime pool limits");
23
- const fields = Object.getOwnPropertyDescriptors(value), keys = Reflect.ownKeys(fields);
24
- if (keys.length !== 2 || !keys.includes("execution") || !keys.includes("roots")) throw new TypeError("Invalid runtime pool limits");
25
- const execution = fields.execution?.value, roots = fields.roots?.value;
26
- if (!Number.isSafeInteger(execution) || execution < 1 || !Number.isSafeInteger(roots) || roots < 1 || !Number.isSafeInteger(execution + roots)) throw new TypeError("Invalid runtime pool limits");
27
- return { execution, roots };
28
- }
29
- function namespace(value) {
30
- const { connectionString, schema, rootSchema, onPoolError } = value;
31
- if (typeof onPoolError !== "function") throw new TypeError("Runtime requires onPoolError reporting");
32
- for (const s of [schema, rootSchema]) {
33
- if (typeof s !== "string" || !/^[a-z_][a-z0-9_]{0,62}$/.test(s) || s === "public" || s === "information_schema" || s.startsWith("pg_")) throw new TypeError("Invalid dedicated runtime schema");
34
- }
35
- if (schema === rootSchema) throw new TypeError("Runtime result namespaces must be distinct");
36
- let url;
37
- try {
38
- url = new URL(connectionString);
39
- } catch {
40
- throw new TypeError("Invalid PostgreSQL connection URL");
41
- }
42
- if (!["postgres:", "postgresql:"].includes(url.protocol) || !url.hostname || url.searchParams.has("options")) throw new TypeError("Runtime requires a PostgreSQL URL without startup options");
43
- return { connectionString, schema, rootSchema, onPoolError };
44
- }
45
- function pools(n, max) {
46
- const open = (schema, size) => {
47
- const pool = new pg.Pool({
48
- connectionString: n.connectionString,
49
- options: `-c search_path=${schema} -c extra_float_digits=3`,
50
- max: size,
51
- connectionTimeoutMillis: 5e3,
52
- idleTimeoutMillis: 1e4
53
- });
54
- pool.on("error", (error) => n.onPoolError(error, schema));
55
- return pool;
56
- };
57
- const execution = open(n.schema, max.execution), roots = open(n.rootSchema, max.roots);
58
- let closing;
59
- return { execution, roots, close() {
60
- return closing ??= Promise.allSettled([Promise.resolve().then(() => execution.end()), Promise.resolve().then(() => roots.end())]).then((results) => {
61
- const errors = results.flatMap((r) => r.status === "rejected" ? [r.reason] : []);
62
- if (errors.length) throw new AggregateError(errors, "Runtime pool cleanup failed");
63
- });
64
- } };
65
- }
66
- function createPostgresRuntime(value) {
67
- const n = namespace(value), p = policies(value), max = limits(value.poolMax), owned = pools(n, max);
68
- return { stores: {
69
- sessions: new PostgresSessionStore(owned.execution),
70
- admissions: new PostgresOperationSessionStore(owned.execution),
71
- rootResults: new PostgresExecutionResultStore(owned.roots, p.rootResultPolicy),
72
- step: {
73
- executions: new PostgresExecutionStore(owned.execution),
74
- results: new PostgresExecutionResultStore(owned.execution, p.stepResultPolicy),
75
- inbox: new PostgresUsageInbox(owned.execution),
76
- settlements: new PostgresCostSettlementStore(owned.execution),
77
- spend: new PostgresSpendStore(owned.execution),
78
- trees: new PostgresOperationTreeStore(owned.execution),
79
- accounting: new PostgresOperationAccountingStore(owned.execution)
80
- }
81
- }, close: owned.close };
82
- }
83
- async function migratePostgresRuntime(value) {
84
- const n = namespace(value), owned = pools(n, { execution: 1, roots: 1 });
85
- try {
86
- await owned.execution.query(`create schema if not exists ${n.schema}`);
87
- await owned.roots.query(`create schema if not exists ${n.rootSchema}`);
88
- await migrateSessionStore(owned.execution);
89
- await migrateExecutionStore(owned.execution);
90
- await migrateExecutionResultStore(owned.execution);
91
- await migrateUsageInbox(owned.execution);
92
- await migrateOperationAccountingStore(owned.execution);
93
- await migrateOperationSessionStore(owned.execution);
94
- await migrateExecutionResultStore(owned.roots);
95
- for (const [pool, schema] of [[owned.execution, n.schema], [owned.roots, n.rootSchema]]) {
96
- await pool.query(`grant usage on schema ${schema} to alma_app, alma_retention; revoke create on schema ${schema} from public, alma_app, alma_retention`);
97
- }
98
- } finally {
99
- await owned.close();
100
- }
101
- }
2
+ createPostgresRuntime,
3
+ migratePostgresRuntime
4
+ } from "./chunk-EGQL2P7U.js";
5
+ import "./chunk-II7NDZRL.js";
102
6
  export {
103
7
  createPostgresRuntime,
104
8
  migratePostgresRuntime
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/postgres.ts"],"sourcesContent":["import pg from \"pg\";\nimport { PostgresSessionStore, PostgresExecutionStore, PostgresExecutionResultStore, PostgresUsageInbox, PostgresCostSettlementStore, PostgresSpendStore,\n migrateSessionStore, migrateExecutionStore, migrateExecutionResultStore, migrateUsageInbox } from \"@alma-harness/postgres\";\nimport { PostgresOperationSessionStore, PostgresOperationTreeStore, PostgresOperationAccountingStore, migrateOperationAccountingStore, migrateOperationSessionStore } from \"@alma-harness/postgres-execution\";\nimport type { Runtime, RuntimePolicies } from \"./index\";\nimport { policies } from \"./policies\";\nexport interface PostgresRuntimeNamespace { connectionString: string; schema: string; rootSchema: string; onPoolError: (error: Error, schema: string) => void }\nexport type PostgresRuntimeOptions = PostgresRuntimeNamespace & RuntimePolicies & {\n /** Per owned pool, not per turn. Omission keeps five connections in each pool. */\n poolMax?: { execution: number; roots: number };\n};\nfunction limits(value: unknown): { execution: number; roots: number } {\n if(value===undefined)return {execution:5,roots:5};\n if(!value || typeof value!==\"object\" || Array.isArray(value))throw new TypeError(\"Invalid runtime pool limits\");\n const fields=Object.getOwnPropertyDescriptors(value),keys=Reflect.ownKeys(fields);\n if(keys.length!==2 || !keys.includes(\"execution\") || !keys.includes(\"roots\"))throw new TypeError(\"Invalid runtime pool limits\");\n const execution=fields.execution?.value,roots=fields.roots?.value;\n if(!Number.isSafeInteger(execution) || execution<1 || !Number.isSafeInteger(roots) || roots<1 || !Number.isSafeInteger(execution+roots))throw new TypeError(\"Invalid runtime pool limits\");\n return {execution,roots};\n}\nfunction namespace(value: PostgresRuntimeNamespace): PostgresRuntimeNamespace {\n const { connectionString, schema, rootSchema, onPoolError } = value;\n if (typeof onPoolError !== \"function\") throw new TypeError(\"Runtime requires onPoolError reporting\");\n for (const s of [schema, rootSchema]) {\n if (typeof s !== \"string\" || !/^[a-z_][a-z0-9_]{0,62}$/.test(s) || s === \"public\" || s === \"information_schema\" || s.startsWith(\"pg_\")) throw new TypeError(\"Invalid dedicated runtime schema\");\n }\n if (schema === rootSchema) throw new TypeError(\"Runtime result namespaces must be distinct\");\n let url: URL;\n try { url = new URL(connectionString); } catch { throw new TypeError(\"Invalid PostgreSQL connection URL\"); }\n if (![\"postgres:\", \"postgresql:\"].includes(url.protocol) || !url.hostname || url.searchParams.has(\"options\")) throw new TypeError(\"Runtime requires a PostgreSQL URL without startup options\");\n return { connectionString, schema, rootSchema, onPoolError };\n}\nfunction pools(n: PostgresRuntimeNamespace, max: { execution: number; roots: number }) {\n const open = (schema: string, size: number) => {\n const pool = new pg.Pool({ connectionString: n.connectionString,\n options: `-c search_path=${schema} -c extra_float_digits=3`, max: size, connectionTimeoutMillis: 5000, idleTimeoutMillis: 10000 });\n pool.on(\"error\", error => n.onPoolError(error, schema));\n return pool;\n };\n const execution = open(n.schema,max.execution), roots = open(n.rootSchema,max.roots);\n let closing: Promise<void> | undefined;\n return { execution, roots, close() {\n return closing ??= Promise.allSettled([Promise.resolve().then(() => execution.end()), Promise.resolve().then(() => roots.end())]).then(results => {\n const errors = results.flatMap(r => r.status === \"rejected\" ? [r.reason] : []);\n if (errors.length) throw new AggregateError(errors, \"Runtime pool cleanup failed\");\n });\n } };\n}\n/** No connections, migrations or grants occur at construction. Owned pools pin both namespaces. */\nexport function createPostgresRuntime(value: PostgresRuntimeOptions): Runtime {\n const n = namespace(value), p = policies(value), max = limits(value.poolMax), owned = pools(n,max);\n return { stores: {\n sessions: new PostgresSessionStore(owned.execution), admissions: new PostgresOperationSessionStore(owned.execution),\n rootResults: new PostgresExecutionResultStore(owned.roots, p.rootResultPolicy),\n step: { executions: new PostgresExecutionStore(owned.execution), results: new PostgresExecutionResultStore(owned.execution, p.stepResultPolicy),\n inbox: new PostgresUsageInbox(owned.execution), settlements: new PostgresCostSettlementStore(owned.execution), spend: new PostgresSpendStore(owned.execution),\n trees: new PostgresOperationTreeStore(owned.execution), accounting: new PostgresOperationAccountingStore(owned.execution) },\n }, close: owned.close };\n}\n/** Explicit administrator operation. Partial installation may be rerun; no login memberships are granted. */\nexport async function migratePostgresRuntime(value: PostgresRuntimeNamespace): Promise<void> {\n const n = namespace(value), owned = pools(n,{execution:1,roots:1});\n try {\n await owned.execution.query(`create schema if not exists ${n.schema}`);\n await owned.roots.query(`create schema if not exists ${n.rootSchema}`);\n await migrateSessionStore(owned.execution);\n await migrateExecutionStore(owned.execution);\n await migrateExecutionResultStore(owned.execution);\n await migrateUsageInbox(owned.execution);\n await migrateOperationAccountingStore(owned.execution);\n await migrateOperationSessionStore(owned.execution);\n await migrateExecutionResultStore(owned.roots);\n for (const [pool, schema] of [[owned.execution, n.schema], [owned.roots, n.rootSchema]] as const) {\n await pool.query(`grant usage on schema ${schema} to alma_app, alma_retention; revoke create on schema ${schema} from public, alma_app, alma_retention`);\n }\n } finally { await owned.close(); }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,100 @@
1
+ import { SessionLabels, Scope } from '@alma-harness/core';
2
+ import { GovernedCostReceipt } from '@alma-harness/execution';
3
+ import { RuntimeStores, RuntimePolicies } from './index.js';
4
+ import { S as ScopeClaim, L as Lap } from './maintenance-core-DDX5ryx7.js';
5
+ import pg from 'pg';
6
+ import '@alma-harness/conversation';
7
+
8
+ type Attribution = {
9
+ state: "available";
10
+ labels: SessionLabels;
11
+ } | {
12
+ state: "erased";
13
+ } | {
14
+ state: "none";
15
+ };
16
+ interface ProjectionDelivery {
17
+ scope: Scope;
18
+ consumer: string;
19
+ settlementId: string;
20
+ /** `receipt.receipt.settlement.sessionId` names the session. */
21
+ receipt: GovernedCostReceipt;
22
+ /** Erased is never none: never fall back to attribution kept earlier (spec: session-labels). */
23
+ attribution: Attribution;
24
+ /** Aborted when this drain loses its claim or its caller aborts. */
25
+ signal: AbortSignal;
26
+ }
27
+ interface ProjectionItem {
28
+ at: string;
29
+ settlementId: string;
30
+ }
31
+ /** Cross-scope metadata only: pending receipt identities and times, and the projection cursors (spec: receipt-projections). */
32
+ interface ProjectionDiscovery {
33
+ verify(): Promise<void>;
34
+ nextScopes(consumer: string, limit: number): Promise<Scope[]>;
35
+ claim(scope: Scope): Promise<ScopeClaim | null>;
36
+ lap(consumer: string, scope: Scope): Promise<{
37
+ bound: ProjectionItem;
38
+ budget: number;
39
+ } | null>;
40
+ items(consumer: string, scope: Scope, after: ProjectionItem | null, bound: ProjectionItem, limit: number): Promise<ProjectionItem[]>;
41
+ cursor(consumer: string, scope: Scope): Promise<Lap<ProjectionItem> | null>;
42
+ saveCursor(consumer: string, scope: Scope, lap: Lap<ProjectionItem> | null): Promise<void>;
43
+ quiesce<T>(scope: Scope, fn: () => Promise<T>, timeoutMs: number): Promise<T>;
44
+ }
45
+ interface ProjectionReport {
46
+ scopes: number;
47
+ skipped: number;
48
+ delivered: number;
49
+ acknowledged: number;
50
+ failed: number;
51
+ }
52
+ /** An erasure that reported `complete: false` inside `quiesce`: nothing is released as done; retry the erasure. */
53
+ declare class IncompleteErasureError extends Error {
54
+ constructor();
55
+ }
56
+ /** At-least-once delivery: acknowledged only after the handler resolved. Counts only in the report. */
57
+ declare function createProjections(options: {
58
+ stores: Pick<RuntimeStores, "labels"> & {
59
+ step: Pick<RuntimeStores["step"], "settlements">;
60
+ };
61
+ discovery: ProjectionDiscovery;
62
+ }): {
63
+ drain(consumerName: string, handler: (d: ProjectionDelivery) => Promise<void>, value: {
64
+ limit: number;
65
+ signal?: AbortSignal;
66
+ }): Promise<ProjectionReport>;
67
+ /** Waits for any drain of the scope, holds new ones off, and runs `fn`. An erasure reporting `complete: false` rejects. */
68
+ quiesce<T>(scopeValue: Scope, fn: () => Promise<T>, value: {
69
+ timeoutMs: number;
70
+ }): Promise<T>;
71
+ };
72
+
73
+ interface PostgresProjectionsOptions extends RuntimePolicies {
74
+ /** A login that is a member of alma_projection only. */
75
+ discovery: {
76
+ connectionString: string;
77
+ };
78
+ /** A login that is a member of alma_app only, never the request path's. */
79
+ worker: {
80
+ connectionString: string;
81
+ };
82
+ schema: string;
83
+ rootSchema: string;
84
+ onPoolError: (error: Error, schema: string) => void;
85
+ }
86
+ /** The PostgreSQL projection discovery on a pool of the discovery login; `workerURL` is only used to verify the worker login. */
87
+ declare function createPostgresProjectionDiscovery(pool: pg.Pool, workerURL: string): ProjectionDiscovery;
88
+ /** Its own logins and pools, never the request path's. Construction opens no connection (spec: receipt-projections). */
89
+ declare function createPostgresProjections(value: PostgresProjectionsOptions): {
90
+ drain: (consumerName: string, handler: (d: ProjectionDelivery) => Promise<void>, value: {
91
+ limit: number;
92
+ signal?: AbortSignal;
93
+ }) => Promise<ProjectionReport>;
94
+ quiesce: <T>(scopeValue: Scope, fn: () => Promise<T>, value: {
95
+ timeoutMs: number;
96
+ }) => Promise<T>;
97
+ close(): Promise<void>;
98
+ };
99
+
100
+ export { type Attribution, IncompleteErasureError, type PostgresProjectionsOptions, type ProjectionDelivery, type ProjectionDiscovery, type ProjectionItem, type ProjectionReport, createPostgresProjectionDiscovery, createPostgresProjections, createProjections };
@@ -0,0 +1,203 @@
1
+ import {
2
+ resumeLap,
3
+ scopeRotation
4
+ } from "./chunk-6MFSZ4VS.js";
5
+ import {
6
+ PROJECTION_ROLE,
7
+ createPostgresRuntime
8
+ } from "./chunk-EGQL2P7U.js";
9
+ import {
10
+ namespace
11
+ } from "./chunk-II7NDZRL.js";
12
+
13
+ // src/projections.ts
14
+ import pg from "pg";
15
+
16
+ // src/projections-core.ts
17
+ import { settlementIdentifier, settlementScope } from "@alma-harness/core";
18
+ var IncompleteErasureError = class extends Error {
19
+ constructor() {
20
+ super("Erasure reported incomplete inside quiesce");
21
+ this.name = "IncompleteErasureError";
22
+ }
23
+ };
24
+ var BUDGET = 50;
25
+ function createProjections(options) {
26
+ const { stores, discovery } = options, settlements = stores.step.settlements;
27
+ async function attribution(scope, sessionId) {
28
+ const record = await stores.labels.get(scope, sessionId);
29
+ return !record ? { state: "none" } : record.state === "erased" ? { state: "erased" } : { state: "available", labels: structuredClone(record.labels) };
30
+ }
31
+ async function work(consumer, scope, handler, signal, r) {
32
+ const stored = await discovery.cursor(consumer, scope);
33
+ const l = await resumeLap(stored, () => discovery.lap(consumer, scope));
34
+ if (!l) {
35
+ await discovery.saveCursor(consumer, scope, null);
36
+ return;
37
+ }
38
+ const want = Math.min(BUDGET, l.budget), items = await discovery.items(consumer, scope, l.after, l.bound, want);
39
+ let attempted = 0;
40
+ for (const item of items) {
41
+ if (signal.aborted) break;
42
+ try {
43
+ const receipt = await settlements.getGoverned(scope, item.settlementId);
44
+ if (!receipt) throw new Error("Pending receipt missing");
45
+ const delivery = Object.freeze({
46
+ scope: { ...scope },
47
+ consumer,
48
+ settlementId: item.settlementId,
49
+ receipt,
50
+ attribution: Object.freeze(await attribution(scope, receipt.receipt.settlement.sessionId)),
51
+ signal
52
+ });
53
+ if (signal.aborted) break;
54
+ await handler(delivery);
55
+ r.delivered++;
56
+ if (signal.aborted) break;
57
+ if (await settlements.acknowledge(scope, consumer, item.settlementId)) r.acknowledged++;
58
+ } catch {
59
+ r.failed++;
60
+ }
61
+ l.after = item;
62
+ l.budget--;
63
+ attempted++;
64
+ }
65
+ await discovery.saveCursor(consumer, scope, attempted < items.length ? l : items.length < want || l.budget < 1 ? null : l);
66
+ }
67
+ let queue = Promise.resolve();
68
+ const serial = (fn) => {
69
+ const run = queue.then(fn, fn);
70
+ queue = run.catch(() => void 0);
71
+ return run;
72
+ };
73
+ return {
74
+ drain(consumerName, handler, value) {
75
+ return serial(async () => {
76
+ const consumer = settlementIdentifier(consumerName);
77
+ if (typeof handler !== "function" || !value || !Number.isSafeInteger(value.limit) || value.limit < 1 || value.limit > 1e3) throw new TypeError("Invalid projection drain");
78
+ await discovery.verify();
79
+ const r = { scopes: 0, skipped: 0, delivered: 0, acknowledged: 0, failed: 0 };
80
+ let lost = false;
81
+ for (const scope of await discovery.nextScopes(consumer, value.limit)) {
82
+ if (value.signal?.aborted || lost) break;
83
+ const claim = await discovery.claim(scope);
84
+ if (!claim) {
85
+ r.skipped++;
86
+ continue;
87
+ }
88
+ r.scopes++;
89
+ const controller = new AbortController(), abort = () => controller.abort();
90
+ value.signal?.addEventListener("abort", abort, { once: true });
91
+ claim.onLost?.(() => {
92
+ lost = true;
93
+ abort();
94
+ });
95
+ if (value.signal?.aborted || !claim.alive()) abort();
96
+ try {
97
+ await work(consumer, scope, handler, controller.signal, r);
98
+ } catch {
99
+ r.failed++;
100
+ } finally {
101
+ value.signal?.removeEventListener("abort", abort);
102
+ await claim.release();
103
+ }
104
+ }
105
+ return r;
106
+ });
107
+ },
108
+ /** Waits for any drain of the scope, holds new ones off, and runs `fn`. An erasure reporting `complete: false` rejects. */
109
+ quiesce(scopeValue, fn, value) {
110
+ return serial(async () => {
111
+ const scope = settlementScope(scopeValue);
112
+ if (typeof fn !== "function" || !value || !Number.isSafeInteger(value.timeoutMs) || value.timeoutMs < 1 || value.timeoutMs > 6e5) throw new TypeError("Invalid quiesce");
113
+ await discovery.verify();
114
+ const result = await discovery.quiesce(scope, fn, value.timeoutMs);
115
+ if (result && typeof result === "object" && result.complete === false) throw new IncompleteErasureError();
116
+ return result;
117
+ });
118
+ }
119
+ };
120
+ }
121
+
122
+ // src/projections.ts
123
+ var PENDING = `from alma_cost_projection_pending p join alma_audit_cost c on c.org=p.org and c.uid=p.uid and c.id=p.cost_id
124
+ where p.consumer=$1 and c.settlement_governed`;
125
+ var KEY = `(c.at, c.settlement_id collate "C")`;
126
+ var json = (v) => v === null || v === void 0 ? null : JSON.stringify(v);
127
+ function createPostgresProjectionDiscovery(pool, workerURL) {
128
+ const rotation = scopeRotation(pool, { role: PROJECTION_ROLE, lockPrefix: "alma-projection", workerURL }), tx = rotation.tx;
129
+ const due = `select distinct p.org collate "C" as org, p.uid collate "C" as uid ${PENDING}`;
130
+ return {
131
+ verify: rotation.verify,
132
+ claim: rotation.claim,
133
+ quiesce: (scope, fn, timeoutMs) => rotation.quiesce(scope, fn, timeoutMs),
134
+ nextScopes(consumer, limit) {
135
+ return tx(async (c) => {
136
+ await c.query("insert into alma_projection_cursors (consumer) values ($1) on conflict do nothing", [consumer]);
137
+ const cur = (await c.query("select org, uid from alma_projection_cursors where consumer=$1 for update", [consumer])).rows[0];
138
+ const page = (where, params) => c.query(`select org, uid from (${due}) d where ${where} order by org collate "C", uid collate "C" limit $${params.length + 1}`, [consumer, ...params]);
139
+ let rows = (await page(`$2::text is null or (org, uid) > ($2::text collate "C", $3::text collate "C")`, [cur.org, cur.uid, limit])).rows;
140
+ if (rows.length < limit && cur.org !== null)
141
+ rows = rows.concat((await page(`(org, uid) <= ($2::text collate "C", $3::text collate "C")`, [cur.org, cur.uid, limit - rows.length])).rows);
142
+ const last = rows.at(-1);
143
+ if (last) await c.query("update alma_projection_cursors set org=$2, uid=$3, updated_at=clock_timestamp() where consumer=$1", [consumer, last.org, last.uid]);
144
+ return rows.map((r) => ({ org: r.org, uid: r.uid }));
145
+ });
146
+ },
147
+ lap(consumer, scope) {
148
+ return tx(async (c) => {
149
+ const r = (await c.query(`select c.at::text as at, c.settlement_id, count(*) over ()::int as n ${PENDING}
150
+ and p.org=$2 and p.uid=$3 order by c.at desc, c.settlement_id collate "C" desc limit 1`, [consumer, scope.org, scope.uid])).rows[0];
151
+ return r ? { bound: { at: r.at, settlementId: r.settlement_id }, budget: r.n } : null;
152
+ });
153
+ },
154
+ items(consumer, scope, after, bound, limit) {
155
+ return tx(async (c) => (await c.query(`select c.at::text as at, c.settlement_id ${PENDING} and p.org=$2 and p.uid=$3
156
+ and ($4::timestamptz is null or ${KEY} > ($4::timestamptz, $5::text collate "C")) and ${KEY} <= ($6::timestamptz, $7::text collate "C")
157
+ order by c.at, c.settlement_id collate "C" limit $8`, [consumer, scope.org, scope.uid, after?.at ?? null, after?.settlementId ?? null, bound.at, bound.settlementId, limit])).rows.map((r) => ({ at: r.at, settlementId: r.settlement_id })));
158
+ },
159
+ cursor(consumer, scope) {
160
+ return tx(async (c) => {
161
+ const row = (await c.query(
162
+ "select after, bound, budget from alma_projection_scopes where consumer=$1 and org=$2 and uid=$3",
163
+ [consumer, scope.org, scope.uid]
164
+ )).rows[0];
165
+ return row && (row.after || row.bound) ? { after: row.after, bound: row.bound, budget: row.budget } : null;
166
+ });
167
+ },
168
+ async saveCursor(consumer, scope, l) {
169
+ await tx((c) => c.query(
170
+ `insert into alma_projection_scopes (consumer, org, uid, after, bound, budget) values ($1, $2, $3, $4::jsonb, $5::jsonb, $6)
171
+ on conflict (consumer, org, uid) do update set after=excluded.after, bound=excluded.bound, budget=excluded.budget, updated_at=clock_timestamp()`,
172
+ [consumer, scope.org, scope.uid, json(l?.after), json(l?.bound), l?.budget ?? null]
173
+ ));
174
+ }
175
+ };
176
+ }
177
+ function createPostgresProjections(value) {
178
+ if (!value || typeof value !== "object" || !value.discovery || !value.worker) throw new TypeError("Projections require discovery and worker logins");
179
+ const base = { schema: value.schema, rootSchema: value.rootSchema, onPoolError: value.onPoolError };
180
+ const d = namespace({ ...base, connectionString: value.discovery.connectionString }), workerURL = namespace({ ...base, connectionString: value.worker.connectionString }).connectionString;
181
+ const runtime = createPostgresRuntime({ ...base, connectionString: workerURL, stepResultPolicy: value.stepResultPolicy, rootResultPolicy: value.rootResultPolicy, poolMax: { execution: 2, roots: 1 } });
182
+ const pool = new pg.Pool({ connectionString: d.connectionString, options: `-c search_path=${d.schema}`, max: 2, connectionTimeoutMillis: 5e3, idleTimeoutMillis: 1e4 });
183
+ pool.on("error", (error) => d.onPoolError(error, d.schema));
184
+ const projections = createProjections({ stores: runtime.stores, discovery: createPostgresProjectionDiscovery(pool, workerURL) });
185
+ let closing;
186
+ return {
187
+ drain: projections.drain,
188
+ quiesce: projections.quiesce,
189
+ close() {
190
+ return closing ??= Promise.allSettled([runtime.close(), pool.end()]).then((results) => {
191
+ const errors = results.flatMap((r) => r.status === "rejected" ? [r.reason] : []);
192
+ if (errors.length) throw new AggregateError(errors, "Projection pool cleanup failed");
193
+ });
194
+ }
195
+ };
196
+ }
197
+ export {
198
+ IncompleteErasureError,
199
+ createPostgresProjectionDiscovery,
200
+ createPostgresProjections,
201
+ createProjections
202
+ };
203
+ //# sourceMappingURL=projections.js.map