@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/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, durableStreamsServiceUrl } from './stream-client.js'
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
- durableStreamsServiceUrl(options.durableStreamsUrl, this.tenantId),
149
- { bearer: options.durableStreamsBearer }
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 = streamsUrl
188
- this.streamClient = new StreamClient(
189
- durableStreamsServiceUrl(streamsUrl, this.tenantId),
190
- { bearer: this.options.durableStreamsBearer }
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, durableStreamsServiceUrl } from './stream-client.js'
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
- durableStreamsServiceUrl(options.durableStreamsUrl, serviceId),
62
- { bearer: options.durableStreamsBearer }
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`)
@@ -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 `${this.baseUrl}${path}`
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
- const normalized = normalizeSubscriptionPath(path)
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
- const normalized = normalizeSubscriptionPath(path)
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
- const url = new URL(this.baseUrl)
232
- const match = /^(.*)\/v1\/stream\/([^/]+)\/?$/.exec(url.pathname)
233
- if (match) {
234
- const [, prefix = ``, serviceId] = match
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","owner_user_id","label","kind","admin_status","wake_stream","wake_stream_offset","last_seen_at","liveness_lease_expires_at","created_at","updated_at"`
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` | `stream-meta`
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 === `stream-meta`
184
- ? routingAdapter.streamMetaUrl(routingInput)
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(target: URL, tenantId: string): void {
235
- const tenantWhere = `tenant_id = ${sqlStringLiteral(tenantId)}`
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`,