@nylorun/runtime 0.13.0-beta → 0.15.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 +434 -0
- package/README.md +137 -41
- package/dist/adapters/execution/restate.d.ts +7 -2
- package/dist/adapters/execution/restate.js +40 -10
- package/dist/adapters/replication/pgoutput.d.ts +21 -0
- package/dist/adapters/replication/pgoutput.js +170 -0
- package/dist/adapters/streams/s2.js +37 -20
- package/dist/admin-openapi.json +570 -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 +8065 -0
- package/dist/api/components.js +82 -0
- package/dist/api/gate/keys.d.ts +14 -0
- package/dist/api/gate/keys.js +59 -0
- package/dist/api/gate/routes.d.ts +34 -0
- package/dist/api/gate/routes.js +235 -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 +122 -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 +266 -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 +11 -0
- package/dist/api/http/routes/tenant.js +244 -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 +137 -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/gates/contract.d.ts +51 -0
- package/dist/gates/contract.js +37 -0
- package/dist/gates/handler.d.ts +21 -0
- package/dist/gates/handler.js +55 -0
- package/dist/gates/http-client.d.ts +12 -0
- package/dist/gates/http-client.js +160 -0
- package/dist/gates/in-process.d.ts +32 -0
- package/dist/gates/in-process.js +42 -0
- package/dist/gates/inflight.d.ts +39 -0
- package/dist/gates/inflight.js +70 -0
- package/dist/gates/mcp-handler.d.ts +35 -0
- package/dist/gates/mcp-handler.js +142 -0
- package/dist/gates/meter.d.ts +24 -0
- package/dist/gates/meter.js +165 -0
- package/dist/gates/model-gate.d.ts +45 -0
- package/dist/gates/model-gate.js +1 -0
- package/dist/gates/tenant-vaults.d.ts +64 -0
- package/dist/gates/tenant-vaults.js +94 -0
- package/dist/gates/tool-calls.d.ts +51 -0
- package/dist/gates/tool-calls.js +64 -0
- package/dist/gates/tool-client.d.ts +12 -0
- package/dist/gates/tool-client.js +201 -0
- package/dist/gates/tool-contract.d.ts +116 -0
- package/dist/gates/tool-contract.js +64 -0
- package/dist/gates/tool-gate.d.ts +45 -0
- package/dist/gates/tool-gate.js +10 -0
- package/dist/host/admin-api.d.ts +23 -0
- package/dist/host/admin-api.js +74 -0
- package/dist/host/app.d.ts +34 -0
- package/dist/host/app.js +181 -0
- package/dist/host/config-for.d.ts +3 -1
- package/dist/host/config-for.js +3 -2
- 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 +36 -8
- package/dist/host/create-host.js +161 -330
- package/dist/host/execution.d.ts +20 -18
- package/dist/host/execution.js +2 -2
- package/dist/host/gates.d.ts +58 -0
- package/dist/host/gates.js +127 -0
- package/dist/host/http.d.ts +20 -4
- package/dist/host/http.js +81 -23
- package/dist/host/index.d.ts +1 -1
- package/dist/host/main.js +189 -58
- package/dist/host/stack-config.d.ts +91 -19
- package/dist/host/stack-config.js +246 -17
- package/dist/infra/database.d.ts +2 -2
- package/dist/infra/execution.d.ts +5 -5
- package/dist/infra/execution.js +8 -6
- package/dist/infra/index.d.ts +1 -1
- package/dist/infra/workers.d.ts +17 -17
- package/dist/infra/workers.js +9 -9
- package/dist/keys/client.d.ts +9 -0
- package/dist/keys/client.js +70 -0
- package/dist/keys/contract.d.ts +24 -0
- package/dist/keys/contract.js +12 -0
- package/dist/keys/keys.d.ts +41 -0
- package/dist/keys/keys.js +50 -0
- package/dist/mcp/connect.d.ts +43 -3
- package/dist/mcp/connect.js +12 -5
- package/dist/mcp/pool.d.ts +39 -3
- package/dist/mcp/pool.js +74 -11
- 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 +25889 -0
- package/dist/record/append.d.ts +51 -0
- package/dist/record/append.js +20 -0
- package/dist/record/envelope.d.ts +19 -0
- package/dist/record/envelope.js +49 -0
- package/dist/record/index.d.ts +11 -0
- package/dist/record/index.js +11 -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/postgres/connect.d.ts +25 -9
- package/dist/store/postgres/connect.js +27 -10
- package/dist/store/postgres/db.d.ts +16 -0
- package/dist/store/postgres/db.js +28 -0
- package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
- package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
- package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
- package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
- package/dist/store/postgres/migrate.d.ts +68 -0
- package/dist/store/postgres/migrate.js +183 -0
- package/dist/store/postgres/record-writer.d.ts +4 -0
- package/dist/store/postgres/record-writer.js +34 -0
- package/dist/store/postgres/record.d.ts +5 -0
- package/dist/store/postgres/record.js +60 -0
- package/dist/store/postgres/schema.d.ts +3096 -0
- package/dist/store/postgres/schema.js +363 -0
- package/dist/store/postgres/store.d.ts +2 -4
- package/dist/store/postgres/store.js +590 -434
- package/dist/store/postgres/tenant.d.ts +56 -0
- package/dist/store/postgres/tenant.js +154 -0
- package/dist/store/types.d.ts +183 -132
- package/dist/store/types.js +0 -1
- 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 +33 -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/types.d.ts +78 -0
- package/dist/streams/relay/types.js +8 -0
- package/dist/streams/types.d.ts +71 -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.d.ts +7 -2
- package/dist/tenant/advance.js +125 -14
- 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/cause.d.ts +25 -0
- package/dist/tenant/cause.js +45 -0
- package/dist/tenant/commands.d.ts +18 -2
- package/dist/tenant/commands.js +111 -115
- package/dist/tenant/context.d.ts +101 -21
- package/dist/tenant/context.js +14 -10
- package/dist/tenant/delivery-token.d.ts +41 -0
- package/dist/tenant/delivery-token.js +104 -0
- package/dist/tenant/delivery.d.ts +62 -0
- package/dist/tenant/delivery.js +481 -0
- package/dist/tenant/effects.d.ts +20 -1
- package/dist/tenant/effects.js +159 -42
- package/dist/tenant/endpoints.d.ts +67 -0
- package/dist/tenant/endpoints.js +90 -0
- package/dist/tenant/ephemeral.d.ts +22 -7
- package/dist/tenant/ephemeral.js +83 -125
- package/dist/tenant/history.d.ts +64 -0
- package/dist/tenant/history.js +120 -0
- 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/model-setting.d.ts +2 -2
- package/dist/tenant/module.d.ts +17 -28
- package/dist/tenant/module.js +133 -279
- package/dist/tenant/outbound.d.ts +44 -0
- package/dist/tenant/outbound.js +127 -0
- package/dist/tenant/paths.d.ts +11 -16
- package/dist/tenant/paths.js +24 -68
- package/dist/tenant/pool.d.ts +1 -3
- package/dist/tenant/pool.js +1 -19
- package/dist/tenant/principals.d.ts +23 -11
- package/dist/tenant/principals.js +61 -49
- package/dist/tenant/reset.d.ts +0 -4
- package/dist/tenant/reset.js +0 -5
- package/dist/tenant/runtime.d.ts +28 -20
- package/dist/tenant/runtime.js +95 -64
- package/dist/tenant/scheduler.d.ts +10 -3
- package/dist/tenant/scheduler.js +9 -5
- package/dist/tenant/session-streams.d.ts +127 -0
- package/dist/tenant/session-streams.js +404 -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 +3 -3
- package/dist/tenant/status.js +20 -13
- package/dist/tenant/store-pg.d.ts +9 -6
- package/dist/tenant/store-pg.js +60 -125
- package/dist/tenant/streams.d.ts +62 -85
- package/dist/tenant/streams.js +143 -226
- 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 +180 -0
- package/dist/tenant/transcript.d.ts +48 -5
- package/dist/tenant/transcript.js +58 -1
- package/dist/tenant/types.d.ts +69 -98
- package/dist/tenant/types.js +4 -15
- package/dist/tenant/worker.d.ts +3 -1
- package/dist/tenant/worker.js +13 -7
- package/dist/vault/host-model.d.ts +31 -0
- package/dist/vault/host-model.js +57 -0
- package/dist/vault/service.d.ts +44 -17
- package/dist/vault/service.js +126 -121
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +25 -9
- package/dist/core/executors.d.ts +0 -44
- package/dist/core/executors.js +0 -95
- package/dist/store/memory.d.ts +0 -57
- package/dist/store/memory.js +0 -645
- package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
- package/dist/store/postgres/migrations/001_initial.js +0 -196
- package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
- package/dist/store/postgres/migrations/002_session_owner.js +0 -13
- package/dist/store/postgres/migrations/index.d.ts +0 -49
- package/dist/store/postgres/migrations/index.js +0 -76
- package/dist/store/postgres/names.d.ts +0 -11
- package/dist/store/postgres/names.js +0 -42
- package/dist/store/postgres/tenants.d.ts +0 -90
- package/dist/store/postgres/tenants.js +0 -214
- package/dist/streams/relay.d.ts +0 -37
- package/dist/streams/relay.js +0 -254
- package/dist/tenant/envelope.d.ts +0 -7
- package/dist/tenant/envelope.js +0 -22
- package/dist/tenant/live.d.ts +0 -92
- package/dist/tenant/live.js +0 -290
- package/dist/tenant/quarantine-error.d.ts +0 -6
- package/dist/tenant/quarantine-error.js +0 -9
- package/dist/tenant/quarantine.d.ts +0 -27
- package/dist/tenant/quarantine.js +0 -72
- 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/tenant/store-memory.d.ts +0 -16
- package/dist/tenant/store-memory.js +0 -111
- /package/dist/{store → record}/cursor.d.ts +0 -0
- /package/dist/{store → record}/cursor.js +0 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
The independent OSS **Runtime Host** consumes `@nylorun/harness/run` and
|
|
4
4
|
`@nylorun/core/contracts`. Cloud installs published `@nylorun/harness` from npm
|
|
5
5
|
and does not import this package. Client authoring and sessions belong to
|
|
6
|
-
`@nylorun/agents`;
|
|
6
|
+
`@nylorun/agents`; the Host's status and derived keys belong to `@nylorun/admin`.
|
|
7
7
|
Vocabulary: [src/CONTEXT.md](./src/CONTEXT.md).
|
|
8
8
|
|
|
9
9
|
Requires Node 24+. Build from the repository root:
|
|
@@ -24,33 +24,66 @@ package is a library with no bin; its Host entry is `@nylorun/runtime/server`
|
|
|
24
24
|
ephemeral embeds, use `startEphemeralRuntime()` from `@nylorun/runtime/core`.
|
|
25
25
|
See [MIGRATION.md](../MIGRATION.md).
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
Postgres
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
A Runtime serves one **Tenant**: an installation is one Runtime with its own
|
|
28
|
+
Postgres database, Restate and S2 basin, and two Tenants are two installations. No
|
|
29
|
+
Tenant state lives only in the Runtime process. The Tenant's data is its Postgres
|
|
30
|
+
database, in the fixed schemas `nylorun` (its state: the Session Store) and
|
|
31
|
+
`nylorun_streams` (the record of its session events). Drizzle defines the tables
|
|
32
|
+
(`src/store/postgres/schema.ts`) and generates the migrations, which ship in the package
|
|
33
|
+
(`dist/store/postgres/drizzle/`). At startup the Host applies the missing ones in one
|
|
34
|
+
transaction under an advisory lock, records them in `nylorun.__drizzle_migrations`, and
|
|
35
|
+
refuses a database holding a migration it does not ship (`schema-too-new`); then, on
|
|
36
|
+
first start, it creates the Tenant there: its id (`NYLORUN_TENANT_ID`, default a
|
|
37
|
+
new one), its name (`NYLORUN_TENANT_NAME`, default `default`), its Studio principal and
|
|
38
|
+
the derived principals (`NYLORUN_DERIVED_PRINCIPALS`, default `project`) whose keys the
|
|
39
|
+
admin key derives. A database written by a Runtime that kept several Tenants in one
|
|
40
|
+
database (`tenant_<id>` schemas), or by a pre-release build of one Tenant per database
|
|
41
|
+
(`schema_version` tables), is refused: this release starts fresh on a new database. Restate runs one advance of a session at a time and holds the Tenant's sweep
|
|
42
|
+
timer (Durable Session Execution); every session's events are relayed from the record to
|
|
43
|
+
its own S2 stream, which history and SSE read (Durable Streams). One image runs every **service**,
|
|
44
|
+
and `--service` picks what a process runs: `core` serves the Tenant API, Admin
|
|
45
|
+
API and SSE and runs the stream relay, `loop` runs advances (the Worker), and
|
|
46
|
+
`gates` is the Model Gate, the only process that reads a model credential and
|
|
47
|
+
calls providers. `core` and `loop` may share a process; `gates` never joins
|
|
48
|
+
them. The local stack packs `core,loop` into the `runtime` container and
|
|
49
|
+
`gates` into the `gateway` container (the combined packing). `--role
|
|
50
|
+
api|worker|all` is the deprecated name of `--service core`, `loop` and
|
|
51
|
+
`core,loop`.
|
|
34
52
|
|
|
35
53
|
The container is configured by its environment, which the stack's Compose file
|
|
36
54
|
sets: `NYLORUN_DATABASE_URL` (required), `NYLORUN_RESTATE_INGRESS_URL`,
|
|
37
55
|
`NYLORUN_RESTATE_ADMIN_URL`, `NYLORUN_WORKER_URL` and
|
|
38
56
|
`NYLORUN_RESTATE_IDENTITY_KEY` (Restate), `NYLORUN_S2_ENDPOINT` and
|
|
39
57
|
`NYLORUN_S2_TOKEN`, and in container mode `NYLORUN_LISTEN_HOST`,
|
|
40
|
-
`NYLORUN_LISTEN_PORT`, `NYLORUN_ALLOWED_HOSTS` and `NYLORUN_PUBLIC_URL
|
|
58
|
+
`NYLORUN_LISTEN_PORT`, `NYLORUN_ALLOWED_HOSTS` and `NYLORUN_PUBLIC_URL`, plus
|
|
59
|
+
`NYLORUN_BROWSER_ACCESS` (`on` or `off`) and the operator listener
|
|
60
|
+
(`NYLORUN_ADMIN_LISTEN_PORT`, `NYLORUN_ADMIN_LISTEN_HOST`,
|
|
61
|
+
`NYLORUN_ADMIN_ALLOWED_HOSTS`). A process that runs `loop` sends its model
|
|
62
|
+
calls to the gate at `NYLORUN_GATES_URL` with `NYLORUN_GATES_TOKEN`; in a
|
|
63
|
+
container it refuses to start without them. A `gates` process needs only
|
|
64
|
+
`NYLORUN_DATABASE_URL`, `NYLORUN_GATES_TOKEN`, its listener
|
|
65
|
+
(`NYLORUN_GATES_LISTEN_HOST`, `NYLORUN_GATES_LISTEN_PORT`, default 4100, and
|
|
66
|
+
`NYLORUN_GATES_ALLOWED_HOSTS`) and the Host's `tenant/` directory, which it
|
|
67
|
+
never writes; it serves the database's one Tenant. `NYLORUN_PACKING` (`combined` or `split`) is logged at startup.
|
|
68
|
+
|
|
69
|
+
With an operator listener (the stack's default: container port 4001), the Host
|
|
70
|
+
serves two ports. The public one serves the Tenant API, to browsers too when
|
|
71
|
+
browser access is on, and answers admin routes with the opaque `404`. The
|
|
72
|
+
operator one serves the Admin API, Host shutdown and the Tenant API, never to
|
|
73
|
+
browsers; keep it on loopback or a private network and never proxy it. Outside
|
|
74
|
+
a container, `adminPort` in `host.json` does the same on loopback.
|
|
41
75
|
|
|
42
76
|
The Host root is `NYLORUN_HOME` or `~/.nylorun` (bind-mounted at `/nylorun` in
|
|
43
|
-
the stack). What stays on the Host is under `
|
|
77
|
+
the stack). What stays on the Host is under `tenant/`.
|
|
44
78
|
|
|
45
79
|
## Layout
|
|
46
80
|
|
|
47
81
|
```text
|
|
48
82
|
<host root>/
|
|
49
|
-
host.json # format 1: hostId, host, port, runtimeVersion, …
|
|
83
|
+
host.json # format 1: hostId, host, port, adminPort?, runtimeVersion, …
|
|
50
84
|
host-credentials.json # adminKey (0600)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
trash/ # SQLite Tenants from before the Postgres switch
|
|
85
|
+
docker/ # compose.yaml, .env (0600), Restate identity key
|
|
86
|
+
tenant/ # vault-kek, plugin-data, logs, home, tmp, sandboxes
|
|
54
87
|
```
|
|
55
88
|
|
|
56
89
|
## HTTP surface
|
|
@@ -58,29 +91,90 @@ the stack). What stays on the Host is under `tenants/<tenantId>/`.
|
|
|
58
91
|
| Route | Auth | Notes |
|
|
59
92
|
| --- | --- | --- |
|
|
60
93
|
| `GET /health` | none | `service: "nylorun-runtime"`, `hostId`, protocol `{min,max,features}`, pid |
|
|
61
|
-
| `GET /ready` | none | Listener up,
|
|
62
|
-
| `GET /
|
|
63
|
-
|
|
|
94
|
+
| `GET /ready` | none | Listener up, the Tenant open, and Postgres, Restate and S2 answer (`checks`) |
|
|
95
|
+
| `GET /openapi.json` | none | The Tenant API's OpenAPI 3.2 document (below); refuses an `Origin` |
|
|
96
|
+
| `GET /v1/admin/status` | admin key | `AdminStatusSchema`: the Host, its protocol and its Tenant (`tenant`: id, name, `open` or `unavailable`, and the cause when it could not be opened); alias `GET /v1/admin/host`. On the operator listener when there is one |
|
|
64
97
|
| `POST /v1/admin/host/shutdown` | admin key | Host-private; not in `@nylorun/admin` |
|
|
65
|
-
|
|
|
98
|
+
| `GET /v1/admin/openapi.json` | admin key | The Admin API's OpenAPI 3.2 document |
|
|
99
|
+
| `/v1/*` Tenant routes | application key, subject token or delivery token | Require `Nylorun-Protocol`; nothing names the Tenant |
|
|
100
|
+
|
|
101
|
+
Every route checks `Host` first (`421 host_rejected`) and rejects non-JSON bodies
|
|
102
|
+
with `415 unsupported_media_type`. An `Origin` is `403 origin_rejected` on
|
|
103
|
+
`/health`, `/ready`, admin routes, and everywhere when browser access is off.
|
|
104
|
+
With browser access on (feature `browser-access`: the stack's default, or
|
|
105
|
+
`browserAccess` in `host.json`), the Host answers preflights for browser routes
|
|
106
|
+
from the route alone, and the Tenant admits an `Origin` only with a publishable
|
|
107
|
+
key (`Nylorun-Key`) that lists it, adding CORS headers only then; Tenant keys
|
|
108
|
+
and delivery tokens are refused from browsers before they are looked up. Missing or
|
|
109
|
+
unsupported protocol → `426` before authentication (the Host serves protocols 4 and 5).
|
|
110
|
+
Protocol 5 clients name no Tenant. A protocol 4 client's `Nylorun-Tenant` naming
|
|
111
|
+
another Tenant (or malformed), a publishable key of another Tenant, a Tenant that
|
|
112
|
+
could not be opened and rejected credentials → opaque `404` with identical body. A path or method no route serves is `404 Route not found`
|
|
113
|
+
once the caller is known.
|
|
114
|
+
|
|
115
|
+
## API reference (OpenAPI)
|
|
116
|
+
|
|
117
|
+
The Runtime describes its APIs as OpenAPI 3.2 documents, generated from the routes it serves,
|
|
118
|
+
so they cannot drift from its answers:
|
|
119
|
+
|
|
120
|
+
| Document | In the package | Served | On each release |
|
|
121
|
+
| --- | --- | --- | --- |
|
|
122
|
+
| Tenant API | `@nylorun/runtime/openapi.json` | `GET /openapi.json` | `openapi.json` |
|
|
123
|
+
| Admin API | `@nylorun/runtime/admin-openapi.json` | `GET /v1/admin/openapi.json` (admin key) | `admin-openapi.json` |
|
|
124
|
+
|
|
125
|
+
The release assets are on the `@nylorun/runtime@<version>` GitHub Release. Point any OpenAPI 3.2
|
|
126
|
+
tool at one of them; for [Scalar](https://scalar.com), the package file of a version works as is:
|
|
66
127
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
`
|
|
128
|
+
```text
|
|
129
|
+
https://cdn.jsdelivr.net/npm/@nylorun/runtime@<version>/dist/openapi.json
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Each operation's `security` says which credentials it takes (application key, subject token,
|
|
133
|
+
publishable key, delivery token, admin key), and its `x-nylorun-credentials`,
|
|
134
|
+
`x-nylorun-scopes` (the subject scopes that reach it) and `x-nylorun-browser` fields say who
|
|
135
|
+
may call it. Event streams are `text/event-stream` with an `itemSchema`. `runtime/openapi/` holds the committed snapshots: a change to a route changes
|
|
136
|
+
them (`node scripts/build-openapi.mjs --write`), and `check-package` fails until they are
|
|
137
|
+
updated.
|
|
138
|
+
|
|
139
|
+
## HTTP layer
|
|
140
|
+
|
|
141
|
+
Routes are Hono routes (`@hono/zod-openapi`), each declared once with who may call it
|
|
142
|
+
(`api/http/define.ts`): that declaration serves the route, checks subject scopes, answers
|
|
143
|
+
browser preflights and makes the OpenAPI document. `host/` holds the listeners, the `Host`
|
|
144
|
+
check (in Node, before Hono) and the Host pipeline (`host/app.ts`); `api/` the Tenant and
|
|
145
|
+
Admin routes (`api/http/routes/`, `api/ag-ui/`, `api/a2a/`, `host/admin-api.ts`). Only `host/`
|
|
146
|
+
and `api/` import Hono.
|
|
72
147
|
|
|
73
148
|
## Embedding and tests
|
|
74
149
|
|
|
75
150
|
Tests and ephemeral embeds use `startEphemeralRuntime()` from
|
|
76
151
|
`@nylorun/runtime/core`: private Host on port 0, temporary Host root, one Tenant,
|
|
77
|
-
returns `{ url, tenantId, applicationKey, adminKey, close() }`.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
152
|
+
returns `{ url, tenantId, applicationKey, adminKey, close() }`. Its Tenant is the
|
|
153
|
+
one Tenant of the Postgres database you pass as `database` (required): a pool, which
|
|
154
|
+
you end, or a URL, for which it opens a pool and ends it on `close()`. It creates the
|
|
155
|
+
Tenant there on first start, as a Host does (with `tenantId`, `name` and an application
|
|
156
|
+
principal for `applicationKey`), and serves the Tenant a database already holds. The
|
|
157
|
+
data stays after `close()`; give each test Tenant its own database and drop it
|
|
158
|
+
afterwards. Its
|
|
159
|
+
streams and scheduling are in process and gone after `close()`. The smoke checks do
|
|
160
|
+
not use it; they reset and seed the Tenant of a temporary Docker stack
|
|
161
|
+
(`scripts/lib/stack-tenant.mjs`).
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const runtime = await startEphemeralRuntime({
|
|
165
|
+
hostRoot, // a temporary directory
|
|
166
|
+
database: "postgres://nylorun:nylorun@127.0.0.1:55432/my_test_db",
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The package's own tests run every Session Store on Postgres: `npm test` gives each
|
|
171
|
+
test file a database on the Docker test stack (`test/stack/compose.yaml`) and starts
|
|
172
|
+
the stack when it is down; see [CONTRIBUTING.md](../CONTRIBUTING.md).
|
|
81
173
|
|
|
82
|
-
Register
|
|
83
|
-
(
|
|
174
|
+
Register Action endpoints with `PUT /v1/endpoints` using the application principal
|
|
175
|
+
(`createActionHandler(...).register({ url })` in `@nylorun/agents` does this). The
|
|
176
|
+
Runtime delivers each Action there with an `http` or `https` POST, so the URL must be
|
|
177
|
+
reachable from the Runtime. Model gateway
|
|
84
178
|
and sandbox backend are Tenant configuration (vault / seed), not Host process
|
|
85
179
|
env.
|
|
86
180
|
|
|
@@ -88,12 +182,12 @@ env.
|
|
|
88
182
|
|
|
89
183
|
The session-first `/v1` API is specified in
|
|
90
184
|
[HOST_CONTRACT.md](../harness/HOST_CONTRACT.md). Application tokens authorize
|
|
91
|
-
definition/session APIs
|
|
92
|
-
|
|
93
|
-
|
|
185
|
+
definition/session APIs. The Runtime delivers Actions to Action endpoints, and a
|
|
186
|
+
delivery token authorizes only its own Action's heartbeat, result and sandbox calls.
|
|
187
|
+
Session observers cannot submit action results.
|
|
94
188
|
|
|
95
189
|
Postgres transactions persist session checkpoints, command receipts, individual
|
|
96
|
-
effects/actions, waits and the
|
|
190
|
+
effects/actions, waits and the record of session events — in the Tenant's database; history is
|
|
97
191
|
read from Durable Streams (S2). An advance owns its session through a lease with
|
|
98
192
|
an epoch; a Worker that takes over after a crash marks in-flight effects
|
|
99
193
|
`uncertain`. Vault ciphertext needs that Tenant's own KEK.
|
|
@@ -103,22 +197,24 @@ Tenant owns each sandbox; backend names are prefixed `nylorun-<tenant-id>-`.
|
|
|
103
197
|
|
|
104
198
|
## Local Project workflow
|
|
105
199
|
|
|
106
|
-
A project depends on `@nylorun/agents` only. `npx nylorun
|
|
107
|
-
stack (Docker)
|
|
108
|
-
|
|
109
|
-
`
|
|
200
|
+
A project depends on `@nylorun/agents` only. `npx nylorun start` in the project
|
|
201
|
+
runs its local stack (Docker), whose Runtime creates the stack's one Tenant, and
|
|
202
|
+
writes the Project link; the project's `npm run dev` runs `src/main.ts` under
|
|
203
|
+
`tsx watch`; `npx nylorun studio` opens Studio on that Tenant.
|
|
110
204
|
|
|
111
205
|
## Troubleshooting
|
|
112
206
|
|
|
113
207
|
| Symptom | What to do |
|
|
114
208
|
| --- | --- |
|
|
115
209
|
| `kek-missing` | Restore `vault-kek` in the Tenant directory |
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
210
|
+
| `GET /ready` is 503 and `/v1/admin/status` names a `tenant.cause` | The Tenant could not be opened; every Tenant request is the opaque `404`. Follow the cause's `repair`, then restart the Runtime |
|
|
211
|
+
| `corrupt` / `migration-failed` / `envelope-invalid` / `open-failed` / `open-timeout` | Follow the cause's `repair` string |
|
|
212
|
+
| `schema-too-new` | Run a Runtime at least as new as the one that migrated the database |
|
|
213
|
+
| `database-layout-old` | The database holds `tenant_<id>` schemas of an older Runtime, or the `schema_version` tables of a pre-release one: point the Runtime at a new database (a new stack); the old one is left as it is |
|
|
118
214
|
| `426 protocol_unsupported` | Upgrade clients or Host to a compatible set |
|
|
119
|
-
| `421 host_rejected` / `403 origin_rejected` |
|
|
215
|
+
| `421 host_rejected` / `403 origin_rejected` | In a container, list the `Host` in `NYLORUN_ALLOWED_HOSTS`. From a browser, use a subject token and a publishable key that lists the page's origin, never a Tenant key |
|
|
120
216
|
| `503` for a Tenant | Postgres or Restate is unreachable; `GET /ready` names which |
|
|
121
|
-
| Port in use | Change `NYLORUN_PORT` in
|
|
217
|
+
| Port in use | Change `NYLORUN_PORT` in `~/.nylorun/stacks/<name>/docker/.env` and run `nylorun start` |
|
|
122
218
|
| Logs | `nylorun logs runtime` |
|
|
123
219
|
|
|
124
220
|
Definitions have no `agent.run()`; applications use `@nylorun/agents`.
|
|
@@ -90,8 +90,10 @@ export declare class RestateExecution implements DurableExecution {
|
|
|
90
90
|
session: string;
|
|
91
91
|
tenant: string;
|
|
92
92
|
timer: string;
|
|
93
|
+
action: string;
|
|
93
94
|
};
|
|
94
95
|
wake(tenantId: string, sessionId: string, wake: Wake): Promise<void>;
|
|
96
|
+
deliver(tenantId: string, actionId: string): Promise<void>;
|
|
95
97
|
timer(tenantId: string, key: string, at: Date): Promise<void>;
|
|
96
98
|
armSweep(tenantId: string): Promise<void>;
|
|
97
99
|
disarmSweep(tenantId: string): Promise<void>;
|
|
@@ -106,8 +108,11 @@ export declare class RestateExecution implements DurableExecution {
|
|
|
106
108
|
start(handlers: WorkerHandlers): Promise<void>;
|
|
107
109
|
stop(): Promise<void>;
|
|
108
110
|
private definitions;
|
|
109
|
-
/**
|
|
110
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Runs a Worker handler with a signal aborted by `stop` or by the end of the Restate
|
|
113
|
+
* attempt (`advance` and `deliver`).
|
|
114
|
+
*/
|
|
115
|
+
private runAborted;
|
|
111
116
|
/** Runs a Worker handler so that `stop` waits for it. Refuses once stopping; Restate retries. */
|
|
112
117
|
private runTracked;
|
|
113
118
|
private staleSweepMs;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Durable Session Execution on Restate (architecture §12.3).
|
|
3
3
|
*
|
|
4
|
-
* The only module that imports the Restate SDK. It serves
|
|
4
|
+
* The only module that imports the Restate SDK. It serves four virtual
|
|
5
5
|
* objects on the Worker endpoint, all named with `servicePrefix`:
|
|
6
6
|
*
|
|
7
7
|
* - `<prefix>NylorunSession`, key `<tenantId>:<sessionId>`. Its exclusive
|
|
@@ -12,14 +12,19 @@
|
|
|
12
12
|
* - `<prefix>NylorunTenant`, key `<tenantId>`. `arm`, `sweep` and `disarm`
|
|
13
13
|
* keep one self-re-arming sweep chain per Tenant. The chain's generation is
|
|
14
14
|
* kept in the object's state, so arming is idempotent and a disarmed or
|
|
15
|
-
* replaced chain dies at its next link. The state is rebuildable:
|
|
16
|
-
* re-
|
|
15
|
+
* replaced chain dies at its next link. The state is rebuildable: a Host
|
|
16
|
+
* re-arms its Tenant's sweep when it opens it, at every start (§14.8).
|
|
17
17
|
* - `<prefix>NylorunTimer`, key `<tenantId>:<timer key>`. `set` records the
|
|
18
18
|
* latest time and sends a delayed `fire`; a `fire` for an older time is a
|
|
19
19
|
* no-op, which is how setting a key again replaces it.
|
|
20
|
+
* - `<prefix>NylorunAction`, key `<tenantId>:<actionId>`. Its exclusive
|
|
21
|
+
* `deliver` handler calls `WorkerHandlers.deliver`. Like `advance`, it keeps
|
|
22
|
+
* no state and makes no `ctx.run`: its only journal entry is the delayed
|
|
23
|
+
* self-send after a `retry` result, so an endpoint that is down never spends
|
|
24
|
+
* the retry budget, which is for infrastructure errors.
|
|
20
25
|
*
|
|
21
|
-
* Wakes, timers and sweep arming go through the ingress as one-way
|
|
22
|
-
* an API node can call them without ever calling `start`.
|
|
26
|
+
* Wakes, deliveries, timers and sweep arming go through the ingress as one-way
|
|
27
|
+
* sends, so an API node can call them without ever calling `start`.
|
|
23
28
|
*/
|
|
24
29
|
import { createServer } from "node:http2";
|
|
25
30
|
import * as restate from "@restatedev/restate-sdk";
|
|
@@ -49,6 +54,7 @@ export class RestateExecution {
|
|
|
49
54
|
session: `${prefix}NylorunSession`,
|
|
50
55
|
tenant: `${prefix}NylorunTenant`,
|
|
51
56
|
timer: `${prefix}NylorunTimer`,
|
|
57
|
+
action: `${prefix}NylorunAction`,
|
|
52
58
|
};
|
|
53
59
|
this.ingressUrl = options.ingressUrl.replace(/\/+$/, "");
|
|
54
60
|
this.adminUrl = options.adminUrl.replace(/\/+$/, "");
|
|
@@ -63,6 +69,9 @@ export class RestateExecution {
|
|
|
63
69
|
throw new Error(`Unknown wake reason: ${String(wake.reason)}`);
|
|
64
70
|
await this.send(this.names.session, sessionKey(tenantId, sessionId), "advance", { reason: wake.reason }, wake.dedupeKey);
|
|
65
71
|
}
|
|
72
|
+
async deliver(tenantId, actionId) {
|
|
73
|
+
await this.send(this.names.action, sessionKey(tenantId, actionId), "deliver", {});
|
|
74
|
+
}
|
|
66
75
|
async timer(tenantId, key, at) {
|
|
67
76
|
await this.send(this.names.timer, sessionKey(tenantId, key), "set", {
|
|
68
77
|
at: at.getTime(),
|
|
@@ -161,7 +170,7 @@ export class RestateExecution {
|
|
|
161
170
|
handlers: {
|
|
162
171
|
advance: async (ctx, _wake) => {
|
|
163
172
|
const { tenantId, sessionId } = parseSessionKey(ctx.key);
|
|
164
|
-
const result = await this.
|
|
173
|
+
const result = await this.runAborted(ctx.request().attemptCompletedSignal, (handlers, signal) => handlers.advance(tenantId, sessionId, signal));
|
|
165
174
|
if (result.status === "busy")
|
|
166
175
|
ctx
|
|
167
176
|
.objectSendClient({ name: names.session }, ctx.key)
|
|
@@ -232,10 +241,31 @@ export class RestateExecution {
|
|
|
232
241
|
},
|
|
233
242
|
options: serviceOptions,
|
|
234
243
|
});
|
|
235
|
-
|
|
244
|
+
const action = restate.object({
|
|
245
|
+
name: names.action,
|
|
246
|
+
handlers: {
|
|
247
|
+
deliver: async (ctx, _input) => {
|
|
248
|
+
const { tenantId, sessionId: actionId } = parseSessionKey(ctx.key);
|
|
249
|
+
const result = await this.runAborted(ctx.request().attemptCompletedSignal, (handlers, signal) => {
|
|
250
|
+
if (!handlers.deliver)
|
|
251
|
+
throw new Error("WorkerHandlers.deliver is required for deliveries");
|
|
252
|
+
return handlers.deliver(tenantId, actionId, signal);
|
|
253
|
+
});
|
|
254
|
+
if (result.status === "retry")
|
|
255
|
+
ctx
|
|
256
|
+
.objectSendClient({ name: names.action }, ctx.key)
|
|
257
|
+
.deliver({}, restate.rpc.sendOpts({ delay: Math.max(0, result.retryAfterMs) }));
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
options: serviceOptions,
|
|
261
|
+
});
|
|
262
|
+
return [session, tenant, timer, action];
|
|
236
263
|
}
|
|
237
|
-
/**
|
|
238
|
-
|
|
264
|
+
/**
|
|
265
|
+
* Runs a Worker handler with a signal aborted by `stop` or by the end of the Restate
|
|
266
|
+
* attempt (`advance` and `deliver`).
|
|
267
|
+
*/
|
|
268
|
+
async runAborted(attemptDone, fn) {
|
|
239
269
|
const controller = new AbortController();
|
|
240
270
|
const onAttemptDone = () => controller.abort(new Error("Restate attempt ended"));
|
|
241
271
|
if (attemptDone.aborted)
|
|
@@ -335,7 +365,7 @@ export async function listStuckInvocations(options) {
|
|
|
335
365
|
const prefix = options.servicePrefix ?? "";
|
|
336
366
|
if (!/^[A-Za-z0-9_]*$/.test(prefix))
|
|
337
367
|
throw new Error(`Invalid Restate service prefix: ${prefix}`);
|
|
338
|
-
const services = ["NylorunSession", "NylorunTenant", "NylorunTimer"].map((name) => `'${prefix}${name}'`);
|
|
368
|
+
const services = ["NylorunSession", "NylorunTenant", "NylorunTimer", "NylorunAction"].map((name) => `'${prefix}${name}'`);
|
|
339
369
|
const clauses = [
|
|
340
370
|
`target_service_name IN (${services.join(", ")})`,
|
|
341
371
|
`status IN ('paused', 'backing-off')`,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ChangeSource } from "../../streams/relay/types.js";
|
|
2
|
+
export interface PgoutputSourceOptions {
|
|
3
|
+
/** A Postgres URL whose role may replicate (`REPLICATION`) and read `nylorun_streams`. */
|
|
4
|
+
connectionString: string;
|
|
5
|
+
/**
|
|
6
|
+
* The Tenant the database holds. The record has no Tenant column (one Tenant per
|
|
7
|
+
* database), so every row read is this Tenant's.
|
|
8
|
+
*/
|
|
9
|
+
tenantId: string;
|
|
10
|
+
/** Default `nylorun_stream_relay`. */
|
|
11
|
+
slot?: string;
|
|
12
|
+
/** Default `nylorun_stream_relay`. */
|
|
13
|
+
publication?: string;
|
|
14
|
+
/** The schema of the record tables. Default `nylorun_streams`. */
|
|
15
|
+
schema?: string;
|
|
16
|
+
/** Wait between attempts to take the slot. Default 5 s. */
|
|
17
|
+
retryMs?: number;
|
|
18
|
+
log?: (message: string, fields?: Record<string, unknown>) => void;
|
|
19
|
+
}
|
|
20
|
+
export declare const DEFAULT_RELAY_SLOT = "nylorun_stream_relay";
|
|
21
|
+
export declare function createPgoutputSource(options: PgoutputSourceOptions): ChangeSource;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stream relay's change source on Postgres logical replication (Durable Streams §7.1,
|
|
3
|
+
* §7.4, §7.6): a persistent `pgoutput` slot on the `nylorun_stream_relay` publication, read
|
|
4
|
+
* with `pg-logical-replication`. The only file importing it (with `pg`, which it needs).
|
|
5
|
+
*
|
|
6
|
+
* - **One reader per slot.** Postgres lets one connection hold a slot, so every Runtime
|
|
7
|
+
* process can run a source and exactly one is active; the others retry every few seconds
|
|
8
|
+
* and take over when it goes away. The slot is the leader election.
|
|
9
|
+
* - **Acknowledged only on request.** The library acknowledges the last position it
|
|
10
|
+
* *received* on a timer; that is turned off (`timeoutSeconds: 0`) and keepalives are
|
|
11
|
+
* answered by the relay, which acknowledges only what S2 has.
|
|
12
|
+
* - **Always from the confirmed position.** Each attempt is a new connection that starts at
|
|
13
|
+
* `0/0`, which Postgres reads as the slot's confirmed position, so a reconnect replays
|
|
14
|
+
* whatever was received but not acknowledged.
|
|
15
|
+
* - **Fresh slots.** A missing slot is created; a slot Postgres invalidated
|
|
16
|
+
* (`max_slot_wal_keep_size`) is dropped and created again. Either way the relay must
|
|
17
|
+
* reconcile the record with S2; that is recorded in `relay_slots` until `reconciled()`, so a
|
|
18
|
+
* crash during reconciliation, or another process winning the slot, does not lose it.
|
|
19
|
+
*/
|
|
20
|
+
import pg from "pg";
|
|
21
|
+
import { LogicalReplicationService, PgoutputPlugin } from "pg-logical-replication";
|
|
22
|
+
export const DEFAULT_RELAY_SLOT = "nylorun_stream_relay";
|
|
23
|
+
export function createPgoutputSource(options) {
|
|
24
|
+
const slot = options.slot ?? DEFAULT_RELAY_SLOT;
|
|
25
|
+
const publication = options.publication ?? "nylorun_stream_relay";
|
|
26
|
+
const schema = options.schema ?? "nylorun_streams";
|
|
27
|
+
const retryMs = options.retryMs ?? 5000;
|
|
28
|
+
const { tenantId } = options;
|
|
29
|
+
const log = options.log ?? (() => { });
|
|
30
|
+
const pool = new pg.Pool({
|
|
31
|
+
connectionString: options.connectionString,
|
|
32
|
+
max: 2,
|
|
33
|
+
application_name: "nylorun-stream-relay",
|
|
34
|
+
});
|
|
35
|
+
let service;
|
|
36
|
+
let stopped = false;
|
|
37
|
+
let running;
|
|
38
|
+
let wake;
|
|
39
|
+
/** Creates the slot when missing or lost; true when the record must be reconciled. */
|
|
40
|
+
async function prepareSlot() {
|
|
41
|
+
const client = await pool.connect();
|
|
42
|
+
try {
|
|
43
|
+
const { rows } = await client.query("SELECT wal_status, active FROM pg_replication_slots WHERE slot_name = $1", [slot]);
|
|
44
|
+
const existing = rows[0];
|
|
45
|
+
if (existing?.wal_status === "lost" && !existing.active) {
|
|
46
|
+
log("replication slot was invalidated; creating it again", { slot });
|
|
47
|
+
await client.query("SELECT pg_drop_replication_slot($1)", [slot]);
|
|
48
|
+
}
|
|
49
|
+
if (!existing || existing.wal_status === "lost") {
|
|
50
|
+
try {
|
|
51
|
+
await client.query("SELECT pg_create_logical_replication_slot($1, 'pgoutput')", [slot]);
|
|
52
|
+
log("replication slot created", { slot });
|
|
53
|
+
await client.query(`INSERT INTO ${schema}.relay_slots (slot_name, reconcile_pending) VALUES ($1, true)
|
|
54
|
+
ON CONFLICT (slot_name) DO UPDATE SET reconcile_pending = true`, [slot]);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
// Another process created it first; its row says whether to reconcile.
|
|
58
|
+
if (error.code !== "42710")
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const pending = await client.query(`SELECT reconcile_pending FROM ${schema}.relay_slots WHERE slot_name = $1`, [slot]);
|
|
63
|
+
// A slot created before this table knew it: reconcile once to be sure.
|
|
64
|
+
return pending.rows[0]?.reconcile_pending ?? true;
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
client.release();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function attempt(handlers) {
|
|
71
|
+
const fresh = await prepareSlot();
|
|
72
|
+
// `stop()` during `prepareSlot` had no service to stop: do not start one it cannot.
|
|
73
|
+
if (stopped)
|
|
74
|
+
return;
|
|
75
|
+
const current = new LogicalReplicationService({ connectionString: options.connectionString, application_name: "nylorun-stream-relay" }, { acknowledge: { auto: false, timeoutSeconds: 0 }, flowControl: { enabled: false } });
|
|
76
|
+
service = current;
|
|
77
|
+
let decoding;
|
|
78
|
+
current.on("start", () => handlers.onActive({ fresh }));
|
|
79
|
+
current.on("data", (_lsn, message) => {
|
|
80
|
+
switch (message.tag) {
|
|
81
|
+
case "begin":
|
|
82
|
+
decoding = { rows: [] };
|
|
83
|
+
break;
|
|
84
|
+
case "insert":
|
|
85
|
+
if (decoding &&
|
|
86
|
+
message.relation?.schema === schema &&
|
|
87
|
+
message.relation?.name === "session_events")
|
|
88
|
+
decoding.rows.push(rowOf(message.new, tenantId));
|
|
89
|
+
break;
|
|
90
|
+
case "commit": {
|
|
91
|
+
const tx = { endLsn: message.commitEndLsn, rows: decoding?.rows ?? [] };
|
|
92
|
+
decoding = undefined;
|
|
93
|
+
// A transaction with no record rows still moves the slot once acknowledged.
|
|
94
|
+
handlers.onTx(tx);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
current.on("heartbeat", (lsn, _timestamp, shouldRespond) => {
|
|
100
|
+
// Mid-transaction, the server's position may be past rows not yet delivered.
|
|
101
|
+
if (!shouldRespond || decoding)
|
|
102
|
+
return;
|
|
103
|
+
const position = handlers.onKeepalive(lsn);
|
|
104
|
+
if (position)
|
|
105
|
+
void current.acknowledge(position).catch(() => { });
|
|
106
|
+
});
|
|
107
|
+
current.on("error", () => { });
|
|
108
|
+
const plugin = new PgoutputPlugin({ protoVersion: 1, publicationNames: [publication] });
|
|
109
|
+
// Resolves when replication ends, rejects when it fails; `0/0` = the slot's confirmed position.
|
|
110
|
+
await current.subscribe(plugin, slot, "0/00000000");
|
|
111
|
+
}
|
|
112
|
+
async function run(handlers) {
|
|
113
|
+
while (!stopped) {
|
|
114
|
+
let failure;
|
|
115
|
+
try {
|
|
116
|
+
await attempt(handlers);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
failure = error;
|
|
120
|
+
}
|
|
121
|
+
if (stopped)
|
|
122
|
+
break;
|
|
123
|
+
const busy = failure?.code === "55006";
|
|
124
|
+
// Another process holds the slot: it is the relay. Anything else: report and retry.
|
|
125
|
+
handlers.onInactive(busy ? undefined : failure ?? new Error("replication ended"));
|
|
126
|
+
await new Promise((resolve) => {
|
|
127
|
+
wake = resolve;
|
|
128
|
+
setTimeout(resolve, retryMs).unref?.();
|
|
129
|
+
});
|
|
130
|
+
wake = undefined;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
start(handlers) {
|
|
135
|
+
if (running)
|
|
136
|
+
throw new Error("The change source is already started");
|
|
137
|
+
running = run(handlers);
|
|
138
|
+
},
|
|
139
|
+
acknowledge(lsn) {
|
|
140
|
+
void service?.acknowledge(lsn).catch(() => { });
|
|
141
|
+
},
|
|
142
|
+
async reconciled() {
|
|
143
|
+
await pool.query(`UPDATE ${schema}.relay_slots SET reconcile_pending = false WHERE slot_name = $1`, [
|
|
144
|
+
slot,
|
|
145
|
+
]);
|
|
146
|
+
},
|
|
147
|
+
async lag() {
|
|
148
|
+
const { rows } = await pool.query(`SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)::text AS lag
|
|
149
|
+
FROM pg_replication_slots WHERE slot_name = $1`, [slot]);
|
|
150
|
+
return rows[0]?.lag == null ? undefined : Number(rows[0].lag);
|
|
151
|
+
},
|
|
152
|
+
async stop() {
|
|
153
|
+
stopped = true;
|
|
154
|
+
wake?.();
|
|
155
|
+
await service?.stop().catch(() => undefined);
|
|
156
|
+
await running?.catch(() => undefined);
|
|
157
|
+
await pool.end().catch(() => undefined);
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/** A decoded `session_events` row (`pg` parses `json`; `bigint` arrives as text). */
|
|
162
|
+
function rowOf(values, tenantId) {
|
|
163
|
+
return {
|
|
164
|
+
tenantId,
|
|
165
|
+
sessionId: values.session_id,
|
|
166
|
+
seq: Number(values.seq),
|
|
167
|
+
generation: Number(values.generation),
|
|
168
|
+
body: typeof values.body === "string" ? JSON.parse(values.body) : values.body,
|
|
169
|
+
};
|
|
170
|
+
}
|