Skip to content

Commit b9b5b5e

Browse files
committed
feat: upgrade all consumers to new ModuleLoader API
- job/compute-service: use ModuleLoader instead of ComputeModuleLoader - job/compute-worker: scope derivation uses entity_type || null - job/worker: graph-complete, compute-meter, storage-meter use ModuleLoader - packages/agentic-server: already using new API (verified) - packages/functions-test: worker helper uses new ModuleLoader - packages/module-loader: graph loader queries graph_execution_module, adds nodeStatesTable, removes all hardcoded defaults - Remove backward-compat shims (UsageLoader, USAGE_DEFAULTS, KNOWN_TABLES)
1 parent 6081789 commit b9b5b5e

28 files changed

Lines changed: 499 additions & 309 deletions

File tree

.github/workflows/test-module-loader.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,7 @@ jobs:
5454
run: pgpm admin-users bootstrap --yes
5555

5656
- name: Install pgpm module dependencies
57-
run: |
58-
cd extensions/@pgpm/metaschema-modules && npm install --legacy-peer-deps && cd ../../..
57+
run: cd extensions/@pgpm/metaschema-modules && pgpm install @pgpm/services
5958

6059
- name: Generate function packages
6160
run: node --experimental-strip-types scripts/generate.ts
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
EXTENSION = metaschema-modules
2-
DATA = sql/metaschema-modules--0.26.5.sql
2+
DATA = sql/metaschema-modules--0.28.0.sql
33

44
PG_CONFIG = pg_config
55
PGXS := $(shell $(PG_CONFIG) --pgxs)
66
include $(PGXS)
7+
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# metaschema-modules extension
22
comment = 'metaschema-modules extension'
3-
default_version = '0.26.5'
3+
default_version = '0.28.0'
44
module_pathname = '$libdir/metaschema-modules'
5-
requires = 'plpgsql,uuid-ossp,metaschema-schema,services,pgpm-verify'
5+
requires = 'metaschema-schema,pgpm-database-jobs,pgpm-inflection,pgpm-jwt-claims,pgpm-types,pgpm-verify,plpgsql,services,uuid-ossp'
66
relocatable = false
77
superuser = false

extensions/@pgpm/metaschema-modules/package.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,6 @@
2020
"test": "jest",
2121
"test:watch": "jest --watch"
2222
},
23-
"dependencies": {
24-
"@pgpm/metaschema-schema": "0.28.0",
25-
"@pgpm/services": "file:../services",
26-
"@pgpm/verify": "0.28.0"
27-
},
28-
"devDependencies": {
29-
"pgpm": "^4.23.2"
30-
},
3123
"repository": {
3224
"type": "git",
3325
"url": "https://github.com/constructive-io/pgpm-modules"
@@ -36,5 +28,13 @@
3628
"bugs": {
3729
"url": "https://github.com/constructive-io/pgpm-modules/issues"
3830
},
39-
"gitHead": "d2ab7ca810ded086eb742eb8f0ca362b6212b97e"
40-
}
31+
"gitHead": "d2ab7ca810ded086eb742eb8f0ca362b6212b97e",
32+
"dependencies": {
33+
"@pgpm/metaschema-schema": "0.28.0",
34+
"@pgpm/services": "0.28.0",
35+
"@pgpm/verify": "0.28.0"
36+
},
37+
"devDependencies": {
38+
"pgpm": "^4.23.2"
39+
}
40+
}

job/compute-service/src/index.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
* 4. A Scheduler for cron-like scheduled jobs
1313
*/
1414

15-
import ComputeWorker, { ComputeModuleLoader } from '@constructive-io/compute-worker';
15+
import ComputeWorker, { ModuleLoader } from '@constructive-io/compute-worker';
1616
import poolManager from '@constructive-io/job-pg';
1717
import Scheduler from '@constructive-io/job-scheduler';
1818
import {
@@ -399,17 +399,9 @@ export const waitForComputePrereqs = async (): Promise<void> => {
399399
database: cfg.database,
400400
max: 1,
401401
});
402-
const loader = new ComputeModuleLoader(pool, 0);
403-
const config = await loader.load(databaseId);
404-
405-
if (config.functionModule) {
406-
const { publicSchema, definitionsTable } = config.functionModule;
407-
await client.query(`SELECT count(*) FROM "${publicSchema}"."${definitionsTable}" LIMIT 1`);
408-
} else {
409-
// Metaschema not populated — check the compute schema directly
410-
log.info('function_module not in metaschema, checking constructive_compute_public directly');
411-
await client.query('SELECT count(*) FROM constructive_compute_public.platform_function_definitions LIMIT 1');
412-
}
402+
const loader = new ModuleLoader({ pool, ttlMs: 0 });
403+
const fnConfig = await loader.function.load(databaseId, null);
404+
await client.query(`SELECT count(*) FROM "${fnConfig.publicSchema}"."${fnConfig.definitionsTable}" LIMIT 1`);
413405

414406
log.info('compute prereqs satisfied (jobs table + compute module present)');
415407
} catch (error) {

job/compute-worker/src/billing.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,56 @@
11
/**
2-
* BillingTracker — re-exports BillingLoader from @constructive-io/module-loader
3-
* under the legacy name `BillingTracker`.
2+
* BillingTracker — quota checks and usage recording via billing_module.
3+
*
4+
* Resolves billing config dynamically via ModuleLoader. Gracefully no-ops
5+
* when billing is not provisioned (standalone dev mode).
46
*/
57

6-
import { BillingLoader } from '@constructive-io/module-loader';
8+
import type { BillingModuleConfig } from '@constructive-io/module-loader';
9+
import { ModuleLoader, ModuleNotProvisionedError } from '@constructive-io/module-loader';
710
import type { Pool } from 'pg';
811

9-
export class BillingTracker extends BillingLoader {
10-
constructor(pool: Pool, databaseId: string, cacheTtlMs?: number) {
11-
super(pool, databaseId, cacheTtlMs);
12+
export class BillingTracker {
13+
private loader: ModuleLoader;
14+
private pool: Pool;
15+
private databaseId: string;
16+
17+
constructor(pool: Pool, databaseId: string) {
18+
this.pool = pool;
19+
this.databaseId = databaseId;
20+
this.loader = new ModuleLoader({ pool });
21+
}
22+
23+
async load(databaseId?: string): Promise<BillingModuleConfig | null> {
24+
try {
25+
return await this.loader.billing.load(databaseId ?? this.databaseId, null);
26+
} catch (err) {
27+
if (err instanceof ModuleNotProvisionedError) return null;
28+
return null;
29+
}
30+
}
31+
32+
async checkQuota(entityId: string, meterSlug: string, amount = 1, databaseId?: string): Promise<boolean> {
33+
const config = await this.load(databaseId);
34+
if (!config) return true;
35+
try {
36+
const sql = `SELECT "${config.privateSchema}"."check_billing_quota"($1, $2::uuid, $3) AS allowed`;
37+
const { rows } = await this.pool.query(sql, [meterSlug, entityId, amount]);
38+
return rows[0]?.allowed !== false;
39+
} catch {
40+
return true;
41+
}
42+
}
43+
44+
async recordUsage(entityId: string, meterSlug: string, amount: number, metadata: Record<string, unknown>, databaseId?: string): Promise<void> {
45+
const config = await this.load(databaseId);
46+
if (!config) return;
47+
try {
48+
const sql = `SELECT "${config.privateSchema}"."${config.recordUsageFunction}"($1, $2::uuid, $3, $4::jsonb)`;
49+
await this.pool.query(sql, [meterSlug, entityId, amount, JSON.stringify(metadata)]);
50+
} catch { /* non-fatal */ }
51+
}
52+
53+
invalidate(databaseId?: string): void {
54+
this.loader.billing.invalidate(databaseId);
1255
}
1356
}

job/compute-worker/src/cache.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,23 @@
11
/**
2-
* Re-export TtlCache from @constructive-io/module-loader.
2+
* Simple TTL cache for compute-worker internal use.
33
*/
4-
export { TtlCache } from '@constructive-io/module-loader';
4+
interface CacheEntry<T> { value: T; expiresAt: number; }
5+
6+
export class TtlCache<T> {
7+
private store = new Map<string, CacheEntry<T>>();
8+
constructor(private ttlMs: number) {}
9+
10+
get(key: string): T | undefined {
11+
const e = this.store.get(key);
12+
if (!e) return undefined;
13+
if (Date.now() > e.expiresAt) { this.store.delete(key); return undefined; }
14+
return e.value;
15+
}
16+
17+
set(key: string, value: T): void {
18+
this.store.set(key, { value, expiresAt: Date.now() + this.ttlMs });
19+
}
20+
21+
delete(key: string): void { this.store.delete(key); }
22+
clear(): void { this.store.clear(); }
23+
}

job/compute-worker/src/compute-log.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/**
2-
* ComputeLogTracker — writes to the platform_compute_log table
2+
* ComputeLogTracker — writes to the compute_log table
33
* after every job dispatch (success or failure) and triggers
44
* the usage_daily rollup.
55
*
6-
* Gracefully no-ops if compute_log_module is not registered.
6+
* Gracefully no-ops if compute_log_module is not provisioned.
77
*/
88

9-
import type { ComputeModuleLoader } from '@constructive-io/module-loader';
9+
import { ModuleLoader, ModuleNotProvisionedError } from '@constructive-io/module-loader';
1010
import { Logger } from '@pgpmjs/logger';
1111
import type { Pool } from 'pg';
1212

@@ -28,25 +28,28 @@ export interface ComputeLogEntry {
2828

2929
export class ComputeLogTracker {
3030
private pool: Pool;
31-
private loader: ComputeModuleLoader;
31+
private loader: ModuleLoader;
3232
private databaseId: string;
3333

34-
constructor(pool: Pool, loader: ComputeModuleLoader, databaseId: string) {
34+
constructor(pool: Pool, loader: ModuleLoader, databaseId: string) {
3535
this.pool = pool;
3636
this.loader = loader;
3737
this.databaseId = databaseId;
3838
}
3939

4040
async log(entry: ComputeLogEntry): Promise<void> {
41-
const config = await this.loader.load(this.databaseId);
42-
if (!config.computeLogModule) {
43-
log.debug('compute_log_module not provisioned — skipping log');
44-
return;
41+
let cfg;
42+
try {
43+
cfg = await this.loader.computeLog.load(this.databaseId, null);
44+
} catch (err) {
45+
if (err instanceof ModuleNotProvisionedError) {
46+
log.debug('compute_log_module not provisioned — skipping log');
47+
return;
48+
}
49+
throw err;
4550
}
4651

47-
const { publicSchema, computeLogTable } = config.computeLogModule;
48-
const qualifiedTable = `"${publicSchema}"."${computeLogTable}"`;
49-
52+
const qualifiedTable = `"${cfg.publicSchema}"."${cfg.computeLogTable}"`;
5053
try {
5154
await this.pool.query(
5255
`INSERT INTO ${qualifiedTable}
@@ -75,16 +78,20 @@ export class ComputeLogTracker {
7578
}
7679

7780
async rollup(since?: Date): Promise<number> {
78-
const config = await this.loader.load(this.databaseId);
79-
if (!config.computeLogModule) {
80-
log.debug('compute_log_module not provisioned — skipping rollup');
81-
return 0;
81+
let cfg;
82+
try {
83+
cfg = await this.loader.computeLog.load(this.databaseId, null);
84+
} catch (err) {
85+
if (err instanceof ModuleNotProvisionedError) {
86+
log.debug('compute_log_module not provisioned — skipping rollup');
87+
return 0;
88+
}
89+
throw err;
8290
}
8391

84-
const { privateSchema } = config.computeLogModule;
8592
try {
8693
const result = await this.pool.query(
87-
`SELECT count(*) AS n FROM "${privateSchema}".rollup_compute_daily($1)`,
94+
`SELECT count(*) AS n FROM "${cfg.privateSchema}".rollup_compute_daily($1)`,
8895
[since || new Date(Date.now() - 2 * 24 * 60 * 60 * 1000)]
8996
);
9097
const n = parseInt(result.rows[0]?.n ?? '0', 10);

job/compute-worker/src/discovery.ts

Lines changed: 18 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,14 @@
22
* FunctionDiscovery — lazy, cached lookups against the
33
* platform function definitions table.
44
*
5-
* Schema and table names are resolved dynamically via ComputeModuleLoader
6-
* instead of hardcoding a specific schema. When a job arrives the worker
7-
* calls `resolve(taskIdentifier)`. On cache miss, a single SQL query
8-
* fetches the function definition and caches it for `ttlMs` (default 60 s).
5+
* Schema and table names are resolved dynamically via ModuleLoader.
96
*/
107

11-
import { TtlCache } from '@constructive-io/module-loader';
12-
import type { ComputeModuleLoader } from '@constructive-io/module-loader';
8+
import { ModuleLoader } from '@constructive-io/module-loader';
139
import { Logger } from '@pgpmjs/logger';
1410
import type { Pool } from 'pg';
1511

12+
import { TtlCache } from './cache';
1613
import type { PlatformFunctionDefinition } from './types';
1714

1815
const log = new Logger('compute:discovery');
@@ -27,75 +24,47 @@ const COLUMNS = `
2724
export class FunctionDiscovery {
2825
private cache: TtlCache<PlatformFunctionDefinition | null>;
2926
private pool: Pool;
30-
private loader: ComputeModuleLoader;
27+
private loader: ModuleLoader;
3128
private databaseId: string;
3229

33-
constructor(pool: Pool, loader: ComputeModuleLoader, databaseId: string, ttlMs = 60_000) {
30+
constructor(pool: Pool, loader: ModuleLoader, databaseId: string, ttlMs = 60_000) {
3431
this.pool = pool;
3532
this.loader = loader;
3633
this.databaseId = databaseId;
3734
this.cache = new TtlCache<PlatformFunctionDefinition | null>(ttlMs);
3835
}
3936

40-
/**
41-
* Lazily resolve a function definition by task_identifier.
42-
* Returns null if not registered. Results are TTL-cached.
43-
*/
4437
async resolve(taskIdentifier: string): Promise<PlatformFunctionDefinition | null> {
4538
const cached = this.cache.get(taskIdentifier);
46-
if (cached !== undefined) {
47-
log.debug(`cache hit: ${taskIdentifier}`);
48-
return cached;
49-
}
39+
if (cached !== undefined) return cached;
5040

51-
log.debug(`cache miss: ${taskIdentifier}, querying DB`);
5241
try {
53-
const config = await this.loader.load(this.databaseId);
54-
const publicSchema = config.functionModule?.publicSchema ?? 'constructive_compute_public';
55-
const definitionsTable = config.functionModule?.definitionsTable ?? 'platform_function_definitions';
56-
const sql = `SELECT ${COLUMNS} FROM "${publicSchema}"."${definitionsTable}" WHERE task_identifier = $1 LIMIT 1`;
57-
42+
const cfg = await this.loader.function.load(this.databaseId, null);
43+
const sql = `SELECT ${COLUMNS} FROM "${cfg.publicSchema}"."${cfg.definitionsTable}" WHERE task_identifier = $1 LIMIT 1`;
5844
const { rows } = await this.pool.query(sql, [taskIdentifier]);
5945
const def = (rows[0] as PlatformFunctionDefinition) ?? null;
6046
this.cache.set(taskIdentifier, def);
61-
if (def) {
62-
log.info(`resolved function: ${def.name} (${taskIdentifier}) → ${def.service_url ?? 'no url'}`);
63-
} else {
64-
log.warn(`no function registered for task_identifier="${taskIdentifier}"`);
65-
}
6647
return def;
67-
} catch (err: any) {
68-
log.error(`failed to resolve "${taskIdentifier}": ${err.message}`);
48+
} catch (err: unknown) {
49+
const msg = err instanceof Error ? err.message : String(err);
50+
log.error(`failed to resolve "${taskIdentifier}": ${msg}`);
6951
return null;
7052
}
7153
}
7254

73-
/**
74-
* List all invocable function definitions.
75-
* Not cached — intended for startup diagnostics / admin endpoints.
76-
*/
7755
async listInvocable(): Promise<PlatformFunctionDefinition[]> {
7856
try {
79-
const config = await this.loader.load(this.databaseId);
80-
const publicSchema = config.functionModule?.publicSchema ?? 'constructive_compute_public';
81-
const definitionsTable = config.functionModule?.definitionsTable ?? 'platform_function_definitions';
82-
const sql = `SELECT ${COLUMNS} FROM "${publicSchema}"."${definitionsTable}" WHERE is_invocable = true ORDER BY name`;
83-
57+
const cfg = await this.loader.function.load(this.databaseId, null);
58+
const sql = `SELECT ${COLUMNS} FROM "${cfg.publicSchema}"."${cfg.definitionsTable}" WHERE is_invocable = true ORDER BY name`;
8459
const { rows } = await this.pool.query(sql);
8560
return rows as PlatformFunctionDefinition[];
86-
} catch (err: any) {
87-
log.error(`failed to list invocable functions: ${err.message}`);
61+
} catch (err: unknown) {
62+
const msg = err instanceof Error ? err.message : String(err);
63+
log.error(`failed to list invocable functions: ${msg}`);
8864
return [];
8965
}
9066
}
9167

92-
/** Invalidate cached entry for a specific task. */
93-
invalidate(taskIdentifier: string): void {
94-
this.cache.delete(taskIdentifier);
95-
}
96-
97-
/** Clear the entire cache. */
98-
invalidateAll(): void {
99-
this.cache.clear();
100-
}
68+
invalidate(taskIdentifier: string): void { this.cache.delete(taskIdentifier); }
69+
invalidateAll(): void { this.cache.clear(); }
10170
}

0 commit comments

Comments
 (0)