@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
package/dist/index.js
CHANGED
|
@@ -287,7 +287,7 @@ function createDb(postgresUrl) {
|
|
|
287
287
|
const poolMax = Number(process.env.ELECTRIC_AGENTS_PG_POOL_MAX ?? `100`);
|
|
288
288
|
const client = postgres(postgresUrl, {
|
|
289
289
|
max: poolMax,
|
|
290
|
-
fetch_types:
|
|
290
|
+
fetch_types: true
|
|
291
291
|
});
|
|
292
292
|
const db = drizzle(client, { schema: schema_exports });
|
|
293
293
|
return {
|
|
@@ -4177,11 +4177,18 @@ function durableStreamsBearerHeaders(bearer) {
|
|
|
4177
4177
|
if (!bearer) return void 0;
|
|
4178
4178
|
return { authorization: async () => await resolveDurableStreamsBearer(bearer) ?? `` };
|
|
4179
4179
|
}
|
|
4180
|
-
function durableStreamsServiceUrl(baseUrl, serviceId) {
|
|
4180
|
+
function durableStreamsServiceUrl(baseUrl, serviceId, options = {}) {
|
|
4181
4181
|
const url = new URL(baseUrl);
|
|
4182
|
-
if (
|
|
4183
|
-
|
|
4184
|
-
|
|
4182
|
+
if (/\/v1\/streams\/[^/]+\/?$/.test(url.pathname)) return baseUrl.replace(/\/+$/, ``);
|
|
4183
|
+
if (/\/v1\/stream\/[^/]+\/?$/.test(url.pathname)) return baseUrl.replace(/\/+$/, ``);
|
|
4184
|
+
const scope = options.scope ?? `service`;
|
|
4185
|
+
const encodedServiceId = encodeURIComponent(serviceId);
|
|
4186
|
+
const path$1 = url.pathname.replace(/\/+$/, ``) || `/`;
|
|
4187
|
+
if (path$1.endsWith(`/v1/streams`)) url.pathname = `${path$1}/${encodedServiceId}`;
|
|
4188
|
+
else if (path$1.endsWith(`/v1/stream`)) url.pathname = scope === `service` ? `${path$1}/${encodedServiceId}` : path$1;
|
|
4189
|
+
else if (scope === `stream-root`) url.pathname = `${path$1 === `/` ? `` : path$1}/v1/stream`;
|
|
4190
|
+
else url.pathname = `${path$1 === `/` ? `` : path$1}/v1/stream/${encodedServiceId}`;
|
|
4191
|
+
return url.toString().replace(/\/+$/, ``);
|
|
4185
4192
|
}
|
|
4186
4193
|
function isNotFoundError(err) {
|
|
4187
4194
|
return err instanceof DurableStreamError && err.code === ErrCodeNotFound || err instanceof FetchError && err.status === 404;
|
|
@@ -4214,34 +4221,15 @@ var StreamClient = class {
|
|
|
4214
4221
|
await applyDurableStreamsBearer(headers, this.options.bearer, { overwrite: opts.overwriteBearer });
|
|
4215
4222
|
return headers;
|
|
4216
4223
|
}
|
|
4217
|
-
subscriptionServiceId() {
|
|
4218
|
-
const url = new URL(this.baseUrl);
|
|
4219
|
-
const match = /^(.*)\/v1\/stream\/([^/]+)\/?$/.exec(url.pathname);
|
|
4220
|
-
return match ? decodeURIComponent(match[2]) : null;
|
|
4221
|
-
}
|
|
4222
4224
|
backendSubscriptionPath(path$1) {
|
|
4223
|
-
|
|
4224
|
-
const serviceId = this.subscriptionServiceId();
|
|
4225
|
-
if (!serviceId) return normalized;
|
|
4226
|
-
if (normalized === serviceId || normalized.startsWith(`${serviceId}/`)) return normalized;
|
|
4227
|
-
return `${serviceId}/${normalized}`;
|
|
4225
|
+
return normalizeSubscriptionPath(path$1);
|
|
4228
4226
|
}
|
|
4229
4227
|
runtimeSubscriptionPath(path$1) {
|
|
4230
|
-
|
|
4231
|
-
const serviceId = this.subscriptionServiceId();
|
|
4232
|
-
if (!serviceId) return normalized;
|
|
4233
|
-
return normalized.startsWith(`${serviceId}/`) ? normalized.slice(serviceId.length + 1) : normalized;
|
|
4228
|
+
return normalizeSubscriptionPath(path$1);
|
|
4234
4229
|
}
|
|
4235
4230
|
subscriptionUrl(subscriptionId) {
|
|
4236
4231
|
const url = new URL(this.baseUrl);
|
|
4237
|
-
|
|
4238
|
-
if (match) {
|
|
4239
|
-
const [, prefix = ``, serviceId] = match;
|
|
4240
|
-
url.pathname = `${prefix}/v1/stream-meta/subscriptions/${encodeURIComponent(subscriptionId)}`;
|
|
4241
|
-
url.searchParams.set(`service`, decodeURIComponent(serviceId));
|
|
4242
|
-
return url.toString();
|
|
4243
|
-
}
|
|
4244
|
-
url.pathname = `${url.pathname.replace(/\/+$/, ``)}/v1/stream-meta/subscriptions/${encodeURIComponent(subscriptionId)}`;
|
|
4232
|
+
url.pathname = `${url.pathname.replace(/\/+$/, ``)}/__ds/subscriptions/${encodeURIComponent(subscriptionId)}`;
|
|
4245
4233
|
return url.toString();
|
|
4246
4234
|
}
|
|
4247
4235
|
subscriptionChildUrl(subscriptionId, ...segments) {
|
|
@@ -4656,7 +4644,7 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
4656
4644
|
this.service = this.serviceId;
|
|
4657
4645
|
this.db = options.db;
|
|
4658
4646
|
if (options.streamClient) this.streamClient = options.streamClient;
|
|
4659
|
-
else if (options.durableStreamsUrl) this.streamClient = new StreamClient(durableStreamsServiceUrl(options.durableStreamsUrl, this.serviceId), { bearer: options.durableStreamsBearer });
|
|
4647
|
+
else if (options.durableStreamsUrl) this.streamClient = new StreamClient(durableStreamsServiceUrl(options.durableStreamsUrl, this.serviceId, { scope: `stream-root` }), { bearer: options.durableStreamsBearer });
|
|
4660
4648
|
else throw new Error(`Either durableStreamsUrl or streamClient is required`);
|
|
4661
4649
|
this.registry = options.registry ?? new PostgresRegistry(this.db, this.serviceId);
|
|
4662
4650
|
this.wakeRegistry = options.wakeRegistry;
|
|
@@ -5925,27 +5913,6 @@ function validateParsedBody(schema, parsed) {
|
|
|
5925
5913
|
};
|
|
5926
5914
|
}
|
|
5927
5915
|
|
|
5928
|
-
//#endregion
|
|
5929
|
-
//#region src/routing/tenant-stream-paths.ts
|
|
5930
|
-
function withoutLeadingSlash(path$1) {
|
|
5931
|
-
return path$1.replace(/^\/+/, ``);
|
|
5932
|
-
}
|
|
5933
|
-
function withLeadingSlash(path$1) {
|
|
5934
|
-
return path$1.startsWith(`/`) ? path$1 : `/${path$1}`;
|
|
5935
|
-
}
|
|
5936
|
-
function prefixTenantStreamPath(path$1, tenantId) {
|
|
5937
|
-
const normalized = withoutLeadingSlash(path$1);
|
|
5938
|
-
if (!normalized || normalized === tenantId) return tenantId;
|
|
5939
|
-
if (normalized.startsWith(`${tenantId}/`)) return normalized;
|
|
5940
|
-
return `${tenantId}/${normalized}`;
|
|
5941
|
-
}
|
|
5942
|
-
function stripTenantStreamPrefix(path$1, tenantId) {
|
|
5943
|
-
const normalized = withoutLeadingSlash(path$1);
|
|
5944
|
-
if (normalized === tenantId) return ``;
|
|
5945
|
-
if (normalized.startsWith(`${tenantId}/`)) return normalized.slice(tenantId.length + 1);
|
|
5946
|
-
return normalized;
|
|
5947
|
-
}
|
|
5948
|
-
|
|
5949
5916
|
//#endregion
|
|
5950
5917
|
//#region src/routing/durable-streams-routing-adapter.ts
|
|
5951
5918
|
function appendSearch(target, source) {
|
|
@@ -5956,43 +5923,30 @@ function removeServiceQuery(target) {
|
|
|
5956
5923
|
target.searchParams.delete(`service`);
|
|
5957
5924
|
return target;
|
|
5958
5925
|
}
|
|
5959
|
-
function
|
|
5960
|
-
|
|
5961
|
-
const segments = incomingUrl.pathname.split(`/`).filter(Boolean);
|
|
5962
|
-
if (segments[0] === `v1` && segments[1] === `stream`) return {
|
|
5963
|
-
incomingUrl,
|
|
5964
|
-
streamPath: segments.length > 2 ? `/${segments.slice(3).join(`/`)}` : `/`
|
|
5965
|
-
};
|
|
5966
|
-
return {
|
|
5967
|
-
incomingUrl,
|
|
5968
|
-
streamPath: incomingUrl.pathname || `/${serviceId}`
|
|
5969
|
-
};
|
|
5926
|
+
function withoutTrailingSlash(pathname) {
|
|
5927
|
+
return pathname.replace(/\/+$/, ``) || `/`;
|
|
5970
5928
|
}
|
|
5971
|
-
function
|
|
5972
|
-
const path$1 = backendStreamPath.replace(/^\/+/, ``);
|
|
5973
|
-
const target = new URL(`/v1/stream/${path$1}`, input.durableStreamsUrl);
|
|
5974
|
-
return target;
|
|
5975
|
-
}
|
|
5976
|
-
function streamMetaUrlWithoutService(input) {
|
|
5929
|
+
function appendRequestPathToStreamRoot(input) {
|
|
5977
5930
|
const incomingUrl = new URL(input.requestUrl, `http://localhost`);
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
return prefixTenantStreamPath(streamPath, serviceId);
|
|
5931
|
+
const path$1 = incomingUrl.pathname.replace(/^\/+/, ``);
|
|
5932
|
+
const target = new URL(input.durableStreamsUrl);
|
|
5933
|
+
target.pathname = path$1 ? `${withoutTrailingSlash(target.pathname)}/${path$1}` : withoutTrailingSlash(target.pathname);
|
|
5934
|
+
return removeServiceQuery(appendSearch(target, incomingUrl));
|
|
5935
|
+
}
|
|
5936
|
+
const streamRootDurableStreamsRoutingAdapter = {
|
|
5937
|
+
streamUrl: appendRequestPathToStreamRoot,
|
|
5938
|
+
controlUrl: appendRequestPathToStreamRoot,
|
|
5939
|
+
toBackendStreamPath(_serviceId, streamPath) {
|
|
5940
|
+
return streamPath.replace(/^\/+/, ``);
|
|
5989
5941
|
},
|
|
5990
|
-
toRuntimeStreamPath(
|
|
5991
|
-
return
|
|
5942
|
+
toRuntimeStreamPath(_serviceId, streamPath) {
|
|
5943
|
+
return streamPath.replace(/^\/+/, ``);
|
|
5992
5944
|
}
|
|
5993
5945
|
};
|
|
5994
|
-
|
|
5995
|
-
|
|
5946
|
+
const pathPrefixedSingleTenantDurableStreamsRoutingAdapter = streamRootDurableStreamsRoutingAdapter;
|
|
5947
|
+
const tenantRootDurableStreamsRoutingAdapter = streamRootDurableStreamsRoutingAdapter;
|
|
5948
|
+
function resolveDurableStreamsRoutingAdapter(adapter, _durableStreamsUrl) {
|
|
5949
|
+
return adapter ?? streamRootDurableStreamsRoutingAdapter;
|
|
5996
5950
|
}
|
|
5997
5951
|
|
|
5998
5952
|
//#endregion
|
|
@@ -6029,13 +5983,13 @@ function buildElectricProxyTarget(options) {
|
|
|
6029
5983
|
return target;
|
|
6030
5984
|
}
|
|
6031
5985
|
async function forwardFetchRequest(options) {
|
|
6032
|
-
const routingAdapter = resolveDurableStreamsRoutingAdapter(options.durableStreamsRouting);
|
|
5986
|
+
const routingAdapter = resolveDurableStreamsRoutingAdapter(options.durableStreamsRouting, options.durableStreamsUrl);
|
|
6033
5987
|
const routingInput = {
|
|
6034
5988
|
durableStreamsUrl: options.durableStreamsUrl,
|
|
6035
5989
|
serviceId: options.serviceId,
|
|
6036
5990
|
requestUrl: options.request.url
|
|
6037
5991
|
};
|
|
6038
|
-
const upstreamUrl = options.route === `
|
|
5992
|
+
const upstreamUrl = options.route === `control` ? routingAdapter.controlUrl(routingInput) : routingAdapter.streamUrl(routingInput);
|
|
6039
5993
|
const headers = new Headers(options.request.headers);
|
|
6040
5994
|
if (options.durableStreamsBearerMode !== `none`) await applyDurableStreamsBearer(headers, options.durableStreamsBearer, { overwrite: options.durableStreamsBearerMode !== `if-missing` });
|
|
6041
5995
|
const init = {
|
|
@@ -6073,8 +6027,21 @@ function sqlStringLiteral(value) {
|
|
|
6073
6027
|
//#endregion
|
|
6074
6028
|
//#region src/routing/durable-streams-router.ts
|
|
6075
6029
|
const subscriptionProxyBodySchema = Type.Object({ webhook: Type.Optional(Type.Object({ url: Type.String() }, { additionalProperties: true })) }, { additionalProperties: true });
|
|
6030
|
+
const subscriptionControlActions = [
|
|
6031
|
+
`callback`,
|
|
6032
|
+
`claim`,
|
|
6033
|
+
`ack`,
|
|
6034
|
+
`release`
|
|
6035
|
+
];
|
|
6076
6036
|
const durableStreamsRouter = Router();
|
|
6077
|
-
durableStreamsRouter.
|
|
6037
|
+
durableStreamsRouter.put(`/__ds/subscriptions/:subscriptionId`, putSubscriptionBase);
|
|
6038
|
+
durableStreamsRouter.get(`/__ds/subscriptions/:subscriptionId`, getSubscriptionBase);
|
|
6039
|
+
durableStreamsRouter.delete(`/__ds/subscriptions/:subscriptionId`, deleteSubscriptionBase);
|
|
6040
|
+
durableStreamsRouter.post(`/__ds/subscriptions/:subscriptionId/streams`, postSubscriptionStreams);
|
|
6041
|
+
durableStreamsRouter.delete(`/__ds/subscriptions/:subscriptionId/streams/:streamPath+`, deleteSubscriptionStream);
|
|
6042
|
+
for (const action of subscriptionControlActions) durableStreamsRouter.post(`/__ds/subscriptions/:subscriptionId/${action}`, subscriptionAction(action));
|
|
6043
|
+
durableStreamsRouter.all(`/__ds`, controlPassThrough);
|
|
6044
|
+
durableStreamsRouter.all(`/__ds/*`, controlPassThrough);
|
|
6078
6045
|
durableStreamsRouter.post(`*`, streamAppend);
|
|
6079
6046
|
durableStreamsRouter.all(`*`, proxyPassThrough);
|
|
6080
6047
|
function bodyFromBytes$1(body) {
|
|
@@ -6087,7 +6054,7 @@ function responseFromUpstream$1(response, body) {
|
|
|
6087
6054
|
headers: responseHeaders(response)
|
|
6088
6055
|
});
|
|
6089
6056
|
}
|
|
6090
|
-
async function forwardToDurableStreams(ctx, request, body, route = `stream`, urlOverride) {
|
|
6057
|
+
async function forwardToDurableStreams(ctx, request, body, route = `stream`, urlOverride, durableStreamsBearerMode = `overwrite`) {
|
|
6091
6058
|
const headers = new Headers(request.headers);
|
|
6092
6059
|
headers.delete(`host`);
|
|
6093
6060
|
let requestBody = body;
|
|
@@ -6101,24 +6068,13 @@ async function forwardToDurableStreams(ctx, request, body, route = `stream`, url
|
|
|
6101
6068
|
body: requestBody,
|
|
6102
6069
|
durableStreamsUrl: ctx.durableStreamsUrl,
|
|
6103
6070
|
durableStreamsBearer: ctx.durableStreamsBearer,
|
|
6104
|
-
durableStreamsBearerMode
|
|
6071
|
+
durableStreamsBearerMode,
|
|
6105
6072
|
durableStreamsRouting: ctx.durableStreamsRouting,
|
|
6106
6073
|
serviceId: ctx.service,
|
|
6107
6074
|
dispatcher: ctx.durableStreamsDispatcher,
|
|
6108
6075
|
route
|
|
6109
6076
|
});
|
|
6110
6077
|
}
|
|
6111
|
-
function subscriptionIdFromPath(pathname) {
|
|
6112
|
-
const match = /^\/v1\/stream-meta\/subscriptions\/([^/]+)(?:\/.*)?$/.exec(pathname);
|
|
6113
|
-
return match ? decodeURIComponent(match[1]) : null;
|
|
6114
|
-
}
|
|
6115
|
-
function isSubscriptionBasePath(pathname) {
|
|
6116
|
-
return /^\/v1\/stream-meta\/subscriptions\/[^/]+\/?$/.test(pathname);
|
|
6117
|
-
}
|
|
6118
|
-
function usesSubscriptionScopedBearer(requestUrl) {
|
|
6119
|
-
const pathname = new URL(requestUrl, `http://localhost`).pathname;
|
|
6120
|
-
return /^\/v1\/stream-meta\/subscriptions\/[^/]+\/(?:ack|release|callback)\/?$/.test(pathname);
|
|
6121
|
-
}
|
|
6122
6078
|
function rewriteSubscriptionBodyForBackend(payload, service, routingAdapter) {
|
|
6123
6079
|
if (typeof payload.pattern === `string`) payload.pattern = routingAdapter.toBackendStreamPath(service, payload.pattern);
|
|
6124
6080
|
if (Array.isArray(payload.streams)) payload.streams = payload.streams.map((stream) => typeof stream === `string` ? routingAdapter.toBackendStreamPath(service, stream) : stream);
|
|
@@ -6163,44 +6119,50 @@ function decodeJson(bytes) {
|
|
|
6163
6119
|
return null;
|
|
6164
6120
|
}
|
|
6165
6121
|
}
|
|
6166
|
-
function
|
|
6167
|
-
const
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
return
|
|
6122
|
+
function routeParam$2(request, name) {
|
|
6123
|
+
const value = request.params[name];
|
|
6124
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
6125
|
+
return decodeURIComponent(raw ?? ``);
|
|
6126
|
+
}
|
|
6127
|
+
function subscriptionRoutingAdapter(ctx) {
|
|
6128
|
+
return resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl);
|
|
6173
6129
|
}
|
|
6174
|
-
async function
|
|
6175
|
-
const
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6130
|
+
async function rewriteSubscriptionRequestBody(request, ctx, subscriptionId, routingAdapter) {
|
|
6131
|
+
const body = await readRequestBody(request);
|
|
6132
|
+
if (body.length === 0) return {
|
|
6133
|
+
ok: true,
|
|
6134
|
+
body,
|
|
6135
|
+
targetWebhookUrl: null
|
|
6136
|
+
};
|
|
6137
|
+
const validation = validateBody(subscriptionProxyBodySchema, body);
|
|
6138
|
+
if (!validation.ok) return {
|
|
6139
|
+
ok: false,
|
|
6140
|
+
response: validation.response
|
|
6141
|
+
};
|
|
6142
|
+
const payload = validation.value;
|
|
6180
6143
|
let targetWebhookUrl = null;
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
if (requestBody.length > 0) {
|
|
6185
|
-
const validation = validateBody(subscriptionProxyBodySchema, requestBody);
|
|
6186
|
-
if (!validation.ok) return validation.response;
|
|
6187
|
-
const payload = validation.value;
|
|
6188
|
-
if (payload.webhook?.url !== void 0) {
|
|
6189
|
-
targetWebhookUrl = rewriteLoopbackWebhookUrl(payload.webhook.url) ?? null;
|
|
6190
|
-
payload.webhook.url = appendPathToUrl(ctx.publicUrl, `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`);
|
|
6191
|
-
}
|
|
6192
|
-
rewriteSubscriptionBodyForBackend(payload, ctx.service, routingAdapter);
|
|
6193
|
-
requestBody = new TextEncoder().encode(JSON.stringify(payload));
|
|
6194
|
-
}
|
|
6144
|
+
if (payload.webhook?.url !== void 0) {
|
|
6145
|
+
targetWebhookUrl = rewriteLoopbackWebhookUrl(payload.webhook.url) ?? null;
|
|
6146
|
+
payload.webhook.url = appendPathToUrl(ctx.publicUrl, `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`);
|
|
6195
6147
|
}
|
|
6196
|
-
|
|
6197
|
-
|
|
6148
|
+
rewriteSubscriptionBodyForBackend(payload, ctx.service, routingAdapter);
|
|
6149
|
+
return {
|
|
6150
|
+
ok: true,
|
|
6151
|
+
body: new TextEncoder().encode(JSON.stringify(payload)),
|
|
6152
|
+
targetWebhookUrl
|
|
6153
|
+
};
|
|
6154
|
+
}
|
|
6155
|
+
async function forwardSubscriptionRequest(request, ctx, routingAdapter, opts = {}) {
|
|
6156
|
+
const upstream = await forwardToDurableStreams(ctx, request, opts.body, `control`, opts.requestUrl, opts.bearerMode ?? `overwrite`);
|
|
6198
6157
|
let responseBytes = upstream.body ? new Uint8Array(await upstream.arrayBuffer()) : new Uint8Array();
|
|
6199
6158
|
responseBytes = rewriteSubscriptionResponseForClient(responseBytes, upstream, ctx.service, routingAdapter);
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6159
|
+
return {
|
|
6160
|
+
upstream,
|
|
6161
|
+
response: responseFromUpstream$1(upstream, responseBytes)
|
|
6162
|
+
};
|
|
6163
|
+
}
|
|
6164
|
+
async function upsertSubscriptionWebhook(ctx, subscriptionId, targetWebhookUrl) {
|
|
6165
|
+
await ctx.pgDb.insert(subscriptionWebhooks).values({
|
|
6204
6166
|
tenantId: ctx.service,
|
|
6205
6167
|
subscriptionId,
|
|
6206
6168
|
webhookUrl: targetWebhookUrl
|
|
@@ -6208,8 +6170,64 @@ async function subscriptionProxy(request, ctx) {
|
|
|
6208
6170
|
target: [subscriptionWebhooks.tenantId, subscriptionWebhooks.subscriptionId],
|
|
6209
6171
|
set: { webhookUrl: targetWebhookUrl }
|
|
6210
6172
|
});
|
|
6173
|
+
}
|
|
6174
|
+
async function deleteSubscriptionWebhook(ctx, subscriptionId) {
|
|
6175
|
+
await ctx.pgDb.delete(subscriptionWebhooks).where(and(eq(subscriptionWebhooks.tenantId, ctx.service), eq(subscriptionWebhooks.subscriptionId, subscriptionId)));
|
|
6176
|
+
}
|
|
6177
|
+
function rewriteSubscriptionStreamPathInUrl(requestUrl, service, routingAdapter, streamPath) {
|
|
6178
|
+
const prefix = requestUrl.pathname.slice(0, requestUrl.pathname.indexOf(`/streams/`) + `/streams/`.length);
|
|
6179
|
+
requestUrl.pathname = `${prefix}${encodeURIComponent(routingAdapter.toBackendStreamPath(service, streamPath))}`;
|
|
6180
|
+
return requestUrl.toString();
|
|
6181
|
+
}
|
|
6182
|
+
async function putSubscriptionBase(request, ctx) {
|
|
6183
|
+
const subscriptionId = routeParam$2(request, `subscriptionId`);
|
|
6184
|
+
const routingAdapter = subscriptionRoutingAdapter(ctx);
|
|
6185
|
+
const rewrite = await rewriteSubscriptionRequestBody(request, ctx, subscriptionId, routingAdapter);
|
|
6186
|
+
if (!rewrite.ok) return rewrite.response;
|
|
6187
|
+
const { upstream, response } = await forwardSubscriptionRequest(request, ctx, routingAdapter, { body: rewrite.body });
|
|
6188
|
+
if (upstream.ok && rewrite.targetWebhookUrl) await upsertSubscriptionWebhook(ctx, subscriptionId, rewrite.targetWebhookUrl);
|
|
6189
|
+
return response;
|
|
6190
|
+
}
|
|
6191
|
+
async function getSubscriptionBase(request, ctx) {
|
|
6192
|
+
const routingAdapter = subscriptionRoutingAdapter(ctx);
|
|
6193
|
+
return (await forwardSubscriptionRequest(request, ctx, routingAdapter)).response;
|
|
6194
|
+
}
|
|
6195
|
+
async function deleteSubscriptionBase(request, ctx) {
|
|
6196
|
+
const subscriptionId = routeParam$2(request, `subscriptionId`);
|
|
6197
|
+
const routingAdapter = subscriptionRoutingAdapter(ctx);
|
|
6198
|
+
const { upstream, response } = await forwardSubscriptionRequest(request, ctx, routingAdapter);
|
|
6199
|
+
if (upstream.ok) await deleteSubscriptionWebhook(ctx, subscriptionId);
|
|
6211
6200
|
return response;
|
|
6212
6201
|
}
|
|
6202
|
+
async function postSubscriptionStreams(request, ctx) {
|
|
6203
|
+
const subscriptionId = routeParam$2(request, `subscriptionId`);
|
|
6204
|
+
const routingAdapter = subscriptionRoutingAdapter(ctx);
|
|
6205
|
+
const rewrite = await rewriteSubscriptionRequestBody(request, ctx, subscriptionId, routingAdapter);
|
|
6206
|
+
if (!rewrite.ok) return rewrite.response;
|
|
6207
|
+
return (await forwardSubscriptionRequest(request, ctx, routingAdapter, { body: rewrite.body })).response;
|
|
6208
|
+
}
|
|
6209
|
+
async function deleteSubscriptionStream(request, ctx) {
|
|
6210
|
+
const routingAdapter = subscriptionRoutingAdapter(ctx);
|
|
6211
|
+
const requestUrl = rewriteSubscriptionStreamPathInUrl(new URL(request.url), ctx.service, routingAdapter, routeParam$2(request, `streamPath`));
|
|
6212
|
+
return (await forwardSubscriptionRequest(request, ctx, routingAdapter, { requestUrl })).response;
|
|
6213
|
+
}
|
|
6214
|
+
function subscriptionAction(action) {
|
|
6215
|
+
return async (request, ctx) => {
|
|
6216
|
+
const subscriptionId = routeParam$2(request, `subscriptionId`);
|
|
6217
|
+
const routingAdapter = subscriptionRoutingAdapter(ctx);
|
|
6218
|
+
const rewrite = await rewriteSubscriptionRequestBody(request, ctx, subscriptionId, routingAdapter);
|
|
6219
|
+
if (!rewrite.ok) return rewrite.response;
|
|
6220
|
+
const bearerMode = action === `ack` || action === `release` || action === `callback` ? `if-missing` : `overwrite`;
|
|
6221
|
+
return (await forwardSubscriptionRequest(request, ctx, routingAdapter, {
|
|
6222
|
+
body: rewrite.body,
|
|
6223
|
+
bearerMode
|
|
6224
|
+
})).response;
|
|
6225
|
+
};
|
|
6226
|
+
}
|
|
6227
|
+
async function controlPassThrough(request, ctx) {
|
|
6228
|
+
const upstream = await forwardToDurableStreams(ctx, request, void 0, `control`);
|
|
6229
|
+
return responseFromUpstream$1(upstream);
|
|
6230
|
+
}
|
|
6213
6231
|
async function streamAppend(request, ctx) {
|
|
6214
6232
|
return await electricAgentsStreamAppendRouter.fetch(createStreamAppendRouteRequest(request), ctx.runtime, (req, body) => forwardFetchRequest({
|
|
6215
6233
|
request: {
|
|
@@ -6230,10 +6248,9 @@ async function proxyPassThrough(request, ctx) {
|
|
|
6230
6248
|
const upstream = await forwardToDurableStreams(ctx, request);
|
|
6231
6249
|
const streamPath = new URL(request.url).pathname;
|
|
6232
6250
|
const method = request.method.toUpperCase();
|
|
6233
|
-
const
|
|
6234
|
-
const endTrackedRead = method === `GET` && !isControlPath ? await ctx.entityBridgeManager.beginClientRead(streamPath) : null;
|
|
6251
|
+
const endTrackedRead = method === `GET` ? await ctx.entityBridgeManager.beginClientRead(streamPath) : null;
|
|
6235
6252
|
try {
|
|
6236
|
-
if (method === `HEAD`
|
|
6253
|
+
if (method === `HEAD`) await ctx.entityBridgeManager.touchByStreamPath(streamPath);
|
|
6237
6254
|
return responseFromUpstream$1(upstream);
|
|
6238
6255
|
} finally {
|
|
6239
6256
|
await endTrackedRead?.();
|
|
@@ -6780,6 +6797,12 @@ function getRequestSpan(req) {
|
|
|
6780
6797
|
return carrier(req)[SPAN_KEY];
|
|
6781
6798
|
}
|
|
6782
6799
|
|
|
6800
|
+
//#endregion
|
|
6801
|
+
//#region src/routing/tenant-stream-paths.ts
|
|
6802
|
+
function withLeadingSlash(path$1) {
|
|
6803
|
+
return path$1.startsWith(`/`) ? path$1 : `/${path$1}`;
|
|
6804
|
+
}
|
|
6805
|
+
|
|
6783
6806
|
//#endregion
|
|
6784
6807
|
//#region src/routing/runners-router.ts
|
|
6785
6808
|
const registerRunnerBodySchema = Type.Object({
|
|
@@ -7103,7 +7126,7 @@ async function webhookForward(request, ctx) {
|
|
|
7103
7126
|
let runningEntityUrl = null;
|
|
7104
7127
|
const parsedBody = parsedBodyResult.value;
|
|
7105
7128
|
const newWebhook = newWebhookPayload(parsedBody);
|
|
7106
|
-
const routingAdapter = resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting);
|
|
7129
|
+
const routingAdapter = resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl);
|
|
7107
7130
|
if (parsedBody) {
|
|
7108
7131
|
const rawPrimaryStream = newWebhook?.primaryStream ?? parsedBody.primary_stream ?? parsedBody.primaryStream ?? parsedBody.streamPath ?? null;
|
|
7109
7132
|
const primaryStream = typeof rawPrimaryStream === `string` ? toRuntimeStreamPath(rawPrimaryStream, ctx.service, routingAdapter) : null;
|
|
@@ -7232,7 +7255,7 @@ async function callbackForward(request, ctx) {
|
|
|
7232
7255
|
}
|
|
7233
7256
|
return json(responseBody);
|
|
7234
7257
|
}
|
|
7235
|
-
const upstreamBody = encodeCallbackForwardBody(ctx.service, consumerId, requestBody, resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting));
|
|
7258
|
+
const upstreamBody = encodeCallbackForwardBody(ctx.service, consumerId, requestBody, resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl));
|
|
7236
7259
|
let upstream;
|
|
7237
7260
|
try {
|
|
7238
7261
|
const subscriptionId = durableStreamsSubscriptionCallback(target.callbackUrl);
|
|
@@ -7343,4 +7366,4 @@ globalRouter.all(`/_electric/*`, internalRouter.fetch);
|
|
|
7343
7366
|
globalRouter.all(`*`, durableStreamsRouter.fetch);
|
|
7344
7367
|
|
|
7345
7368
|
//#endregion
|
|
7346
|
-
export { AgentsHost, DEFAULT_TENANT_ID, StreamClient, UnregisteredTenantError, createDb, globalRouter, isUnregisteredTenantError, pathPrefixedSingleTenantDurableStreamsRoutingAdapter, runMigrations };
|
|
7369
|
+
export { AgentsHost, DEFAULT_TENANT_ID, StreamClient, UnregisteredTenantError, createDb, globalRouter, isUnregisteredTenantError, pathPrefixedSingleTenantDurableStreamsRoutingAdapter, runMigrations, streamRootDurableStreamsRoutingAdapter, tenantRootDurableStreamsRoutingAdapter };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@electric-ax/agents-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Electric Agents entity runtime server",
|
|
5
5
|
"author": "Durable Stream contributors",
|
|
6
6
|
"bin": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@durable-streams/client": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/client@350",
|
|
40
40
|
"@durable-streams/server": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/server@350",
|
|
41
41
|
"@durable-streams/state": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/state@350",
|
|
42
|
-
"@electric-sql/client": "^1.5.
|
|
42
|
+
"@electric-sql/client": "^1.5.18",
|
|
43
43
|
"@mariozechner/pi-agent-core": "^0.70.2",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1",
|
|
45
45
|
"@sinclair/typebox": "^0.34.48",
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
"tsx": "^4.19.0",
|
|
66
66
|
"typescript": "^5.0.0",
|
|
67
67
|
"vitest": "^4.1.0",
|
|
68
|
-
"@electric-ax/agents": "0.4.
|
|
69
|
-
"@electric-ax/agents-server-conformance-tests": "0.1.
|
|
70
|
-
"@electric-ax/agents-server-ui": "0.4.
|
|
68
|
+
"@electric-ax/agents": "0.4.2",
|
|
69
|
+
"@electric-ax/agents-server-conformance-tests": "0.1.5",
|
|
70
|
+
"@electric-ax/agents-server-ui": "0.4.3"
|
|
71
71
|
},
|
|
72
72
|
"files": [
|
|
73
73
|
"dist",
|
package/src/db/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ export function createDb(postgresUrl: string): {
|
|
|
16
16
|
const poolMax = Number(process.env.ELECTRIC_AGENTS_PG_POOL_MAX ?? `100`)
|
|
17
17
|
const client = postgres(postgresUrl, {
|
|
18
18
|
max: poolMax,
|
|
19
|
-
fetch_types:
|
|
19
|
+
fetch_types: true,
|
|
20
20
|
})
|
|
21
21
|
const db = drizzle(client, { schema })
|
|
22
22
|
return { db, client }
|
package/src/index.ts
CHANGED
|
@@ -37,7 +37,11 @@ export type { Principal, PrincipalKind } from './principal.js'
|
|
|
37
37
|
export { globalRouter } from './routing/global-router.js'
|
|
38
38
|
export type { GlobalRoutes } from './routing/global-router.js'
|
|
39
39
|
export type { TenantContext } from './routing/context.js'
|
|
40
|
-
export {
|
|
40
|
+
export {
|
|
41
|
+
streamRootDurableStreamsRoutingAdapter,
|
|
42
|
+
pathPrefixedSingleTenantDurableStreamsRoutingAdapter,
|
|
43
|
+
tenantRootDurableStreamsRoutingAdapter,
|
|
44
|
+
} from './routing/durable-streams-routing-adapter.js'
|
|
41
45
|
export type {
|
|
42
46
|
DurableStreamsRoutingAdapter,
|
|
43
47
|
DurableStreamsRoutingInput,
|
package/src/routing/context.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface TenantContext {
|
|
|
19
19
|
principal: Principal
|
|
20
20
|
publicUrl: string
|
|
21
21
|
localUrl?: string
|
|
22
|
+
/** Resolved Durable Streams root URL for this tenant. */
|
|
22
23
|
durableStreamsUrl: string
|
|
23
24
|
durableStreamsBearer?: DurableStreamsBearerProvider
|
|
24
25
|
durableStreamsRouting?: DurableStreamsRoutingAdapter
|