@nylorun/runtime 0.13.0-beta → 0.14.0-beta
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/CHANGELOG.md +330 -0
- package/README.md +69 -15
- package/dist/adapters/execution/restate.d.ts +7 -2
- package/dist/adapters/execution/restate.js +38 -8
- package/dist/adapters/replication/pgoutput.d.ts +16 -0
- package/dist/adapters/replication/pgoutput.js +169 -0
- package/dist/adapters/streams/s2.js +37 -20
- package/dist/admin-openapi.json +1071 -0
- package/dist/api/a2a/card.d.ts +15 -0
- package/dist/api/a2a/card.js +24 -0
- package/dist/api/a2a/endpoint.d.ts +8 -0
- package/dist/api/a2a/endpoint.js +81 -0
- package/dist/api/a2a/protocol.d.ts +144 -0
- package/dist/api/a2a/protocol.js +188 -0
- package/dist/api/a2a/routes.d.ts +19 -0
- package/dist/api/a2a/routes.js +289 -0
- package/dist/api/a2a/tasks.d.ts +50 -0
- package/dist/api/a2a/tasks.js +186 -0
- package/dist/api/ag-ui/endpoint.d.ts +8 -0
- package/dist/api/ag-ui/endpoint.js +126 -0
- package/dist/api/ag-ui/history.d.ts +7 -0
- package/dist/api/ag-ui/history.js +60 -0
- package/dist/api/ag-ui/routes.d.ts +32 -0
- package/dist/api/ag-ui/routes.js +305 -0
- package/dist/api/ag-ui/session-id.d.ts +1 -0
- package/dist/api/ag-ui/session-id.js +12 -0
- package/dist/api/ag-ui/sse.d.ts +9 -0
- package/dist/api/ag-ui/sse.js +7 -0
- package/dist/api/ag-ui/translate.d.ts +38 -0
- package/dist/api/ag-ui/translate.js +222 -0
- package/dist/api/components.d.ts +7825 -0
- package/dist/api/components.js +83 -0
- package/dist/api/http/app.d.ts +18 -0
- package/dist/api/http/app.js +58 -0
- package/dist/api/http/body.d.ts +4 -0
- package/dist/api/http/body.js +37 -0
- package/dist/api/http/define.d.ts +39 -0
- package/dist/api/http/define.js +130 -0
- package/dist/api/http/respond.d.ts +9 -0
- package/dist/api/http/respond.js +44 -0
- package/dist/api/http/routes/access.d.ts +24 -0
- package/dist/api/http/routes/access.js +259 -0
- package/dist/api/http/routes/actions.d.ts +8 -0
- package/dist/api/http/routes/actions.js +76 -0
- package/dist/api/http/routes/endpoints.d.ts +7 -0
- package/dist/api/http/routes/endpoints.js +100 -0
- package/dist/api/http/routes/sessions.d.ts +3 -0
- package/dist/api/http/routes/sessions.js +205 -0
- package/dist/api/http/routes/tenant.d.ts +10 -0
- package/dist/api/http/routes/tenant.js +172 -0
- package/dist/api/http/routes/vaults.d.ts +8 -0
- package/dist/api/http/routes/vaults.js +167 -0
- package/dist/api/openapi.d.ts +19 -0
- package/dist/api/openapi.js +138 -0
- package/dist/api/route.d.ts +12 -0
- package/dist/api/route.js +4 -0
- package/dist/core/bearer.d.ts +8 -0
- package/dist/core/bearer.js +11 -0
- package/dist/core/flow-host.d.ts +7 -15
- package/dist/core/flow-host.js +16 -41
- package/dist/core/sandbox-routes.d.ts +12 -7
- package/dist/core/sandbox-routes.js +24 -25
- package/dist/execution/memory.d.ts +8 -5
- package/dist/execution/memory.js +33 -13
- package/dist/execution/types.d.ts +38 -6
- package/dist/execution/types.js +12 -3
- package/dist/host/admin-api.d.ts +22 -0
- package/dist/host/admin-api.js +178 -0
- package/dist/host/app.d.ts +34 -0
- package/dist/host/app.js +185 -0
- package/dist/host/config-for.d.ts +2 -0
- package/dist/host/config-for.js +1 -0
- package/dist/host/config.d.ts +11 -0
- package/dist/host/cors.d.ts +27 -0
- package/dist/host/cors.js +64 -0
- package/dist/host/create-host.d.ts +31 -3
- package/dist/host/create-host.js +168 -327
- package/dist/host/http.d.ts +14 -3
- package/dist/host/http.js +64 -23
- package/dist/host/main.js +55 -4
- package/dist/host/stack-config.d.ts +20 -0
- package/dist/host/stack-config.js +55 -0
- package/dist/mcp/pool.d.ts +19 -1
- package/dist/mcp/pool.js +60 -10
- package/dist/model/catalog.d.ts +2 -10
- package/dist/model/classify.d.ts +13 -0
- package/dist/model/classify.js +61 -0
- package/dist/model/models.d.ts +10 -1
- package/dist/model/models.js +14 -6
- package/dist/model/pi-model.d.ts +17 -2
- package/dist/model/pi-model.js +366 -222
- package/dist/openapi.json +25696 -0
- package/dist/sandbox/manager.d.ts +13 -1
- package/dist/sandbox/manager.js +8 -11
- package/dist/sandbox/resolve.d.ts +33 -0
- package/dist/sandbox/resolve.js +81 -0
- package/dist/sandbox/session-sandbox.d.ts +11 -0
- package/dist/sandbox/session-sandbox.js +51 -0
- package/dist/sandbox/share.d.ts +14 -2
- package/dist/sandbox/share.js +33 -3
- package/dist/sandbox/tenant-config.d.ts +37 -0
- package/dist/sandbox/tenant-config.js +51 -0
- package/dist/sandbox/tools.d.ts +7 -9
- package/dist/sandbox/tools.js +11 -3
- package/dist/sandbox/types.d.ts +8 -2
- package/dist/store/event.d.ts +19 -0
- package/dist/store/event.js +49 -0
- package/dist/store/memory.d.ts +20 -8
- package/dist/store/memory.js +265 -100
- package/dist/store/postgres/connect.d.ts +13 -0
- package/dist/store/postgres/connect.js +5 -0
- package/dist/store/postgres/migrations/003_subject_tokens.d.ts +8 -0
- package/dist/store/postgres/migrations/003_subject_tokens.js +42 -0
- package/dist/store/postgres/migrations/004_publishable_keys.d.ts +6 -0
- package/dist/store/postgres/migrations/004_publishable_keys.js +18 -0
- package/dist/store/postgres/migrations/005_action_endpoints.d.ts +6 -0
- package/dist/store/postgres/migrations/005_action_endpoints.js +32 -0
- package/dist/store/postgres/migrations/006_drop_executors.d.ts +11 -0
- package/dist/store/postgres/migrations/006_drop_executors.js +32 -0
- package/dist/store/postgres/migrations/007_event_time.d.ts +7 -0
- package/dist/store/postgres/migrations/007_event_time.js +14 -0
- package/dist/store/postgres/migrations/008_durable_streams.d.ts +13 -0
- package/dist/store/postgres/migrations/008_durable_streams.js +35 -0
- package/dist/store/postgres/migrations/index.d.ts +5 -1
- package/dist/store/postgres/migrations/index.js +22 -2
- package/dist/store/postgres/migrations/shared/index.d.ts +31 -0
- package/dist/store/postgres/migrations/shared/index.js +92 -0
- package/dist/store/postgres/record.d.ts +11 -0
- package/dist/store/postgres/record.js +64 -0
- package/dist/store/postgres/store.js +337 -95
- package/dist/store/postgres/tenants.js +14 -0
- package/dist/store/types.d.ts +176 -81
- package/dist/streams/basin.d.ts +18 -1
- package/dist/streams/basin.js +34 -2
- package/dist/streams/control.d.ts +18 -0
- package/dist/streams/control.js +29 -0
- package/dist/streams/memory.d.ts +3 -1
- package/dist/streams/memory.js +14 -8
- package/dist/streams/relay/core.d.ts +35 -0
- package/dist/streams/relay/core.js +366 -0
- package/dist/streams/relay/memory.d.ts +69 -0
- package/dist/streams/relay/memory.js +176 -0
- package/dist/streams/relay/types.d.ts +77 -0
- package/dist/streams/relay/types.js +7 -0
- package/dist/streams/types.d.ts +66 -36
- package/dist/streams/types.js +18 -53
- package/dist/tenant/access-policy.d.ts +30 -0
- package/dist/tenant/access-policy.js +52 -0
- package/dist/tenant/actions.d.ts +10 -149
- package/dist/tenant/actions.js +5 -167
- package/dist/tenant/advance.js +32 -0
- package/dist/tenant/auth.d.ts +19 -17
- package/dist/tenant/auth.js +90 -93
- package/dist/tenant/browser.d.ts +15 -0
- package/dist/tenant/browser.js +39 -0
- package/dist/tenant/commands.d.ts +18 -2
- package/dist/tenant/commands.js +107 -113
- package/dist/tenant/context.d.ts +75 -18
- package/dist/tenant/context.js +14 -10
- package/dist/tenant/delivery-token.d.ts +41 -0
- package/dist/tenant/delivery-token.js +105 -0
- package/dist/tenant/delivery.d.ts +62 -0
- package/dist/tenant/delivery.js +476 -0
- package/dist/tenant/effects.d.ts +5 -0
- package/dist/tenant/effects.js +78 -37
- package/dist/tenant/endpoints.d.ts +67 -0
- package/dist/tenant/endpoints.js +90 -0
- package/dist/tenant/ephemeral.d.ts +6 -0
- package/dist/tenant/ephemeral.js +60 -36
- package/dist/tenant/http.d.ts +8 -9
- package/dist/tenant/http.js +13 -27
- package/dist/tenant/jwt.d.ts +16 -0
- package/dist/tenant/jwt.js +50 -0
- package/dist/tenant/module.d.ts +3 -0
- package/dist/tenant/module.js +6 -12
- package/dist/tenant/outbound.d.ts +44 -0
- package/dist/tenant/outbound.js +127 -0
- package/dist/tenant/pool.d.ts +0 -2
- package/dist/tenant/pool.js +0 -18
- package/dist/tenant/reset.d.ts +0 -4
- package/dist/tenant/reset.js +0 -5
- package/dist/tenant/runtime.d.ts +10 -20
- package/dist/tenant/runtime.js +59 -42
- package/dist/tenant/scheduler.d.ts +7 -2
- package/dist/tenant/scheduler.js +7 -3
- package/dist/tenant/session-streams.d.ts +125 -0
- package/dist/tenant/session-streams.js +399 -0
- package/dist/tenant/sessions.d.ts +28 -4
- package/dist/tenant/sessions.js +118 -24
- package/dist/tenant/signing-keys.d.ts +55 -0
- package/dist/tenant/signing-keys.js +172 -0
- package/dist/tenant/slim.d.ts +8 -0
- package/dist/tenant/slim.js +21 -0
- package/dist/tenant/status.d.ts +0 -3
- package/dist/tenant/status.js +19 -12
- package/dist/tenant/streams.d.ts +60 -67
- package/dist/tenant/streams.js +140 -208
- package/dist/tenant/subject-limits.d.ts +16 -0
- package/dist/tenant/subject-limits.js +45 -0
- package/dist/tenant/sweep.d.ts +1 -10
- package/dist/tenant/sweep.js +29 -69
- package/dist/tenant/tokens.d.ts +14 -0
- package/dist/tenant/tokens.js +178 -0
- package/dist/tenant/transcript.d.ts +48 -5
- package/dist/tenant/transcript.js +58 -1
- package/dist/tenant/types.d.ts +29 -7
- package/dist/tenant/worker.d.ts +3 -1
- package/dist/tenant/worker.js +9 -3
- package/dist/vault/service.d.ts +18 -2
- package/dist/vault/service.js +57 -13
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +20 -8
- package/dist/core/executors.d.ts +0 -44
- package/dist/core/executors.js +0 -95
- package/dist/streams/relay.d.ts +0 -37
- package/dist/streams/relay.js +0 -254
- package/dist/tenant/live.d.ts +0 -92
- package/dist/tenant/live.js +0 -290
- package/dist/tenant/routes-tenant.d.ts +0 -12
- package/dist/tenant/routes-tenant.js +0 -137
- package/dist/tenant/routes.d.ts +0 -9
- package/dist/tenant/routes.js +0 -151
package/dist/vault/service.js
CHANGED
|
@@ -36,9 +36,6 @@
|
|
|
36
36
|
* selectHostModel(body: SelectHostModelRequest): Promise<HostModelView>
|
|
37
37
|
* readHostModel(): Promise<HostModelSecret | undefined>
|
|
38
38
|
* updateHostCredential(credential): Promise<void>
|
|
39
|
-
*
|
|
40
|
-
* // Rejected caller audit: opens its own transaction.
|
|
41
|
-
* reject(route: string): Promise<void>
|
|
42
39
|
* ```
|
|
43
40
|
*
|
|
44
41
|
* ## Transactions and I/O
|
|
@@ -67,16 +64,21 @@ import { VaultError } from "./error.js";
|
|
|
67
64
|
import { hostModelCatalog } from "../model/catalog.js";
|
|
68
65
|
import { normalizeVaultUrl } from "./url.js";
|
|
69
66
|
const REFRESH_SKEW_MS = 60_000;
|
|
67
|
+
const REFRESH_TIMEOUT_MS = 30_000;
|
|
70
68
|
const HOST_VAULT_ID = "host";
|
|
71
69
|
const HOST_MODEL_ID = "host-model";
|
|
72
70
|
export class VaultService {
|
|
73
71
|
store;
|
|
74
72
|
kek;
|
|
75
73
|
fetchImpl;
|
|
74
|
+
refreshTimeoutMs;
|
|
75
|
+
/** Refreshes in progress by credential id: concurrent uses share one. */
|
|
76
|
+
refreshing = new Map();
|
|
76
77
|
constructor(options) {
|
|
77
78
|
this.store = options.store;
|
|
78
79
|
this.kek = options.kek;
|
|
79
80
|
this.fetchImpl = options.fetch;
|
|
81
|
+
this.refreshTimeoutMs = options.refreshTimeoutMs ?? REFRESH_TIMEOUT_MS;
|
|
80
82
|
}
|
|
81
83
|
// --- administration --------------------------------------------------------
|
|
82
84
|
async createVault(body) {
|
|
@@ -338,7 +340,7 @@ export class VaultService {
|
|
|
338
340
|
};
|
|
339
341
|
}
|
|
340
342
|
if (row.type === "oauth" && dueForRefresh(row.expiresAt)) {
|
|
341
|
-
const refreshed = await this.
|
|
343
|
+
const refreshed = await this.refreshShared(kek, row, payload, input.sessionId, url);
|
|
342
344
|
if (refreshed.status === "refused")
|
|
343
345
|
return refreshed;
|
|
344
346
|
payload = refreshed.payload;
|
|
@@ -386,6 +388,7 @@ export class VaultService {
|
|
|
386
388
|
model: binding.model,
|
|
387
389
|
authType: binding.authType,
|
|
388
390
|
...(binding.baseUrl ? { baseUrl: binding.baseUrl } : {}),
|
|
391
|
+
...(binding.settings ? { settings: binding.settings } : {}),
|
|
389
392
|
lastUpdated: row.rotatedAt ?? row.createdAt,
|
|
390
393
|
active: binding.provider === active,
|
|
391
394
|
};
|
|
@@ -412,6 +415,7 @@ export class VaultService {
|
|
|
412
415
|
provider: body.provider,
|
|
413
416
|
model: body.model,
|
|
414
417
|
baseUrl: body.baseUrl,
|
|
418
|
+
settings: body.settings,
|
|
415
419
|
authType: body.auth.type,
|
|
416
420
|
payload,
|
|
417
421
|
});
|
|
@@ -442,6 +446,8 @@ export class VaultService {
|
|
|
442
446
|
provider: body.provider,
|
|
443
447
|
model: body.model,
|
|
444
448
|
baseUrl: body.baseUrl,
|
|
449
|
+
// Selecting a model keeps the endpoint's settings.
|
|
450
|
+
settings: binding.settings,
|
|
445
451
|
authType: binding.authType,
|
|
446
452
|
payload,
|
|
447
453
|
});
|
|
@@ -474,6 +480,7 @@ export class VaultService {
|
|
|
474
480
|
provider: binding.provider,
|
|
475
481
|
model: binding.model,
|
|
476
482
|
...(binding.baseUrl ? { baseUrl: binding.baseUrl } : {}),
|
|
483
|
+
...(binding.settings ? { settings: binding.settings } : {}),
|
|
477
484
|
authType: binding.authType,
|
|
478
485
|
credential,
|
|
479
486
|
};
|
|
@@ -500,17 +507,47 @@ export class VaultService {
|
|
|
500
507
|
});
|
|
501
508
|
});
|
|
502
509
|
}
|
|
503
|
-
// --- rejected callers ---------------------------------------------------------
|
|
504
|
-
async reject(route) {
|
|
505
|
-
await this.store.tx((t) => this.audit(t, {
|
|
506
|
-
actor: "executor",
|
|
507
|
-
action: "reject",
|
|
508
|
-
target: route,
|
|
509
|
-
outcome: "rejected",
|
|
510
|
-
}));
|
|
511
|
-
}
|
|
512
510
|
// --- internals -----------------------------------------------------------------
|
|
513
511
|
/** Calls the token endpoint outside any transaction, then stores the result. */
|
|
512
|
+
/**
|
|
513
|
+
* Refreshes an OAuth grant once for every concurrent use in this process. A rotating refresh
|
|
514
|
+
* token is valid once, so two refreshes with it would fail the second, or make the provider
|
|
515
|
+
* revoke the grant.
|
|
516
|
+
*/
|
|
517
|
+
refreshShared(kek, row, payload, sessionId, url) {
|
|
518
|
+
let pending = this.refreshing.get(row.id);
|
|
519
|
+
if (!pending) {
|
|
520
|
+
pending = this.refreshLatest(kek, row, payload, sessionId, url).finally(() => this.refreshing.delete(row.id));
|
|
521
|
+
this.refreshing.set(row.id, pending);
|
|
522
|
+
}
|
|
523
|
+
return pending;
|
|
524
|
+
}
|
|
525
|
+
/** Uses a token another process refreshed instead of refreshing again, before and after. */
|
|
526
|
+
async refreshLatest(kek, row, payload, sessionId, url) {
|
|
527
|
+
const before = await this.rotatedSince(kek, row);
|
|
528
|
+
if (before)
|
|
529
|
+
return { status: "ok", payload: before };
|
|
530
|
+
const refreshed = await this.refresh(kek, row, payload, sessionId, url);
|
|
531
|
+
if (refreshed.status === "ok")
|
|
532
|
+
return refreshed;
|
|
533
|
+
// The loser of a race with another process fails with the spent refresh token.
|
|
534
|
+
const after = await this.rotatedSince(kek, row);
|
|
535
|
+
return after ? { status: "ok", payload: after } : refreshed;
|
|
536
|
+
}
|
|
537
|
+
/** The credential's payload when it was rotated after `row` was read and is not due. */
|
|
538
|
+
async rotatedSince(kek, row) {
|
|
539
|
+
const current = await this.store.tx((t) => t.getCredential(row.id));
|
|
540
|
+
if (!current || current.vaultId !== row.vaultId || !isUserCredential(current))
|
|
541
|
+
return undefined;
|
|
542
|
+
if (current.rotatedAt === row.rotatedAt || dueForRefresh(current.expiresAt))
|
|
543
|
+
return undefined;
|
|
544
|
+
try {
|
|
545
|
+
return readPayload(kek, current);
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
return undefined;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
514
551
|
async refresh(kek, row, payload, sessionId, url) {
|
|
515
552
|
const failRefresh = async () => {
|
|
516
553
|
await this.store.tx((t) => this.audit(t, {
|
|
@@ -557,6 +594,7 @@ export class VaultService {
|
|
|
557
594
|
headers,
|
|
558
595
|
body: body.toString(),
|
|
559
596
|
redirect: "error",
|
|
597
|
+
signal: AbortSignal.timeout(this.refreshTimeoutMs),
|
|
560
598
|
});
|
|
561
599
|
if (!response.ok)
|
|
562
600
|
return failRefresh();
|
|
@@ -640,6 +678,8 @@ export class VaultService {
|
|
|
640
678
|
});
|
|
641
679
|
}
|
|
642
680
|
validateHostModel(body) {
|
|
681
|
+
if (body.settings && body.provider !== "custom")
|
|
682
|
+
throw new VaultError(400, "Model settings (context window, output limit, reasoning, compat) apply only to a custom provider.");
|
|
643
683
|
if (body.provider === "custom") {
|
|
644
684
|
if (!body.baseUrl)
|
|
645
685
|
throw new VaultError(400, "A base URL is required for a custom provider.");
|
|
@@ -673,6 +713,9 @@ export class VaultService {
|
|
|
673
713
|
provider: input.provider,
|
|
674
714
|
model: input.model,
|
|
675
715
|
...(input.baseUrl ? { baseUrl: input.baseUrl } : {}),
|
|
716
|
+
...(input.settings && Object.keys(input.settings).length
|
|
717
|
+
? { settings: input.settings }
|
|
718
|
+
: {}),
|
|
676
719
|
authType: input.authType,
|
|
677
720
|
}),
|
|
678
721
|
expiresAt: null,
|
|
@@ -692,6 +735,7 @@ export class VaultService {
|
|
|
692
735
|
model: binding.model,
|
|
693
736
|
authType: binding.authType,
|
|
694
737
|
...(binding.baseUrl ? { baseUrl: binding.baseUrl } : {}),
|
|
738
|
+
...(binding.settings ? { settings: binding.settings } : {}),
|
|
695
739
|
};
|
|
696
740
|
}
|
|
697
741
|
async hostModelRows(t) {
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const RUNTIME_VERSION = "0.
|
|
1
|
+
export declare const RUNTIME_VERSION = "0.14.0-beta";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nylorun/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0-beta",
|
|
4
4
|
"description": "Nylorun Runtime: the agent execution Host on Postgres, Restate and S2, and model providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
"types": "./dist/configuration.d.ts",
|
|
31
31
|
"import": "./dist/configuration.js"
|
|
32
32
|
},
|
|
33
|
+
"./openapi.json": "./dist/openapi.json",
|
|
34
|
+
"./admin-openapi.json": "./dist/admin-openapi.json",
|
|
33
35
|
"./package.json": "./package.json"
|
|
34
36
|
},
|
|
35
37
|
"files": [
|
|
@@ -43,7 +45,7 @@
|
|
|
43
45
|
"provenance": true
|
|
44
46
|
},
|
|
45
47
|
"scripts": {
|
|
46
|
-
"build": "npm run clean && node ../scripts/check-typescript-version.mjs && tsc -p tsconfig.json",
|
|
48
|
+
"build": "npm run clean && node ../scripts/check-typescript-version.mjs && tsc -p tsconfig.json && node scripts/build-openapi.mjs",
|
|
47
49
|
"check": "npm run build && npm test && node scripts/check-package.mjs",
|
|
48
50
|
"typecheck": "node ../scripts/check-typescript-version.mjs && tsc -p tsconfig.json --noEmit",
|
|
49
51
|
"test": "vitest run",
|
|
@@ -55,21 +57,31 @@
|
|
|
55
57
|
"start": "node dist/host/main.js"
|
|
56
58
|
},
|
|
57
59
|
"dependencies": {
|
|
58
|
-
"@
|
|
59
|
-
"@
|
|
60
|
+
"@ag-ui/core": "~1.0.0",
|
|
61
|
+
"@asteasolutions/zod-to-openapi": "^9.1.0",
|
|
62
|
+
"@earendil-works/pi-ai": "0.99.1",
|
|
60
63
|
"@hono/node-server": "^2.1.1",
|
|
61
|
-
"@
|
|
62
|
-
"@
|
|
64
|
+
"@hono/zod-openapi": "^1.6.3",
|
|
65
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
66
|
+
"@nylorun/core": "0.9.0-beta",
|
|
67
|
+
"@nylorun/harness": "0.21.0-beta",
|
|
63
68
|
"@restatedev/restate-sdk": "1.16.9",
|
|
64
69
|
"@s2-dev/streamstore": "0.27.0",
|
|
70
|
+
"hono": "^4.13.12",
|
|
71
|
+
"jose": "^6.2.12",
|
|
65
72
|
"just-bash": "3.4.2",
|
|
66
|
-
"
|
|
73
|
+
"pg": "8.23.1",
|
|
74
|
+
"pg-logical-replication": "2.5.0",
|
|
75
|
+
"postgres": "^3.4.9",
|
|
76
|
+
"zod": "^4.6.5"
|
|
67
77
|
},
|
|
68
78
|
"devDependencies": {
|
|
79
|
+
"@a2a-js/sdk": "~1.3.0",
|
|
69
80
|
"@ag-ui/client": "1.0.0",
|
|
81
|
+
"@scalar/openapi-parser": "^0.29.9",
|
|
70
82
|
"@types/node": "^26.5.0",
|
|
83
|
+
"@types/pg": "^8.23.1",
|
|
71
84
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
72
|
-
"hono": "^4.13.8",
|
|
73
85
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
74
86
|
"vitest": "^5.0.1"
|
|
75
87
|
}
|
package/dist/core/executors.d.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { ExecutorScope } from "@nylorun/core/contracts";
|
|
2
|
-
import type { SessionStore } from "../store/types.js";
|
|
3
|
-
/**
|
|
4
|
-
* Executor bearer tokens are 256-bit CSPRNG values minted by the CLI and the wire schema
|
|
5
|
-
* requires at least 16 characters, so an unsalted digest is adequate to keep them out of
|
|
6
|
-
* the Session Store at rest. This is not a password KDF and must not be used for one.
|
|
7
|
-
*/
|
|
8
|
-
export declare const hashToken: (token: string) => string;
|
|
9
|
-
export interface ExecutorRecord extends ExecutorScope {
|
|
10
|
-
readonly tokenHash: string;
|
|
11
|
-
/** Always true under Runtime Tenants; startup env executors are removed (D8). */
|
|
12
|
-
readonly persisted: boolean;
|
|
13
|
-
readonly updatedAt: string;
|
|
14
|
-
/** Application principal that last registered this executor, when known. */
|
|
15
|
-
readonly principalId?: string;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Rejects tokens that are weak, empty of agent identity, or whose hash equals any
|
|
19
|
-
* application principal hash (A5). Replaces the old `serverToken` equality check.
|
|
20
|
-
*/
|
|
21
|
-
export declare function assertExecutorCredential(executor: {
|
|
22
|
-
agentId: string;
|
|
23
|
-
implementationVersion: string;
|
|
24
|
-
token: string;
|
|
25
|
-
}, applicationHashes: readonly string[]): void;
|
|
26
|
-
/** Mint a 32-byte hex application/executor key for tests and CLI bootstrap. */
|
|
27
|
-
export declare function mintBearerToken(): string;
|
|
28
|
-
export declare class ExecutorRegistry {
|
|
29
|
-
private readonly byHash;
|
|
30
|
-
private readonly byAgent;
|
|
31
|
-
seed(records: readonly ExecutorRecord[]): void;
|
|
32
|
-
find(tokenHash: string): ExecutorRecord | undefined;
|
|
33
|
-
get(agentId: string): ExecutorRecord | undefined;
|
|
34
|
-
list(): readonly ExecutorRecord[];
|
|
35
|
-
upsert(record: ExecutorRecord): {
|
|
36
|
-
rotated: boolean;
|
|
37
|
-
previousHash?: string;
|
|
38
|
-
previousPrincipalId?: string;
|
|
39
|
-
};
|
|
40
|
-
remove(agentId: string): ExecutorRecord | undefined;
|
|
41
|
-
private write;
|
|
42
|
-
}
|
|
43
|
-
/** A registry seeded from the executors the store persisted (at Tenant open). */
|
|
44
|
-
export declare function loadExecutorRegistry(store: SessionStore): Promise<ExecutorRegistry>;
|
package/dist/core/executors.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
-
const equals = (a, b) => {
|
|
3
|
-
const aa = Buffer.from(a), bb = Buffer.from(b);
|
|
4
|
-
return aa.length === bb.length && timingSafeEqual(aa, bb);
|
|
5
|
-
};
|
|
6
|
-
/**
|
|
7
|
-
* Executor bearer tokens are 256-bit CSPRNG values minted by the CLI and the wire schema
|
|
8
|
-
* requires at least 16 characters, so an unsalted digest is adequate to keep them out of
|
|
9
|
-
* the Session Store at rest. This is not a password KDF and must not be used for one.
|
|
10
|
-
*/
|
|
11
|
-
export const hashToken = (token) => createHash("sha256").update(token, "utf8").digest("hex");
|
|
12
|
-
/**
|
|
13
|
-
* Rejects tokens that are weak, empty of agent identity, or whose hash equals any
|
|
14
|
-
* application principal hash (A5). Replaces the old `serverToken` equality check.
|
|
15
|
-
*/
|
|
16
|
-
export function assertExecutorCredential(executor, applicationHashes) {
|
|
17
|
-
const tokenHash = hashToken(executor.token);
|
|
18
|
-
const matchesApplication = applicationHashes.some((hash) => equals(hash, tokenHash));
|
|
19
|
-
if (executor.token.length < 16 ||
|
|
20
|
-
matchesApplication ||
|
|
21
|
-
!executor.agentId ||
|
|
22
|
-
!executor.implementationVersion)
|
|
23
|
-
throw new Error("Executor tokens require independent credentials and an agent id");
|
|
24
|
-
}
|
|
25
|
-
/** Mint a 32-byte hex application/executor key for tests and CLI bootstrap. */
|
|
26
|
-
export function mintBearerToken() {
|
|
27
|
-
return randomBytes(32).toString("hex");
|
|
28
|
-
}
|
|
29
|
-
export class ExecutorRegistry {
|
|
30
|
-
byHash = new Map();
|
|
31
|
-
byAgent = new Map();
|
|
32
|
-
seed(records) {
|
|
33
|
-
for (const record of records)
|
|
34
|
-
this.write(record);
|
|
35
|
-
}
|
|
36
|
-
find(tokenHash) {
|
|
37
|
-
return this.byHash.get(tokenHash);
|
|
38
|
-
}
|
|
39
|
-
get(agentId) {
|
|
40
|
-
return this.byAgent.get(agentId);
|
|
41
|
-
}
|
|
42
|
-
list() {
|
|
43
|
-
return [...this.byAgent.values()];
|
|
44
|
-
}
|
|
45
|
-
upsert(record) {
|
|
46
|
-
const previous = this.byAgent.get(record.agentId);
|
|
47
|
-
const collision = this.byHash.get(record.tokenHash);
|
|
48
|
-
if (collision && collision.agentId !== record.agentId)
|
|
49
|
-
throw new Error("Executor tokens must be unique");
|
|
50
|
-
this.write(record);
|
|
51
|
-
if (!previous || previous.tokenHash === record.tokenHash)
|
|
52
|
-
return { rotated: false };
|
|
53
|
-
return {
|
|
54
|
-
rotated: true,
|
|
55
|
-
previousHash: previous.tokenHash,
|
|
56
|
-
...(previous.principalId === undefined
|
|
57
|
-
? {}
|
|
58
|
-
: { previousPrincipalId: previous.principalId }),
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
remove(agentId) {
|
|
62
|
-
const record = this.byAgent.get(agentId);
|
|
63
|
-
if (!record)
|
|
64
|
-
return undefined;
|
|
65
|
-
this.byAgent.delete(agentId);
|
|
66
|
-
this.byHash.delete(record.tokenHash);
|
|
67
|
-
return record;
|
|
68
|
-
}
|
|
69
|
-
write(record) {
|
|
70
|
-
const previous = this.byAgent.get(record.agentId);
|
|
71
|
-
if (previous)
|
|
72
|
-
this.byHash.delete(previous.tokenHash);
|
|
73
|
-
this.byAgent.set(record.agentId, record);
|
|
74
|
-
this.byHash.set(record.tokenHash, record);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
/** A registry seeded from the executors the store persisted (at Tenant open). */
|
|
78
|
-
export async function loadExecutorRegistry(store) {
|
|
79
|
-
const registry = new ExecutorRegistry();
|
|
80
|
-
const rows = await store.tx((t) => t.listExecutors());
|
|
81
|
-
registry.seed(rows.map((row) => ({
|
|
82
|
-
agentId: row.agentId,
|
|
83
|
-
implementationVersion: row.implementationVersion,
|
|
84
|
-
...(row.manifestHash === undefined
|
|
85
|
-
? {}
|
|
86
|
-
: { manifestHash: row.manifestHash }),
|
|
87
|
-
tokenHash: row.tokenHash,
|
|
88
|
-
persisted: true,
|
|
89
|
-
updatedAt: row.updatedAt,
|
|
90
|
-
...(row.principalId === undefined
|
|
91
|
-
? {}
|
|
92
|
-
: { principalId: row.principalId }),
|
|
93
|
-
})));
|
|
94
|
-
return registry;
|
|
95
|
-
}
|
package/dist/streams/relay.d.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { SessionStore } from "../store/types.js";
|
|
2
|
-
import { type DurableStreams } from "./types.js";
|
|
3
|
-
/** A session's outbox starts past the end of its stream: events are missing from the stream. */
|
|
4
|
-
export declare class StreamGapError extends Error {
|
|
5
|
-
constructor(sessionId: string, first: number, tail: number);
|
|
6
|
-
}
|
|
7
|
-
export interface RelayOptions {
|
|
8
|
-
store: SessionStore;
|
|
9
|
-
streams: DurableStreams;
|
|
10
|
-
/** The Tenant whose basin receives the events; must match `store.tenantId`. */
|
|
11
|
-
tenantId: string;
|
|
12
|
-
/** Receives append and delete failures. The rows they concern stay in the outbox. */
|
|
13
|
-
onError?: (error: unknown) => void;
|
|
14
|
-
}
|
|
15
|
-
export interface Relay {
|
|
16
|
-
/**
|
|
17
|
-
* Appends leftover outbox rows (at most `limit`, default 1000) per session in
|
|
18
|
-
* sequence order: the Tenant sweep's recovery after a crash or an S2 outage.
|
|
19
|
-
* Resolves with the number of rows now in S2 and removed from the outbox.
|
|
20
|
-
* Per-session failures go to `onError`; those rows stay.
|
|
21
|
-
*/
|
|
22
|
-
drain(limit?: number): Promise<number>;
|
|
23
|
-
/** Resolves when every relay started so far has settled. */
|
|
24
|
-
idle(): Promise<void>;
|
|
25
|
-
/** Stops listening to commits and waits for in-flight relays. */
|
|
26
|
-
close(): Promise<void>;
|
|
27
|
-
}
|
|
28
|
-
export declare function createRelay(options: RelayOptions): Relay;
|
|
29
|
-
/** Appends a `work_available` signal to `tenant/work`. Not relayed through the outbox. */
|
|
30
|
-
export declare function signalWork(streams: DurableStreams, tenantId: string): Promise<void>;
|
|
31
|
-
/** Appends a `session.cancel` signal to `tenant/control`. Not relayed through the outbox. */
|
|
32
|
-
export declare function signalCancel(streams: DurableStreams, tenantId: string, sessionId: string): Promise<void>;
|
|
33
|
-
/**
|
|
34
|
-
* Appends a `sessions.reset` signal to `tenant/control`: every process with the Tenant open
|
|
35
|
-
* checks its session feeds against the Session Store.
|
|
36
|
-
*/
|
|
37
|
-
export declare function signalSessionsReset(streams: DurableStreams, tenantId: string): Promise<void>;
|
package/dist/streams/relay.js
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The outbox relay (architecture §12.4, "Writing: outbox and relay").
|
|
3
|
-
*
|
|
4
|
-
* The Session Store writes every event to its outbox in the same transaction
|
|
5
|
-
* as the state change. After the commit the relay appends the events to the
|
|
6
|
-
* stream of their session's incarnation (`sessions/<id>/<incarnation>`) with
|
|
7
|
-
* `matchSeq` set to each batch's first sequence, and deletes the rows S2 has:
|
|
8
|
-
*
|
|
9
|
-
* - **The right incarnation.** A committed event comes with the incarnation
|
|
10
|
-
* its session had when the event was allocated (`Commit.incarnations`).
|
|
11
|
-
* Leftover rows (`drain`, and a commit that finds earlier rows still
|
|
12
|
-
* waiting) are read with the session's incarnation in one transaction that
|
|
13
|
-
* locks the session; deleting a session deletes its outbox rows in the same
|
|
14
|
-
* transaction, so those rows belong to the incarnation read. Either way a
|
|
15
|
-
* session deleted and created again with the same id never gets the old
|
|
16
|
-
* incarnation's events, or the reverse.
|
|
17
|
-
* - **ok**: the rows are deleted through the last appended sequence, only
|
|
18
|
-
* while the session still has the incarnation appended to (one statement,
|
|
19
|
-
* `deleteOutbox` with `incarnation`).
|
|
20
|
-
* - **`seq_mismatch` with the tail past the batch start**: an earlier attempt
|
|
21
|
-
* (this process or another) already appended those events. The rows below
|
|
22
|
-
* the tail are deleted, and whatever is left of the batch is appended from
|
|
23
|
-
* the tail.
|
|
24
|
-
* - **`seq_mismatch` with the tail before the batch start**: earlier events of
|
|
25
|
-
* the session are still in the outbox (an earlier append failed), and the
|
|
26
|
-
* session's outbox is relayed in sequence order, which includes this batch.
|
|
27
|
-
* If the outbox's first row is past the tail, events are missing from the
|
|
28
|
-
* stream: the rows stay and the error goes to `onError`.
|
|
29
|
-
* - **append throws** (S2 unreachable, basin missing): the rows stay in the
|
|
30
|
-
* outbox and the error goes to `onError`. The next commit of the session, or
|
|
31
|
-
* the Tenant sweep's `drain`, appends them in order.
|
|
32
|
-
*
|
|
33
|
-
* Conditional appends make this exactly-once and in order per session however
|
|
34
|
-
* many processes relay the same rows. Within one process the relay also runs
|
|
35
|
-
* one relay at a time per session.
|
|
36
|
-
*
|
|
37
|
-
* `work_available`, `session.cancel` and `sessions.reset` are signals, not
|
|
38
|
-
* canonical events: they skip the outbox (`signalWork`, `signalCancel`,
|
|
39
|
-
* `signalSessionsReset`). A commit that called `t.signalWork()` is signalled
|
|
40
|
-
* by the relay after its events.
|
|
41
|
-
*/
|
|
42
|
-
import { decodeCursor } from "../store/cursor.js";
|
|
43
|
-
import { CONTROL_STREAM, WORK_AVAILABLE, WORK_STREAM, streamOfSession, } from "./types.js";
|
|
44
|
-
/** A session's outbox starts past the end of its stream: events are missing from the stream. */
|
|
45
|
-
export class StreamGapError extends Error {
|
|
46
|
-
constructor(sessionId, first, tail) {
|
|
47
|
-
super(`Outbox of session ${sessionId} starts at ${first}, past the end of its stream at ${tail}; events before it are missing`);
|
|
48
|
-
this.name = "StreamGapError";
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
/** At most this many records per append (S2 allows 1000). */
|
|
52
|
-
const MAX_BATCH_RECORDS = 500;
|
|
53
|
-
/** At most this many bytes of JSON per append (S2 allows 1 MiB metered). */
|
|
54
|
-
const MAX_BATCH_BYTES = 768 * 1024;
|
|
55
|
-
/** Outbox rows read per page while relaying one session. */
|
|
56
|
-
const DRAIN_PAGE = 1000;
|
|
57
|
-
export function createRelay(options) {
|
|
58
|
-
const { store, streams, tenantId } = options;
|
|
59
|
-
if (store.tenantId !== tenantId)
|
|
60
|
-
throw new Error(`Relay tenant ${tenantId} does not match store tenant ${store.tenantId}`);
|
|
61
|
-
const onError = options.onError ?? (() => { });
|
|
62
|
-
/** sessionId → tail of that session's relay chain. */
|
|
63
|
-
const chains = new Map();
|
|
64
|
-
const pending = new Set();
|
|
65
|
-
let closed = false;
|
|
66
|
-
/** Runs `task` after every earlier task of the same session. */
|
|
67
|
-
function serialize(sessionId, task) {
|
|
68
|
-
const previous = chains.get(sessionId) ?? Promise.resolve();
|
|
69
|
-
const run = previous.then(task, task);
|
|
70
|
-
const settled = run.then(() => undefined, () => undefined);
|
|
71
|
-
chains.set(sessionId, settled);
|
|
72
|
-
pending.add(settled);
|
|
73
|
-
void settled.then(() => {
|
|
74
|
-
pending.delete(settled);
|
|
75
|
-
if (chains.get(sessionId) === settled)
|
|
76
|
-
chains.delete(sessionId);
|
|
77
|
-
});
|
|
78
|
-
return run;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Appends `rows` (one session, ascending, contiguous) to the stream of `incarnation`
|
|
82
|
-
* with `matchSeq`, and deletes what S2 has while the session still has that incarnation.
|
|
83
|
-
* Throws a `StreamGapError` when the stream's tail is before the first row still to append.
|
|
84
|
-
*/
|
|
85
|
-
async function appendRows(sessionId, incarnation, rows) {
|
|
86
|
-
const stream = streamOfSession({
|
|
87
|
-
id: sessionId,
|
|
88
|
-
...(incarnation === null ? {} : { streamIncarnation: incarnation }),
|
|
89
|
-
});
|
|
90
|
-
const deleteThrough = (seq) => store.tx((t) => t.deleteOutbox(sessionId, seq, incarnation));
|
|
91
|
-
let rest = rows;
|
|
92
|
-
while (rest.length > 0) {
|
|
93
|
-
const batch = takeBatch(rest);
|
|
94
|
-
const first = batch[0].seq;
|
|
95
|
-
const last = batch[batch.length - 1].seq;
|
|
96
|
-
const result = await streams.append(tenantId, stream, batch.map((row) => row.event), { matchSeq: first });
|
|
97
|
-
if (result.status === "ok") {
|
|
98
|
-
await deleteThrough(last);
|
|
99
|
-
rest = rest.slice(batch.length);
|
|
100
|
-
}
|
|
101
|
-
else if (result.tail > first) {
|
|
102
|
-
// Already appended by an earlier attempt: S2 has everything below the tail.
|
|
103
|
-
await deleteThrough(result.tail - 1);
|
|
104
|
-
rest = rest.filter((row) => row.seq >= result.tail);
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
throw new StreamGapError(sessionId, first, result.tail);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Appends the session's outbox rows in sequence order, at most `limit`, reading them with
|
|
113
|
-
* the session's incarnation under its lock. Resolves with the number relayed.
|
|
114
|
-
*/
|
|
115
|
-
async function relaySession(sessionId, limit = Infinity) {
|
|
116
|
-
let relayed = 0;
|
|
117
|
-
while (relayed < limit) {
|
|
118
|
-
const page = Math.min(DRAIN_PAGE, limit - relayed);
|
|
119
|
-
const read = await store.tx(async (t) => {
|
|
120
|
-
const session = await t.lockSession(sessionId);
|
|
121
|
-
// A deleted session took its outbox rows with it.
|
|
122
|
-
if (!session)
|
|
123
|
-
return undefined;
|
|
124
|
-
return {
|
|
125
|
-
incarnation: session.streamIncarnation ?? null,
|
|
126
|
-
rows: await t.outbox(page, { sessionId }),
|
|
127
|
-
};
|
|
128
|
-
});
|
|
129
|
-
if (!read || read.rows.length === 0)
|
|
130
|
-
return relayed;
|
|
131
|
-
const run = contiguous(read.rows);
|
|
132
|
-
await appendRows(sessionId, read.incarnation, run);
|
|
133
|
-
relayed += run.length;
|
|
134
|
-
// A gap inside the outbox: the next page starts at it and reports it.
|
|
135
|
-
if (read.rows.length < page && run.length === read.rows.length)
|
|
136
|
-
return relayed;
|
|
137
|
-
}
|
|
138
|
-
return relayed;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Appends one commit's rows of a session to the stream of the incarnation they were
|
|
142
|
-
* written under. When earlier rows are still in the outbox, relays the whole outbox.
|
|
143
|
-
*/
|
|
144
|
-
async function relayCommitted(sessionId, incarnation, rows) {
|
|
145
|
-
try {
|
|
146
|
-
await appendRows(sessionId, incarnation, contiguous(rows));
|
|
147
|
-
}
|
|
148
|
-
catch (error) {
|
|
149
|
-
if (!(error instanceof StreamGapError))
|
|
150
|
-
throw error;
|
|
151
|
-
await relaySession(sessionId);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
function onCommit(commit) {
|
|
155
|
-
if (closed)
|
|
156
|
-
return;
|
|
157
|
-
/** `sessionId\0incarnation` → the commit's rows of that incarnation. */
|
|
158
|
-
const groups = new Map();
|
|
159
|
-
commit.events.forEach((event, i) => {
|
|
160
|
-
let seq;
|
|
161
|
-
try {
|
|
162
|
-
seq = decodeCursor(event.sessionId, event.cursor);
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
onError(new Error(`Event ${event.eventId} has an invalid cursor; left for the sweep`));
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
const incarnation = commit.incarnations[i] ?? null;
|
|
169
|
-
const key = `${event.sessionId}\u0000${incarnation ?? ""}`;
|
|
170
|
-
let group = groups.get(key);
|
|
171
|
-
if (!group)
|
|
172
|
-
groups.set(key, (group = { sessionId: event.sessionId, incarnation, rows: [] }));
|
|
173
|
-
group.rows.push({ sessionId: event.sessionId, seq, event });
|
|
174
|
-
});
|
|
175
|
-
const relays = [...groups.values()].map(({ sessionId, incarnation, rows }) => {
|
|
176
|
-
rows.sort((a, b) => a.seq - b.seq);
|
|
177
|
-
return serialize(sessionId, () => relayCommitted(sessionId, incarnation, rows)).catch(onError);
|
|
178
|
-
});
|
|
179
|
-
if (commit.workAvailable) {
|
|
180
|
-
const signalled = Promise.all(relays)
|
|
181
|
-
.then(() => signalWork(streams, tenantId))
|
|
182
|
-
.catch(onError);
|
|
183
|
-
pending.add(signalled);
|
|
184
|
-
void signalled.then(() => pending.delete(signalled));
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
const unsubscribe = store.onCommit(onCommit);
|
|
188
|
-
async function idle() {
|
|
189
|
-
while (pending.size > 0)
|
|
190
|
-
await Promise.all([...pending]);
|
|
191
|
-
}
|
|
192
|
-
return {
|
|
193
|
-
async drain(limit = 1000) {
|
|
194
|
-
const rows = await store.tx((t) => t.outbox(limit));
|
|
195
|
-
const bySession = new Map();
|
|
196
|
-
for (const row of rows)
|
|
197
|
-
bySession.set(row.sessionId, (bySession.get(row.sessionId) ?? 0) + 1);
|
|
198
|
-
const counts = await Promise.all([...bySession].map(([sessionId, count]) =>
|
|
199
|
-
// Rows may have been relayed since they were read; the conditional
|
|
200
|
-
// appends skip those.
|
|
201
|
-
serialize(sessionId, () => relaySession(sessionId, count)).catch((error) => {
|
|
202
|
-
onError(error);
|
|
203
|
-
return 0;
|
|
204
|
-
})));
|
|
205
|
-
return counts.reduce((sum, n) => sum + n, 0);
|
|
206
|
-
},
|
|
207
|
-
idle,
|
|
208
|
-
async close() {
|
|
209
|
-
closed = true;
|
|
210
|
-
unsubscribe();
|
|
211
|
-
await idle();
|
|
212
|
-
},
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
/** Appends a `work_available` signal to `tenant/work`. Not relayed through the outbox. */
|
|
216
|
-
export async function signalWork(streams, tenantId) {
|
|
217
|
-
await streams.append(tenantId, WORK_STREAM, [WORK_AVAILABLE]);
|
|
218
|
-
}
|
|
219
|
-
/** Appends a `session.cancel` signal to `tenant/control`. Not relayed through the outbox. */
|
|
220
|
-
export async function signalCancel(streams, tenantId, sessionId) {
|
|
221
|
-
if (!sessionId)
|
|
222
|
-
throw new Error("sessionId is required");
|
|
223
|
-
const signal = { type: "session.cancel", sessionId };
|
|
224
|
-
await streams.append(tenantId, CONTROL_STREAM, [signal]);
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Appends a `sessions.reset` signal to `tenant/control`: every process with the Tenant open
|
|
228
|
-
* checks its session feeds against the Session Store.
|
|
229
|
-
*/
|
|
230
|
-
export async function signalSessionsReset(streams, tenantId) {
|
|
231
|
-
const signal = { type: "sessions.reset" };
|
|
232
|
-
await streams.append(tenantId, CONTROL_STREAM, [signal]);
|
|
233
|
-
}
|
|
234
|
-
// ---------------------------------------------------------------------------
|
|
235
|
-
/** The leading run of `rows` (ascending) without a sequence gap. */
|
|
236
|
-
function contiguous(rows) {
|
|
237
|
-
let n = 1;
|
|
238
|
-
while (n < rows.length && rows[n].seq === rows[n - 1].seq + 1)
|
|
239
|
-
n += 1;
|
|
240
|
-
return rows.slice(0, n);
|
|
241
|
-
}
|
|
242
|
-
/** The leading rows that fit one append. Always at least one row. */
|
|
243
|
-
function takeBatch(rows) {
|
|
244
|
-
let bytes = 0;
|
|
245
|
-
let n = 0;
|
|
246
|
-
while (n < rows.length && n < MAX_BATCH_RECORDS) {
|
|
247
|
-
const size = JSON.stringify(rows[n].event).length + 8;
|
|
248
|
-
if (n > 0 && bytes + size > MAX_BATCH_BYTES)
|
|
249
|
-
break;
|
|
250
|
-
bytes += size;
|
|
251
|
-
n += 1;
|
|
252
|
-
}
|
|
253
|
-
return rows.slice(0, n);
|
|
254
|
-
}
|