@electric-ax/agents-server 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entrypoint.js +529 -248
- package/dist/index.cjs +1603 -1332
- package/dist/index.d.cts +274 -162
- package/dist/index.d.ts +274 -162
- package/dist/index.js +1601 -1332
- package/drizzle/0007_runner_diagnostics_and_principal.sql +22 -0
- package/drizzle/0008_runner_runtime_diagnostics.sql +50 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +6 -6
- package/src/db/schema.ts +33 -10
- package/src/electric-agents-types.ts +49 -3
- package/src/entity-registry.ts +136 -26
- package/src/host.ts +4 -5
- package/src/index.ts +5 -1
- package/src/principal.ts +23 -11
- package/src/routing/context.ts +1 -0
- package/src/routing/dispatch-policy.ts +123 -20
- package/src/routing/durable-streams-router.ts +286 -116
- package/src/routing/durable-streams-routing-adapter.ts +31 -64
- package/src/routing/electric-proxy-router.ts +1 -0
- package/src/routing/entities-router.ts +5 -5
- package/src/routing/hooks.ts +8 -1
- package/src/routing/internal-router.ts +6 -2
- package/src/routing/runners-router.ts +257 -19
- package/src/runtime.ts +4 -5
- package/src/server.ts +21 -15
- package/src/standalone-runtime.ts +4 -5
- package/src/stream-client.ts +18 -69
- package/src/utils/server-utils.ts +27 -8
package/src/server.ts
CHANGED
|
@@ -7,10 +7,9 @@ import {
|
|
|
7
7
|
createRuntimeHandler,
|
|
8
8
|
} from '@electric-ax/agents-runtime'
|
|
9
9
|
import { createDb, runMigrations } from './db/index.js'
|
|
10
|
-
import { pathPrefixedSingleTenantDurableStreamsRoutingAdapter } from './routing/durable-streams-routing-adapter.js'
|
|
11
10
|
import { ossServerRouter } from './routing/oss-server-router.js'
|
|
12
11
|
import { startStandaloneAgentsRuntime } from './standalone-runtime.js'
|
|
13
|
-
import { StreamClient
|
|
12
|
+
import { StreamClient } from './stream-client.js'
|
|
14
13
|
import { DEFAULT_TENANT_ID } from './tenant.js'
|
|
15
14
|
import { getDevPrincipal, getPrincipalFromRequest } from './principal.js'
|
|
16
15
|
import { apiError } from './electric-agents-http.js'
|
|
@@ -121,6 +120,16 @@ function createMockAgentBootstrap(options: {
|
|
|
121
120
|
return { runtime, registry }
|
|
122
121
|
}
|
|
123
122
|
|
|
123
|
+
function durableStreamTestServerBackendUrl(origin: string): string {
|
|
124
|
+
// DurableStreamTestServer.start() returns the HTTP origin, while the
|
|
125
|
+
// reference server's stream backend is mounted under /v1/stream.
|
|
126
|
+
// User-provided durableStreamsUrl values are already backend prefixes and
|
|
127
|
+
// are passed through unchanged.
|
|
128
|
+
const url = new URL(origin)
|
|
129
|
+
url.pathname = `${url.pathname.replace(/\/+$/, ``)}/v1/stream`
|
|
130
|
+
return url.toString().replace(/\/+$/, ``)
|
|
131
|
+
}
|
|
132
|
+
|
|
124
133
|
export class ElectricAgentsServer {
|
|
125
134
|
private server?: Server
|
|
126
135
|
private electricAgentsManager?: StartedStandaloneAgentsRuntime[`manager`]
|
|
@@ -144,10 +153,9 @@ export class ElectricAgentsServer {
|
|
|
144
153
|
}
|
|
145
154
|
this.options = options
|
|
146
155
|
this.streamClient = options.durableStreamsUrl
|
|
147
|
-
? new StreamClient(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
)
|
|
156
|
+
? new StreamClient(options.durableStreamsUrl, {
|
|
157
|
+
bearer: options.durableStreamsBearer,
|
|
158
|
+
})
|
|
151
159
|
: null!
|
|
152
160
|
}
|
|
153
161
|
|
|
@@ -184,11 +192,11 @@ export class ElectricAgentsServer {
|
|
|
184
192
|
serverLog.info(
|
|
185
193
|
`[agent-server] durable streams server started at ${streamsUrl}`
|
|
186
194
|
)
|
|
187
|
-
this.options.durableStreamsUrl =
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
)
|
|
195
|
+
this.options.durableStreamsUrl =
|
|
196
|
+
durableStreamTestServerBackendUrl(streamsUrl)
|
|
197
|
+
this.streamClient = new StreamClient(this.options.durableStreamsUrl, {
|
|
198
|
+
bearer: this.options.durableStreamsBearer,
|
|
199
|
+
})
|
|
192
200
|
}
|
|
193
201
|
|
|
194
202
|
this.streamsAgent = new Agent({
|
|
@@ -401,11 +409,9 @@ export class ElectricAgentsServer {
|
|
|
401
409
|
principal,
|
|
402
410
|
publicUrl: this.publicUrl,
|
|
403
411
|
localUrl: this._url,
|
|
404
|
-
durableStreamsUrl: this.options.durableStreamsUrl
|
|
412
|
+
durableStreamsUrl: this.options.durableStreamsUrl!,
|
|
405
413
|
durableStreamsBearer: this.options.durableStreamsBearer,
|
|
406
|
-
durableStreamsRouting:
|
|
407
|
-
this.options.durableStreamsRouting ??
|
|
408
|
-
pathPrefixedSingleTenantDurableStreamsRoutingAdapter,
|
|
414
|
+
durableStreamsRouting: this.options.durableStreamsRouting,
|
|
409
415
|
durableStreamsDispatcher: this.streamsAgent,
|
|
410
416
|
electricUrl: this.options.electricUrl,
|
|
411
417
|
electricSecret: this.options.electricSecret,
|
|
@@ -4,7 +4,7 @@ import { EntityBridgeManager } from './entity-bridge-manager.js'
|
|
|
4
4
|
import { serverLog } from './utils/log.js'
|
|
5
5
|
import { ElectricAgentsTenantRuntime } from './runtime.js'
|
|
6
6
|
import { Scheduler } from './scheduler.js'
|
|
7
|
-
import { StreamClient
|
|
7
|
+
import { StreamClient } from './stream-client.js'
|
|
8
8
|
import { TagStreamOutboxDrainer } from './tag-stream-outbox-drainer.js'
|
|
9
9
|
import { DEFAULT_TENANT_ID } from './tenant.js'
|
|
10
10
|
import { WakeRegistry } from './wake-registry.js'
|
|
@@ -57,10 +57,9 @@ export async function startStandaloneAgentsRuntime(
|
|
|
57
57
|
const streamClient =
|
|
58
58
|
options.streamClient ??
|
|
59
59
|
(options.durableStreamsUrl
|
|
60
|
-
? new StreamClient(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
)
|
|
60
|
+
? new StreamClient(options.durableStreamsUrl, {
|
|
61
|
+
bearer: options.durableStreamsBearer,
|
|
62
|
+
})
|
|
64
63
|
: undefined)
|
|
65
64
|
if (!streamClient) {
|
|
66
65
|
throw new Error(`Either durableStreamsUrl or streamClient is required`)
|
package/src/stream-client.ts
CHANGED
|
@@ -32,15 +32,6 @@ export interface WaitForMessagesResult {
|
|
|
32
32
|
timedOut: boolean
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export interface ConsumerStateResponse {
|
|
36
|
-
state: string
|
|
37
|
-
wake_id?: string | null
|
|
38
|
-
webhook?: {
|
|
39
|
-
wake_id?: string | null
|
|
40
|
-
subscription_id?: string
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
35
|
export interface SubscriptionStreamInfo {
|
|
45
36
|
path: string
|
|
46
37
|
tail_offset?: string
|
|
@@ -129,6 +120,16 @@ export async function applyDurableStreamsBearer(
|
|
|
129
120
|
}
|
|
130
121
|
}
|
|
131
122
|
|
|
123
|
+
function appendPathToBaseUrl(baseUrl: string, path: string): string {
|
|
124
|
+
const url = new URL(baseUrl)
|
|
125
|
+
const basePath = url.pathname.replace(/\/+$/, ``)
|
|
126
|
+
const childPath = path.replace(/^\/+/, ``)
|
|
127
|
+
url.pathname = childPath
|
|
128
|
+
? `${basePath === `/` ? `` : basePath}/${childPath}`
|
|
129
|
+
: basePath || `/`
|
|
130
|
+
return url.toString().replace(/\/+$/, ``)
|
|
131
|
+
}
|
|
132
|
+
|
|
132
133
|
function durableStreamsBearerHeaders(
|
|
133
134
|
bearer: DurableStreamsBearerProvider | undefined
|
|
134
135
|
): HeadersRecord | undefined {
|
|
@@ -139,18 +140,6 @@ function durableStreamsBearerHeaders(
|
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
export function durableStreamsServiceUrl(
|
|
143
|
-
baseUrl: string,
|
|
144
|
-
serviceId: string
|
|
145
|
-
): string {
|
|
146
|
-
const url = new URL(baseUrl)
|
|
147
|
-
if (/^\/v1\/stream\/[^/]+\/?$/.test(url.pathname)) {
|
|
148
|
-
return baseUrl.replace(/\/+$/, ``)
|
|
149
|
-
}
|
|
150
|
-
const base = baseUrl.replace(/\/+$/, ``)
|
|
151
|
-
return `${base}/v1/stream/${encodeURIComponent(serviceId)}`
|
|
152
|
-
}
|
|
153
|
-
|
|
154
143
|
function isNotFoundError(err: unknown): boolean {
|
|
155
144
|
return (
|
|
156
145
|
(err instanceof DurableStreamError && err.code === ErrCodeNotFound) ||
|
|
@@ -184,7 +173,7 @@ export class StreamClient {
|
|
|
184
173
|
) {}
|
|
185
174
|
|
|
186
175
|
private streamUrl(path: string): string {
|
|
187
|
-
return
|
|
176
|
+
return appendPathToBaseUrl(this.baseUrl, path)
|
|
188
177
|
}
|
|
189
178
|
|
|
190
179
|
private streamHeaders(): HeadersRecord | undefined {
|
|
@@ -202,43 +191,19 @@ export class StreamClient {
|
|
|
202
191
|
return headers
|
|
203
192
|
}
|
|
204
193
|
|
|
205
|
-
private subscriptionServiceId(): string | null {
|
|
206
|
-
const url = new URL(this.baseUrl)
|
|
207
|
-
const match = /^(.*)\/v1\/stream\/([^/]+)\/?$/.exec(url.pathname)
|
|
208
|
-
return match ? decodeURIComponent(match[2]!) : null
|
|
209
|
-
}
|
|
210
|
-
|
|
211
194
|
private backendSubscriptionPath(path: string): string {
|
|
212
|
-
|
|
213
|
-
const serviceId = this.subscriptionServiceId()
|
|
214
|
-
if (!serviceId) return normalized
|
|
215
|
-
if (normalized === serviceId || normalized.startsWith(`${serviceId}/`)) {
|
|
216
|
-
return normalized
|
|
217
|
-
}
|
|
218
|
-
return `${serviceId}/${normalized}`
|
|
195
|
+
return normalizeSubscriptionPath(path)
|
|
219
196
|
}
|
|
220
197
|
|
|
221
198
|
private runtimeSubscriptionPath(path: string): string {
|
|
222
|
-
|
|
223
|
-
const serviceId = this.subscriptionServiceId()
|
|
224
|
-
if (!serviceId) return normalized
|
|
225
|
-
return normalized.startsWith(`${serviceId}/`)
|
|
226
|
-
? normalized.slice(serviceId.length + 1)
|
|
227
|
-
: normalized
|
|
199
|
+
return normalizeSubscriptionPath(path)
|
|
228
200
|
}
|
|
229
201
|
|
|
230
202
|
private subscriptionUrl(subscriptionId: string): string {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
url.pathname = `${prefix}/v1/stream-meta/subscriptions/${encodeURIComponent(subscriptionId)}`
|
|
236
|
-
url.searchParams.set(`service`, decodeURIComponent(serviceId!))
|
|
237
|
-
return url.toString()
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
url.pathname = `${url.pathname.replace(/\/+$/, ``)}/v1/stream-meta/subscriptions/${encodeURIComponent(subscriptionId)}`
|
|
241
|
-
return url.toString()
|
|
203
|
+
return appendPathToBaseUrl(
|
|
204
|
+
this.baseUrl,
|
|
205
|
+
`/__ds/subscriptions/${encodeURIComponent(subscriptionId)}`
|
|
206
|
+
)
|
|
242
207
|
}
|
|
243
208
|
|
|
244
209
|
private subscriptionChildUrl(
|
|
@@ -278,7 +243,7 @@ export class StreamClient {
|
|
|
278
243
|
})
|
|
279
244
|
const headers: Record<string, string> = {
|
|
280
245
|
'content-type': `application/json`,
|
|
281
|
-
'Stream-Forked-From': sourcePath,
|
|
246
|
+
'Stream-Forked-From': new URL(this.streamUrl(sourcePath)).pathname,
|
|
282
247
|
}
|
|
283
248
|
injectTraceHeaders(headers)
|
|
284
249
|
|
|
@@ -823,20 +788,4 @@ export class StreamClient {
|
|
|
823
788
|
JSON.parse(text) as SubscriptionResponse
|
|
824
789
|
)
|
|
825
790
|
}
|
|
826
|
-
|
|
827
|
-
async getConsumerState(
|
|
828
|
-
consumerId: string
|
|
829
|
-
): Promise<ConsumerStateResponse | null> {
|
|
830
|
-
const res = await fetch(
|
|
831
|
-
`${this.baseUrl}/consumers/${encodeURIComponent(consumerId)}`,
|
|
832
|
-
{ method: `GET`, headers: await this.requestHeaders() }
|
|
833
|
-
)
|
|
834
|
-
if (res.status === 404) return null
|
|
835
|
-
if (!res.ok) {
|
|
836
|
-
throw new Error(
|
|
837
|
-
`Consumer query failed: ${res.status} ${await res.text()}`
|
|
838
|
-
)
|
|
839
|
-
}
|
|
840
|
-
return res.json() as Promise<ConsumerStateResponse>
|
|
841
|
-
}
|
|
842
791
|
}
|
|
@@ -95,6 +95,7 @@ export function buildElectricProxyTarget(options: {
|
|
|
95
95
|
electricUrl: string
|
|
96
96
|
electricSecret?: string
|
|
97
97
|
tenantId: string
|
|
98
|
+
principalUrl?: string
|
|
98
99
|
}): URL {
|
|
99
100
|
const targetPath = options.incomingUrl.pathname.replace(
|
|
100
101
|
`/_electric/electric`,
|
|
@@ -130,9 +131,19 @@ export function buildElectricProxyTarget(options: {
|
|
|
130
131
|
} else if (table === `runners`) {
|
|
131
132
|
target.searchParams.set(
|
|
132
133
|
`columns`,
|
|
133
|
-
`"tenant_id","id","
|
|
134
|
+
`"tenant_id","id","owner_principal","label","kind","admin_status","wake_stream","created_at","updated_at"`
|
|
134
135
|
)
|
|
135
|
-
applyTenantShapeWhere(target, options.tenantId
|
|
136
|
+
applyTenantShapeWhere(target, options.tenantId, [
|
|
137
|
+
`owner_principal = ${sqlStringLiteral(options.principalUrl ?? ``)}`,
|
|
138
|
+
])
|
|
139
|
+
} else if (table === `runner_runtime_diagnostics`) {
|
|
140
|
+
target.searchParams.set(
|
|
141
|
+
`columns`,
|
|
142
|
+
`"tenant_id","runner_id","owner_principal","wake_stream_offset","last_seen_at","liveness_lease_expires_at","diagnostics","updated_at"`
|
|
143
|
+
)
|
|
144
|
+
applyTenantShapeWhere(target, options.tenantId, [
|
|
145
|
+
`owner_principal = ${sqlStringLiteral(options.principalUrl ?? ``)}`,
|
|
146
|
+
])
|
|
136
147
|
} else if (table === `entity_dispatch_state`) {
|
|
137
148
|
target.searchParams.set(
|
|
138
149
|
`columns`,
|
|
@@ -167,12 +178,13 @@ export async function forwardFetchRequest(options: {
|
|
|
167
178
|
serviceId: string
|
|
168
179
|
body?: Uint8Array
|
|
169
180
|
dispatcher?: Agent
|
|
170
|
-
route?: `stream` | `
|
|
181
|
+
route?: `stream` | `control`
|
|
171
182
|
durableStreamsBearer?: DurableStreamsBearerProvider
|
|
172
183
|
durableStreamsBearerMode?: `overwrite` | `if-missing` | `none`
|
|
173
184
|
}): Promise<Response> {
|
|
174
185
|
const routingAdapter = resolveDurableStreamsRoutingAdapter(
|
|
175
|
-
options.durableStreamsRouting
|
|
186
|
+
options.durableStreamsRouting,
|
|
187
|
+
options.durableStreamsUrl
|
|
176
188
|
)
|
|
177
189
|
const routingInput = {
|
|
178
190
|
durableStreamsUrl: options.durableStreamsUrl,
|
|
@@ -180,8 +192,8 @@ export async function forwardFetchRequest(options: {
|
|
|
180
192
|
requestUrl: options.request.url,
|
|
181
193
|
}
|
|
182
194
|
const upstreamUrl =
|
|
183
|
-
options.route === `
|
|
184
|
-
? routingAdapter.
|
|
195
|
+
options.route === `control`
|
|
196
|
+
? routingAdapter.controlUrl(routingInput)
|
|
185
197
|
: routingAdapter.streamUrl(routingInput)
|
|
186
198
|
|
|
187
199
|
const headers = new Headers(options.request.headers)
|
|
@@ -231,8 +243,15 @@ export function decodeJsonObject(
|
|
|
231
243
|
return null
|
|
232
244
|
}
|
|
233
245
|
|
|
234
|
-
function applyTenantShapeWhere(
|
|
235
|
-
|
|
246
|
+
function applyTenantShapeWhere(
|
|
247
|
+
target: URL,
|
|
248
|
+
tenantId: string,
|
|
249
|
+
extraConditions: Array<string> = []
|
|
250
|
+
): void {
|
|
251
|
+
const tenantWhere = [
|
|
252
|
+
`tenant_id = ${sqlStringLiteral(tenantId)}`,
|
|
253
|
+
...extraConditions,
|
|
254
|
+
].join(` AND `)
|
|
236
255
|
const existingWhere = target.searchParams.get(`where`)
|
|
237
256
|
target.searchParams.set(
|
|
238
257
|
`where`,
|