@electric-ax/agents-server 0.4.1 → 0.4.3
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 +178 -157
- package/dist/index.cjs +161 -136
- package/dist/index.d.cts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +159 -136
- package/package.json +5 -5
- package/src/db/index.ts +1 -1
- package/src/index.ts +5 -1
- package/src/routing/context.ts +1 -0
- package/src/routing/durable-streams-router.ts +286 -116
- package/src/routing/durable-streams-routing-adapter.ts +26 -58
- package/src/routing/internal-router.ts +6 -2
- package/src/routing/runners-router.ts +1 -0
- package/src/runtime.ts +3 -1
- package/src/server.ts +8 -7
- package/src/standalone-runtime.ts +3 -1
- package/src/stream-client.ts +24 -32
- package/src/utils/server-utils.ts +5 -4
|
@@ -36,6 +36,13 @@ const subscriptionProxyBodySchema = Type.Object(
|
|
|
36
36
|
|
|
37
37
|
type SubscriptionProxyBody = Static<typeof subscriptionProxyBodySchema>
|
|
38
38
|
|
|
39
|
+
const subscriptionControlActions = [
|
|
40
|
+
`callback`,
|
|
41
|
+
`claim`,
|
|
42
|
+
`ack`,
|
|
43
|
+
`release`,
|
|
44
|
+
] as const
|
|
45
|
+
|
|
39
46
|
export type DurableStreamsRoutes = RouterType<
|
|
40
47
|
IRequest,
|
|
41
48
|
[TenantContext],
|
|
@@ -48,7 +55,34 @@ export const durableStreamsRouter: DurableStreamsRoutes = Router<
|
|
|
48
55
|
Response | undefined
|
|
49
56
|
>()
|
|
50
57
|
|
|
51
|
-
durableStreamsRouter.
|
|
58
|
+
durableStreamsRouter.put(
|
|
59
|
+
`/__ds/subscriptions/:subscriptionId`,
|
|
60
|
+
putSubscriptionBase
|
|
61
|
+
)
|
|
62
|
+
durableStreamsRouter.get(
|
|
63
|
+
`/__ds/subscriptions/:subscriptionId`,
|
|
64
|
+
getSubscriptionBase
|
|
65
|
+
)
|
|
66
|
+
durableStreamsRouter.delete(
|
|
67
|
+
`/__ds/subscriptions/:subscriptionId`,
|
|
68
|
+
deleteSubscriptionBase
|
|
69
|
+
)
|
|
70
|
+
durableStreamsRouter.post(
|
|
71
|
+
`/__ds/subscriptions/:subscriptionId/streams`,
|
|
72
|
+
postSubscriptionStreams
|
|
73
|
+
)
|
|
74
|
+
durableStreamsRouter.delete(
|
|
75
|
+
`/__ds/subscriptions/:subscriptionId/streams/:streamPath+`,
|
|
76
|
+
deleteSubscriptionStream
|
|
77
|
+
)
|
|
78
|
+
for (const action of subscriptionControlActions) {
|
|
79
|
+
durableStreamsRouter.post(
|
|
80
|
+
`/__ds/subscriptions/:subscriptionId/${action}`,
|
|
81
|
+
subscriptionAction(action)
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
durableStreamsRouter.all(`/__ds`, controlPassThrough)
|
|
85
|
+
durableStreamsRouter.all(`/__ds/*`, controlPassThrough)
|
|
52
86
|
durableStreamsRouter.post(`*`, streamAppend)
|
|
53
87
|
durableStreamsRouter.all(`*`, proxyPassThrough)
|
|
54
88
|
|
|
@@ -71,8 +105,9 @@ async function forwardToDurableStreams(
|
|
|
71
105
|
ctx: TenantContext,
|
|
72
106
|
request: IRequest,
|
|
73
107
|
body?: Uint8Array,
|
|
74
|
-
route: `stream` | `
|
|
75
|
-
urlOverride?: string
|
|
108
|
+
route: `stream` | `control` = `stream`,
|
|
109
|
+
urlOverride?: string,
|
|
110
|
+
durableStreamsBearerMode: `overwrite` | `if-missing` = `overwrite`
|
|
76
111
|
): Promise<Response> {
|
|
77
112
|
const headers = new Headers(request.headers)
|
|
78
113
|
headers.delete(`host`)
|
|
@@ -94,11 +129,7 @@ async function forwardToDurableStreams(
|
|
|
94
129
|
body: requestBody,
|
|
95
130
|
durableStreamsUrl: ctx.durableStreamsUrl,
|
|
96
131
|
durableStreamsBearer: ctx.durableStreamsBearer,
|
|
97
|
-
durableStreamsBearerMode
|
|
98
|
-
urlOverride ?? request.url
|
|
99
|
-
)
|
|
100
|
-
? `if-missing`
|
|
101
|
-
: `overwrite`,
|
|
132
|
+
durableStreamsBearerMode,
|
|
102
133
|
durableStreamsRouting: ctx.durableStreamsRouting,
|
|
103
134
|
serviceId: ctx.service,
|
|
104
135
|
dispatcher: ctx.durableStreamsDispatcher,
|
|
@@ -106,23 +137,7 @@ async function forwardToDurableStreams(
|
|
|
106
137
|
})
|
|
107
138
|
}
|
|
108
139
|
|
|
109
|
-
|
|
110
|
-
const match = /^\/v1\/stream-meta\/subscriptions\/([^/]+)(?:\/.*)?$/.exec(
|
|
111
|
-
pathname
|
|
112
|
-
)
|
|
113
|
-
return match ? decodeURIComponent(match[1]!) : null
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function isSubscriptionBasePath(pathname: string): boolean {
|
|
117
|
-
return /^\/v1\/stream-meta\/subscriptions\/[^/]+\/?$/.test(pathname)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function usesSubscriptionScopedBearer(requestUrl: string): boolean {
|
|
121
|
-
const pathname = new URL(requestUrl, `http://localhost`).pathname
|
|
122
|
-
return /^\/v1\/stream-meta\/subscriptions\/[^/]+\/(?:ack|release|callback)\/?$/.test(
|
|
123
|
-
pathname
|
|
124
|
-
)
|
|
125
|
-
}
|
|
140
|
+
type SubscriptionControlAction = (typeof subscriptionControlActions)[number]
|
|
126
141
|
|
|
127
142
|
function rewriteSubscriptionBodyForBackend(
|
|
128
143
|
payload: Record<string, unknown>,
|
|
@@ -239,80 +254,82 @@ function decodeJson(bytes: Uint8Array): Record<string, unknown> | null {
|
|
|
239
254
|
}
|
|
240
255
|
}
|
|
241
256
|
|
|
242
|
-
function
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
): string {
|
|
247
|
-
const match =
|
|
248
|
-
/^(\/v1\/stream-meta\/subscriptions\/[^/]+\/streams\/)(.+)$/.exec(
|
|
249
|
-
requestUrl.pathname
|
|
250
|
-
)
|
|
251
|
-
if (!match) return requestUrl.toString()
|
|
252
|
-
|
|
253
|
-
const [, prefix, encodedPath] = match
|
|
254
|
-
const streamPath = decodeURIComponent(encodedPath!)
|
|
255
|
-
requestUrl.pathname = `${prefix}${encodeURIComponent(
|
|
256
|
-
routingAdapter.toBackendStreamPath(service, streamPath)
|
|
257
|
-
)}`
|
|
258
|
-
return requestUrl.toString()
|
|
257
|
+
function routeParam(request: IRequest, name: string): string {
|
|
258
|
+
const value = request.params[name]
|
|
259
|
+
const raw = Array.isArray(value) ? value[0] : value
|
|
260
|
+
return decodeURIComponent(raw ?? ``)
|
|
259
261
|
}
|
|
260
262
|
|
|
261
|
-
|
|
262
|
-
request: IRequest,
|
|
263
|
+
function subscriptionRoutingAdapter(
|
|
263
264
|
ctx: TenantContext
|
|
264
|
-
):
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const routingAdapter = resolveDurableStreamsRoutingAdapter(
|
|
270
|
-
ctx.durableStreamsRouting
|
|
265
|
+
): DurableStreamsRoutingAdapter {
|
|
266
|
+
return resolveDurableStreamsRoutingAdapter(
|
|
267
|
+
ctx.durableStreamsRouting,
|
|
268
|
+
ctx.durableStreamsUrl
|
|
271
269
|
)
|
|
272
|
-
|
|
273
|
-
let targetWebhookUrl: string | null = null
|
|
274
|
-
let requestUrl = request.url
|
|
270
|
+
}
|
|
275
271
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
ctx.publicUrl,
|
|
287
|
-
`/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`
|
|
288
|
-
)
|
|
289
|
-
}
|
|
290
|
-
rewriteSubscriptionBodyForBackend(
|
|
291
|
-
payload as Record<string, unknown>,
|
|
292
|
-
ctx.service,
|
|
293
|
-
routingAdapter
|
|
294
|
-
)
|
|
295
|
-
requestBody = new TextEncoder().encode(JSON.stringify(payload))
|
|
272
|
+
async function rewriteSubscriptionRequestBody(
|
|
273
|
+
request: IRequest,
|
|
274
|
+
ctx: TenantContext,
|
|
275
|
+
subscriptionId: string,
|
|
276
|
+
routingAdapter: DurableStreamsRoutingAdapter
|
|
277
|
+
): Promise<
|
|
278
|
+
| {
|
|
279
|
+
ok: true
|
|
280
|
+
body: Uint8Array
|
|
281
|
+
targetWebhookUrl: string | null
|
|
296
282
|
}
|
|
283
|
+
| { ok: false; response: Response }
|
|
284
|
+
> {
|
|
285
|
+
const body = await readRequestBody(request as Request)
|
|
286
|
+
if (body.length === 0) {
|
|
287
|
+
return { ok: true, body, targetWebhookUrl: null }
|
|
297
288
|
}
|
|
298
289
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
290
|
+
const validation = validateBody(subscriptionProxyBodySchema, body)
|
|
291
|
+
if (!validation.ok) return { ok: false, response: validation.response }
|
|
292
|
+
|
|
293
|
+
const payload = validation.value as SubscriptionProxyBody
|
|
294
|
+
let targetWebhookUrl: string | null = null
|
|
295
|
+
if (payload.webhook?.url !== undefined) {
|
|
296
|
+
targetWebhookUrl = rewriteLoopbackWebhookUrl(payload.webhook.url) ?? null
|
|
297
|
+
payload.webhook.url = appendPathToUrl(
|
|
298
|
+
ctx.publicUrl,
|
|
299
|
+
`/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`
|
|
307
300
|
)
|
|
308
301
|
}
|
|
309
302
|
|
|
303
|
+
rewriteSubscriptionBodyForBackend(
|
|
304
|
+
payload as Record<string, unknown>,
|
|
305
|
+
ctx.service,
|
|
306
|
+
routingAdapter
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
ok: true,
|
|
311
|
+
body: new TextEncoder().encode(JSON.stringify(payload)),
|
|
312
|
+
targetWebhookUrl,
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function forwardSubscriptionRequest(
|
|
317
|
+
request: IRequest,
|
|
318
|
+
ctx: TenantContext,
|
|
319
|
+
routingAdapter: DurableStreamsRoutingAdapter,
|
|
320
|
+
opts: {
|
|
321
|
+
body?: Uint8Array
|
|
322
|
+
requestUrl?: string
|
|
323
|
+
bearerMode?: `overwrite` | `if-missing`
|
|
324
|
+
} = {}
|
|
325
|
+
): Promise<{ upstream: Response; response: Response }> {
|
|
310
326
|
const upstream = await forwardToDurableStreams(
|
|
311
327
|
ctx,
|
|
312
328
|
request,
|
|
313
|
-
|
|
314
|
-
`
|
|
315
|
-
requestUrl
|
|
329
|
+
opts.body,
|
|
330
|
+
`control`,
|
|
331
|
+
opts.requestUrl,
|
|
332
|
+
opts.bearerMode ?? `overwrite`
|
|
316
333
|
)
|
|
317
334
|
let responseBytes: Uint8Array = upstream.body
|
|
318
335
|
? new Uint8Array(await upstream.arrayBuffer())
|
|
@@ -323,42 +340,196 @@ async function subscriptionProxy(
|
|
|
323
340
|
ctx.service,
|
|
324
341
|
routingAdapter
|
|
325
342
|
)
|
|
326
|
-
|
|
343
|
+
return {
|
|
344
|
+
upstream,
|
|
345
|
+
response: responseFromUpstream(upstream, responseBytes),
|
|
346
|
+
}
|
|
347
|
+
}
|
|
327
348
|
|
|
328
|
-
|
|
349
|
+
async function upsertSubscriptionWebhook(
|
|
350
|
+
ctx: TenantContext,
|
|
351
|
+
subscriptionId: string,
|
|
352
|
+
targetWebhookUrl: string
|
|
353
|
+
): Promise<void> {
|
|
354
|
+
await ctx.pgDb
|
|
355
|
+
.insert(subscriptionWebhooks)
|
|
356
|
+
.values({
|
|
357
|
+
tenantId: ctx.service,
|
|
358
|
+
subscriptionId,
|
|
359
|
+
webhookUrl: targetWebhookUrl,
|
|
360
|
+
})
|
|
361
|
+
.onConflictDoUpdate({
|
|
362
|
+
target: [
|
|
363
|
+
subscriptionWebhooks.tenantId,
|
|
364
|
+
subscriptionWebhooks.subscriptionId,
|
|
365
|
+
],
|
|
366
|
+
set: { webhookUrl: targetWebhookUrl },
|
|
367
|
+
})
|
|
368
|
+
}
|
|
329
369
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
)
|
|
370
|
+
async function deleteSubscriptionWebhook(
|
|
371
|
+
ctx: TenantContext,
|
|
372
|
+
subscriptionId: string
|
|
373
|
+
): Promise<void> {
|
|
374
|
+
await ctx.pgDb
|
|
375
|
+
.delete(subscriptionWebhooks)
|
|
376
|
+
.where(
|
|
377
|
+
and(
|
|
378
|
+
eq(subscriptionWebhooks.tenantId, ctx.service),
|
|
379
|
+
eq(subscriptionWebhooks.subscriptionId, subscriptionId)
|
|
341
380
|
)
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function rewriteSubscriptionStreamPathInUrl(
|
|
385
|
+
requestUrl: URL,
|
|
386
|
+
service: string,
|
|
387
|
+
routingAdapter: DurableStreamsRoutingAdapter,
|
|
388
|
+
streamPath: string
|
|
389
|
+
): string {
|
|
390
|
+
const prefix = requestUrl.pathname.slice(
|
|
391
|
+
0,
|
|
392
|
+
requestUrl.pathname.indexOf(`/streams/`) + `/streams/`.length
|
|
393
|
+
)
|
|
394
|
+
requestUrl.pathname = `${prefix}${encodeURIComponent(
|
|
395
|
+
routingAdapter.toBackendStreamPath(service, streamPath)
|
|
396
|
+
)}`
|
|
397
|
+
return requestUrl.toString()
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function putSubscriptionBase(
|
|
401
|
+
request: IRequest,
|
|
402
|
+
ctx: TenantContext
|
|
403
|
+
): Promise<Response> {
|
|
404
|
+
const subscriptionId = routeParam(request, `subscriptionId`)
|
|
405
|
+
const routingAdapter = subscriptionRoutingAdapter(ctx)
|
|
406
|
+
const rewrite = await rewriteSubscriptionRequestBody(
|
|
407
|
+
request,
|
|
408
|
+
ctx,
|
|
409
|
+
subscriptionId,
|
|
410
|
+
routingAdapter
|
|
411
|
+
)
|
|
412
|
+
if (!rewrite.ok) return rewrite.response
|
|
413
|
+
|
|
414
|
+
const { upstream, response } = await forwardSubscriptionRequest(
|
|
415
|
+
request,
|
|
416
|
+
ctx,
|
|
417
|
+
routingAdapter,
|
|
418
|
+
{ body: rewrite.body }
|
|
419
|
+
)
|
|
420
|
+
if (upstream.ok && rewrite.targetWebhookUrl) {
|
|
421
|
+
await upsertSubscriptionWebhook(
|
|
422
|
+
ctx,
|
|
423
|
+
subscriptionId,
|
|
424
|
+
rewrite.targetWebhookUrl
|
|
425
|
+
)
|
|
357
426
|
}
|
|
427
|
+
return response
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function getSubscriptionBase(
|
|
431
|
+
request: IRequest,
|
|
432
|
+
ctx: TenantContext
|
|
433
|
+
): Promise<Response> {
|
|
434
|
+
const routingAdapter = subscriptionRoutingAdapter(ctx)
|
|
435
|
+
return (await forwardSubscriptionRequest(request, ctx, routingAdapter))
|
|
436
|
+
.response
|
|
437
|
+
}
|
|
358
438
|
|
|
439
|
+
async function deleteSubscriptionBase(
|
|
440
|
+
request: IRequest,
|
|
441
|
+
ctx: TenantContext
|
|
442
|
+
): Promise<Response> {
|
|
443
|
+
const subscriptionId = routeParam(request, `subscriptionId`)
|
|
444
|
+
const routingAdapter = subscriptionRoutingAdapter(ctx)
|
|
445
|
+
const { upstream, response } = await forwardSubscriptionRequest(
|
|
446
|
+
request,
|
|
447
|
+
ctx,
|
|
448
|
+
routingAdapter
|
|
449
|
+
)
|
|
450
|
+
if (upstream.ok) {
|
|
451
|
+
await deleteSubscriptionWebhook(ctx, subscriptionId)
|
|
452
|
+
}
|
|
359
453
|
return response
|
|
360
454
|
}
|
|
361
455
|
|
|
456
|
+
async function postSubscriptionStreams(
|
|
457
|
+
request: IRequest,
|
|
458
|
+
ctx: TenantContext
|
|
459
|
+
): Promise<Response> {
|
|
460
|
+
const subscriptionId = routeParam(request, `subscriptionId`)
|
|
461
|
+
const routingAdapter = subscriptionRoutingAdapter(ctx)
|
|
462
|
+
const rewrite = await rewriteSubscriptionRequestBody(
|
|
463
|
+
request,
|
|
464
|
+
ctx,
|
|
465
|
+
subscriptionId,
|
|
466
|
+
routingAdapter
|
|
467
|
+
)
|
|
468
|
+
if (!rewrite.ok) return rewrite.response
|
|
469
|
+
|
|
470
|
+
return (
|
|
471
|
+
await forwardSubscriptionRequest(request, ctx, routingAdapter, {
|
|
472
|
+
body: rewrite.body,
|
|
473
|
+
})
|
|
474
|
+
).response
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function deleteSubscriptionStream(
|
|
478
|
+
request: IRequest,
|
|
479
|
+
ctx: TenantContext
|
|
480
|
+
): Promise<Response> {
|
|
481
|
+
const routingAdapter = subscriptionRoutingAdapter(ctx)
|
|
482
|
+
const requestUrl = rewriteSubscriptionStreamPathInUrl(
|
|
483
|
+
new URL(request.url),
|
|
484
|
+
ctx.service,
|
|
485
|
+
routingAdapter,
|
|
486
|
+
routeParam(request, `streamPath`)
|
|
487
|
+
)
|
|
488
|
+
return (
|
|
489
|
+
await forwardSubscriptionRequest(request, ctx, routingAdapter, {
|
|
490
|
+
requestUrl,
|
|
491
|
+
})
|
|
492
|
+
).response
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function subscriptionAction(action: SubscriptionControlAction) {
|
|
496
|
+
return async (request: IRequest, ctx: TenantContext): Promise<Response> => {
|
|
497
|
+
const subscriptionId = routeParam(request, `subscriptionId`)
|
|
498
|
+
const routingAdapter = subscriptionRoutingAdapter(ctx)
|
|
499
|
+
const rewrite = await rewriteSubscriptionRequestBody(
|
|
500
|
+
request,
|
|
501
|
+
ctx,
|
|
502
|
+
subscriptionId,
|
|
503
|
+
routingAdapter
|
|
504
|
+
)
|
|
505
|
+
if (!rewrite.ok) return rewrite.response
|
|
506
|
+
|
|
507
|
+
const bearerMode =
|
|
508
|
+
action === `ack` || action === `release` || action === `callback`
|
|
509
|
+
? `if-missing`
|
|
510
|
+
: `overwrite`
|
|
511
|
+
return (
|
|
512
|
+
await forwardSubscriptionRequest(request, ctx, routingAdapter, {
|
|
513
|
+
body: rewrite.body,
|
|
514
|
+
bearerMode,
|
|
515
|
+
})
|
|
516
|
+
).response
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function controlPassThrough(
|
|
521
|
+
request: IRequest,
|
|
522
|
+
ctx: TenantContext
|
|
523
|
+
): Promise<Response> {
|
|
524
|
+
const upstream = await forwardToDurableStreams(
|
|
525
|
+
ctx,
|
|
526
|
+
request,
|
|
527
|
+
undefined,
|
|
528
|
+
`control`
|
|
529
|
+
)
|
|
530
|
+
return responseFromUpstream(upstream)
|
|
531
|
+
}
|
|
532
|
+
|
|
362
533
|
async function streamAppend(
|
|
363
534
|
request: IRequest,
|
|
364
535
|
ctx: TenantContext
|
|
@@ -391,13 +562,12 @@ async function proxyPassThrough(
|
|
|
391
562
|
const upstream = await forwardToDurableStreams(ctx, request)
|
|
392
563
|
const streamPath = new URL(request.url).pathname
|
|
393
564
|
const method = request.method.toUpperCase()
|
|
394
|
-
const isControlPath = streamPath.startsWith(`/v1/stream-meta/`)
|
|
395
565
|
const endTrackedRead =
|
|
396
|
-
method === `GET`
|
|
566
|
+
method === `GET`
|
|
397
567
|
? await ctx.entityBridgeManager.beginClientRead(streamPath)
|
|
398
568
|
: null
|
|
399
569
|
try {
|
|
400
|
-
if (method === `HEAD`
|
|
570
|
+
if (method === `HEAD`) {
|
|
401
571
|
await ctx.entityBridgeManager.touchByStreamPath(streamPath)
|
|
402
572
|
}
|
|
403
573
|
return responseFromUpstream(upstream)
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
prefixTenantStreamPath,
|
|
3
|
-
stripTenantStreamPrefix,
|
|
4
|
-
} from './tenant-stream-paths.js'
|
|
5
|
-
|
|
6
1
|
export interface DurableStreamsRoutingInput {
|
|
7
2
|
durableStreamsUrl: string
|
|
8
3
|
serviceId: string
|
|
@@ -11,7 +6,7 @@ export interface DurableStreamsRoutingInput {
|
|
|
11
6
|
|
|
12
7
|
export interface DurableStreamsRoutingAdapter {
|
|
13
8
|
streamUrl(input: DurableStreamsRoutingInput): URL
|
|
14
|
-
|
|
9
|
+
controlUrl(input: DurableStreamsRoutingInput): URL
|
|
15
10
|
toBackendStreamPath(serviceId: string, streamPath: string): string
|
|
16
11
|
toRuntimeStreamPath(serviceId: string, streamPath: string): string
|
|
17
12
|
}
|
|
@@ -26,71 +21,44 @@ function removeServiceQuery(target: URL): URL {
|
|
|
26
21
|
return target
|
|
27
22
|
}
|
|
28
23
|
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
serviceId: string
|
|
32
|
-
): { incomingUrl: URL; streamPath: string } {
|
|
33
|
-
const incomingUrl = new URL(requestUrl, `http://localhost`)
|
|
34
|
-
const segments = incomingUrl.pathname.split(`/`).filter(Boolean)
|
|
35
|
-
if (segments[0] === `v1` && segments[1] === `stream`) {
|
|
36
|
-
return {
|
|
37
|
-
incomingUrl,
|
|
38
|
-
streamPath: segments.length > 2 ? `/${segments.slice(3).join(`/`)}` : `/`,
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
incomingUrl,
|
|
44
|
-
streamPath: incomingUrl.pathname || `/${serviceId}`,
|
|
45
|
-
}
|
|
24
|
+
function withoutTrailingSlash(pathname: string): string {
|
|
25
|
+
return pathname.replace(/\/+$/, ``) || `/`
|
|
46
26
|
}
|
|
47
27
|
|
|
48
|
-
function
|
|
49
|
-
input: DurableStreamsRoutingInput,
|
|
50
|
-
backendStreamPath: string
|
|
51
|
-
): URL {
|
|
52
|
-
const path = backendStreamPath.replace(/^\/+/, ``)
|
|
53
|
-
const target = new URL(`/v1/stream/${path}`, input.durableStreamsUrl)
|
|
54
|
-
return target
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function streamMetaUrlWithoutService(input: DurableStreamsRoutingInput): URL {
|
|
28
|
+
function appendRequestPathToStreamRoot(input: DurableStreamsRoutingInput): URL {
|
|
58
29
|
const incomingUrl = new URL(input.requestUrl, `http://localhost`)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
)
|
|
64
|
-
)
|
|
30
|
+
const path = incomingUrl.pathname.replace(/^\/+/, ``)
|
|
31
|
+
const target = new URL(input.durableStreamsUrl)
|
|
32
|
+
target.pathname = path
|
|
33
|
+
? `${withoutTrailingSlash(target.pathname)}/${path}`
|
|
34
|
+
: withoutTrailingSlash(target.pathname)
|
|
35
|
+
return removeServiceQuery(appendSearch(target, incomingUrl))
|
|
65
36
|
}
|
|
66
37
|
|
|
67
|
-
export const
|
|
38
|
+
export const streamRootDurableStreamsRoutingAdapter: DurableStreamsRoutingAdapter =
|
|
68
39
|
{
|
|
69
|
-
streamUrl
|
|
70
|
-
const { incomingUrl, streamPath } = logicalStreamPathFromRequest(
|
|
71
|
-
input.requestUrl,
|
|
72
|
-
input.serviceId
|
|
73
|
-
)
|
|
74
|
-
const target = backendStreamUrl(
|
|
75
|
-
input,
|
|
76
|
-
prefixTenantStreamPath(streamPath, input.serviceId)
|
|
77
|
-
)
|
|
78
|
-
return removeServiceQuery(appendSearch(target, incomingUrl))
|
|
79
|
-
},
|
|
40
|
+
streamUrl: appendRequestPathToStreamRoot,
|
|
80
41
|
|
|
81
|
-
|
|
42
|
+
controlUrl: appendRequestPathToStreamRoot,
|
|
82
43
|
|
|
83
|
-
toBackendStreamPath(
|
|
84
|
-
return
|
|
44
|
+
toBackendStreamPath(_serviceId, streamPath) {
|
|
45
|
+
return streamPath.replace(/^\/+/, ``)
|
|
85
46
|
},
|
|
86
47
|
|
|
87
|
-
toRuntimeStreamPath(
|
|
88
|
-
return
|
|
48
|
+
toRuntimeStreamPath(_serviceId, streamPath) {
|
|
49
|
+
return streamPath.replace(/^\/+/, ``)
|
|
89
50
|
},
|
|
90
51
|
}
|
|
91
52
|
|
|
53
|
+
export const pathPrefixedSingleTenantDurableStreamsRoutingAdapter =
|
|
54
|
+
streamRootDurableStreamsRoutingAdapter
|
|
55
|
+
|
|
56
|
+
export const tenantRootDurableStreamsRoutingAdapter =
|
|
57
|
+
streamRootDurableStreamsRoutingAdapter
|
|
58
|
+
|
|
92
59
|
export function resolveDurableStreamsRoutingAdapter(
|
|
93
|
-
adapter?: DurableStreamsRoutingAdapter
|
|
60
|
+
adapter?: DurableStreamsRoutingAdapter,
|
|
61
|
+
_durableStreamsUrl?: string
|
|
94
62
|
): DurableStreamsRoutingAdapter {
|
|
95
|
-
return adapter ??
|
|
63
|
+
return adapter ?? streamRootDurableStreamsRoutingAdapter
|
|
96
64
|
}
|
|
@@ -294,7 +294,8 @@ async function webhookForward(
|
|
|
294
294
|
const parsedBody = parsedBodyResult.value as WebhookForwardBody | undefined
|
|
295
295
|
const newWebhook = newWebhookPayload(parsedBody)
|
|
296
296
|
const routingAdapter = resolveDurableStreamsRoutingAdapter(
|
|
297
|
-
ctx.durableStreamsRouting
|
|
297
|
+
ctx.durableStreamsRouting,
|
|
298
|
+
ctx.durableStreamsUrl
|
|
298
299
|
)
|
|
299
300
|
|
|
300
301
|
if (parsedBody) {
|
|
@@ -537,7 +538,10 @@ async function callbackForward(
|
|
|
537
538
|
ctx.service,
|
|
538
539
|
consumerId,
|
|
539
540
|
requestBody,
|
|
540
|
-
resolveDurableStreamsRoutingAdapter(
|
|
541
|
+
resolveDurableStreamsRoutingAdapter(
|
|
542
|
+
ctx.durableStreamsRouting,
|
|
543
|
+
ctx.durableStreamsUrl
|
|
544
|
+
)
|
|
541
545
|
)
|
|
542
546
|
|
|
543
547
|
let upstream: Response
|
package/src/runtime.ts
CHANGED
|
@@ -64,7 +64,9 @@ export class ElectricAgentsTenantRuntime {
|
|
|
64
64
|
this.streamClient = options.streamClient
|
|
65
65
|
} else if (options.durableStreamsUrl) {
|
|
66
66
|
this.streamClient = new StreamClient(
|
|
67
|
-
durableStreamsServiceUrl(options.durableStreamsUrl, this.serviceId
|
|
67
|
+
durableStreamsServiceUrl(options.durableStreamsUrl, this.serviceId, {
|
|
68
|
+
scope: `stream-root`,
|
|
69
|
+
}),
|
|
68
70
|
{ bearer: options.durableStreamsBearer }
|
|
69
71
|
)
|
|
70
72
|
} else {
|
package/src/server.ts
CHANGED
|
@@ -7,7 +7,6 @@ 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
12
|
import { StreamClient, durableStreamsServiceUrl } from './stream-client.js'
|
|
@@ -145,7 +144,9 @@ export class ElectricAgentsServer {
|
|
|
145
144
|
this.options = options
|
|
146
145
|
this.streamClient = options.durableStreamsUrl
|
|
147
146
|
? new StreamClient(
|
|
148
|
-
durableStreamsServiceUrl(options.durableStreamsUrl, this.tenantId
|
|
147
|
+
durableStreamsServiceUrl(options.durableStreamsUrl, this.tenantId, {
|
|
148
|
+
scope: `stream-root`,
|
|
149
|
+
}),
|
|
149
150
|
{ bearer: options.durableStreamsBearer }
|
|
150
151
|
)
|
|
151
152
|
: null!
|
|
@@ -186,7 +187,9 @@ export class ElectricAgentsServer {
|
|
|
186
187
|
)
|
|
187
188
|
this.options.durableStreamsUrl = streamsUrl
|
|
188
189
|
this.streamClient = new StreamClient(
|
|
189
|
-
durableStreamsServiceUrl(streamsUrl, this.tenantId
|
|
190
|
+
durableStreamsServiceUrl(streamsUrl, this.tenantId, {
|
|
191
|
+
scope: `stream-root`,
|
|
192
|
+
}),
|
|
190
193
|
{ bearer: this.options.durableStreamsBearer }
|
|
191
194
|
)
|
|
192
195
|
}
|
|
@@ -401,11 +404,9 @@ export class ElectricAgentsServer {
|
|
|
401
404
|
principal,
|
|
402
405
|
publicUrl: this.publicUrl,
|
|
403
406
|
localUrl: this._url,
|
|
404
|
-
durableStreamsUrl: this.
|
|
407
|
+
durableStreamsUrl: this.streamClient.baseUrl,
|
|
405
408
|
durableStreamsBearer: this.options.durableStreamsBearer,
|
|
406
|
-
durableStreamsRouting:
|
|
407
|
-
this.options.durableStreamsRouting ??
|
|
408
|
-
pathPrefixedSingleTenantDurableStreamsRoutingAdapter,
|
|
409
|
+
durableStreamsRouting: this.options.durableStreamsRouting,
|
|
409
410
|
durableStreamsDispatcher: this.streamsAgent,
|
|
410
411
|
electricUrl: this.options.electricUrl,
|
|
411
412
|
electricSecret: this.options.electricSecret,
|
|
@@ -58,7 +58,9 @@ export async function startStandaloneAgentsRuntime(
|
|
|
58
58
|
options.streamClient ??
|
|
59
59
|
(options.durableStreamsUrl
|
|
60
60
|
? new StreamClient(
|
|
61
|
-
durableStreamsServiceUrl(options.durableStreamsUrl, serviceId
|
|
61
|
+
durableStreamsServiceUrl(options.durableStreamsUrl, serviceId, {
|
|
62
|
+
scope: `stream-root`,
|
|
63
|
+
}),
|
|
62
64
|
{ bearer: options.durableStreamsBearer }
|
|
63
65
|
)
|
|
64
66
|
: undefined)
|