@electric-ax/agents-server 0.4.7 → 0.4.11
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 +198 -83
- package/dist/index.cjs +195 -82
- package/dist/index.d.cts +26 -7
- package/dist/index.d.ts +26 -7
- package/dist/index.js +196 -83
- package/package.json +6 -6
- package/src/electric-agents/default-entity-schemas.ts +1 -1
- package/src/electric-agents-types.ts +2 -1
- package/src/entity-manager.ts +69 -5
- package/src/index.ts +9 -1
- package/src/manifest-side-effects.ts +11 -0
- package/src/routing/context.ts +18 -1
- package/src/routing/dispatch-policy.ts +1 -1
- package/src/routing/durable-streams-router.ts +1 -1
- package/src/routing/durable-streams-routing-adapter.ts +1 -3
- package/src/routing/entities-router.ts +133 -24
- package/src/routing/entity-types-router.ts +23 -26
- package/src/routing/hooks.ts +1 -1
- package/src/routing/internal-router.ts +83 -38
- package/src/routing/observations-router.ts +74 -0
- package/src/routing/runners-router.ts +1 -1
- package/src/server.ts +12 -0
- package/src/wake-registry.ts +1 -1
- package/src/routing/cron-router.ts +0 -45
package/dist/index.cjs
CHANGED
|
@@ -451,7 +451,7 @@ const ErrCodeForkInProgress = `FORK_IN_PROGRESS`;
|
|
|
451
451
|
const ErrCodeForkWaitTimeout = `FORK_WAIT_TIMEOUT`;
|
|
452
452
|
const ErrCodeEntityPersistFailed = `ENTITY_PERSIST_FAILED`;
|
|
453
453
|
const ErrCodeSubscriptionNotFound = `SUBSCRIPTION_NOT_FOUND`;
|
|
454
|
-
const
|
|
454
|
+
const ErrCodeWakeCallbackNotFound = `WAKE_CALLBACK_NOT_FOUND`;
|
|
455
455
|
|
|
456
456
|
//#endregion
|
|
457
457
|
//#region src/tenant.ts
|
|
@@ -2532,7 +2532,7 @@ async function linkStreamToTargetSubscription(ctx, target, entity, subscriptionI
|
|
|
2532
2532
|
}
|
|
2533
2533
|
const webhookUrl = rewriteLoopbackWebhookUrl(target.url);
|
|
2534
2534
|
if (!webhookUrl) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Webhook dispatch target must include a valid URL`, 400);
|
|
2535
|
-
const forwardUrl = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/
|
|
2535
|
+
const forwardUrl = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/subscription-webhooks/${encodeURIComponent(subscriptionId)}`);
|
|
2536
2536
|
await ensureSubscriptionIncludesStream(ctx, subscriptionId, streamPath, {
|
|
2537
2537
|
type: `webhook`,
|
|
2538
2538
|
streams: [streamPath],
|
|
@@ -2653,6 +2653,10 @@ function extractManifestSourceUrl(manifest) {
|
|
|
2653
2653
|
}
|
|
2654
2654
|
if (manifest.sourceType === `entities`) return typeof manifest.sourceRef === `string` ? `/_entities/${manifest.sourceRef}` : void 0;
|
|
2655
2655
|
if (manifest.sourceType === `db`) return typeof manifest.sourceRef === `string` ? (0, __electric_ax_agents_runtime.getSharedStateStreamPath)(manifest.sourceRef) : void 0;
|
|
2656
|
+
if (manifest.sourceType === `webhook`) {
|
|
2657
|
+
if (typeof config?.streamUrl === `string`) return config.streamUrl;
|
|
2658
|
+
if (typeof config?.endpointKey === `string`) return (0, __electric_ax_agents_runtime.getWebhookStreamPath)(config.endpointKey, typeof config.bucket === `string` ? config.bucket : void 0);
|
|
2659
|
+
}
|
|
2656
2660
|
return void 0;
|
|
2657
2661
|
}
|
|
2658
2662
|
if (manifest.kind === `shared-state`) return typeof manifest.id === `string` ? (0, __electric_ax_agents_runtime.getSharedStateStreamPath)(manifest.id) : void 0;
|
|
@@ -2939,7 +2943,8 @@ var EntityManager = class {
|
|
|
2939
2943
|
debounceMs: req.wake.debounceMs,
|
|
2940
2944
|
timeoutMs: req.wake.timeoutMs,
|
|
2941
2945
|
oneShot: false,
|
|
2942
|
-
includeResponse: req.wake.includeResponse
|
|
2946
|
+
includeResponse: req.wake.includeResponse,
|
|
2947
|
+
manifestKey: req.wake.manifestKey
|
|
2943
2948
|
});
|
|
2944
2949
|
const contentType = `application/json`;
|
|
2945
2950
|
const createdEvent = __electric_ax_agents_runtime.entityStateSchema.entityCreated.insert({
|
|
@@ -3612,7 +3617,7 @@ var EntityManager = class {
|
|
|
3612
3617
|
};
|
|
3613
3618
|
}
|
|
3614
3619
|
/**
|
|
3615
|
-
* Deliver a message to an entity's main stream, with optional
|
|
3620
|
+
* Deliver a message to an entity's main stream, with optional inbox schema
|
|
3616
3621
|
* validation.
|
|
3617
3622
|
*/
|
|
3618
3623
|
async send(entityUrl, req, opts) {
|
|
@@ -3700,7 +3705,7 @@ var EntityManager = class {
|
|
|
3700
3705
|
if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
3701
3706
|
return updated;
|
|
3702
3707
|
}
|
|
3703
|
-
async
|
|
3708
|
+
async deleteTag(entityUrl, key, token) {
|
|
3704
3709
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3705
3710
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3706
3711
|
if (!this.isValidWriteToken(entity, token)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Invalid write token`, 401);
|
|
@@ -3711,7 +3716,7 @@ var EntityManager = class {
|
|
|
3711
3716
|
if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
3712
3717
|
return updated;
|
|
3713
3718
|
}
|
|
3714
|
-
async
|
|
3719
|
+
async ensureEntitiesMembershipStream(tags) {
|
|
3715
3720
|
if (!this.entityBridgeManager) throw new Error(`Entity bridge manager not configured`);
|
|
3716
3721
|
return this.entityBridgeManager.register(this.validateTags(tags));
|
|
3717
3722
|
}
|
|
@@ -3833,6 +3838,35 @@ var EntityManager = class {
|
|
|
3833
3838
|
await this.writeManifestEntry(entityUrl, manifestKey, `delete`, void 0, { txid });
|
|
3834
3839
|
return { txid };
|
|
3835
3840
|
}
|
|
3841
|
+
async upsertEventSourceSubscription(entityUrl, req) {
|
|
3842
|
+
const manifestKey = req.subscription.manifestKey;
|
|
3843
|
+
const txid = (0, node_crypto.randomUUID)();
|
|
3844
|
+
await this.writeManifestEntry(entityUrl, manifestKey, `upsert`, req.manifest, { txid });
|
|
3845
|
+
await this.wakeRegistry.unregisterByManifestKey(entityUrl, manifestKey, this.tenantId);
|
|
3846
|
+
await this.wakeRegistry.register({
|
|
3847
|
+
tenantId: this.tenantId,
|
|
3848
|
+
subscriberUrl: entityUrl,
|
|
3849
|
+
sourceUrl: req.subscription.sourceUrl,
|
|
3850
|
+
condition: {
|
|
3851
|
+
on: `change`,
|
|
3852
|
+
collections: [`webhook_event`],
|
|
3853
|
+
ops: [`insert`]
|
|
3854
|
+
},
|
|
3855
|
+
oneShot: false,
|
|
3856
|
+
manifestKey
|
|
3857
|
+
});
|
|
3858
|
+
return {
|
|
3859
|
+
txid,
|
|
3860
|
+
subscription: req.subscription
|
|
3861
|
+
};
|
|
3862
|
+
}
|
|
3863
|
+
async deleteEventSourceSubscription(entityUrl, req) {
|
|
3864
|
+
const manifestKey = (0, __electric_ax_agents_runtime.eventSourceSubscriptionManifestKey)(req.id);
|
|
3865
|
+
const txid = (0, node_crypto.randomUUID)();
|
|
3866
|
+
await this.writeManifestEntry(entityUrl, manifestKey, `delete`, void 0, { txid });
|
|
3867
|
+
await this.wakeRegistry.unregisterByManifestKey(entityUrl, manifestKey, this.tenantId);
|
|
3868
|
+
return { txid };
|
|
3869
|
+
}
|
|
3836
3870
|
/**
|
|
3837
3871
|
* Register a wake subscription from a subscriber to a source entity.
|
|
3838
3872
|
*/
|
|
@@ -4111,7 +4145,7 @@ var EntityManager = class {
|
|
|
4111
4145
|
return null;
|
|
4112
4146
|
}
|
|
4113
4147
|
/**
|
|
4114
|
-
* Add new
|
|
4148
|
+
* Add new inbox/state schema keys to an entity type directly in Postgres.
|
|
4115
4149
|
*/
|
|
4116
4150
|
async amendSchemas(typeName, schemas) {
|
|
4117
4151
|
if (typeName === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be amended`, 400);
|
|
@@ -4151,7 +4185,7 @@ var EntityManager = class {
|
|
|
4151
4185
|
}
|
|
4152
4186
|
/**
|
|
4153
4187
|
* Enrich webhook payload with entity context.
|
|
4154
|
-
* Called by ElectricAgentsServer during webhook
|
|
4188
|
+
* Called by ElectricAgentsServer during subscription webhook dispatch to inject entity context.
|
|
4155
4189
|
*/
|
|
4156
4190
|
async enrichPayload(payload, consumer) {
|
|
4157
4191
|
const entity = await this.registry.getEntityByStream(consumer.primary_stream);
|
|
@@ -5410,7 +5444,7 @@ var WakeRegistry = class {
|
|
|
5410
5444
|
try {
|
|
5411
5445
|
for (const message of messages) {
|
|
5412
5446
|
await this.applyShapeMessage(message);
|
|
5413
|
-
if (!settled &&
|
|
5447
|
+
if (!settled && (0, __electric_sql_client.isControlMessage)(message) && message.headers.control === `up-to-date`) {
|
|
5414
5448
|
settled = true;
|
|
5415
5449
|
resolve$1();
|
|
5416
5450
|
}
|
|
@@ -6197,7 +6231,7 @@ function validateParsedBody(schema, parsed) {
|
|
|
6197
6231
|
//#region src/routing/durable-streams-routing-adapter.ts
|
|
6198
6232
|
function appendSearch(target, source) {
|
|
6199
6233
|
source.searchParams.forEach((value, key) => {
|
|
6200
|
-
|
|
6234
|
+
target.searchParams.append(key, value);
|
|
6201
6235
|
});
|
|
6202
6236
|
return target;
|
|
6203
6237
|
}
|
|
@@ -6517,7 +6551,7 @@ async function rewriteSubscriptionRequestBody(request, ctx, subscriptionId, rout
|
|
|
6517
6551
|
let targetWebhookUrl = null;
|
|
6518
6552
|
if (payload.webhook?.url !== void 0) {
|
|
6519
6553
|
targetWebhookUrl = rewriteLoopbackWebhookUrl(payload.webhook.url) ?? null;
|
|
6520
|
-
payload.webhook.url = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/
|
|
6554
|
+
payload.webhook.url = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/subscription-webhooks/${encodeURIComponent(subscriptionId)}`);
|
|
6521
6555
|
}
|
|
6522
6556
|
rewriteSubscriptionBodyForBackend(payload, ctx.service, routingAdapter);
|
|
6523
6557
|
return {
|
|
@@ -6640,20 +6674,6 @@ async function proxyPassThrough(request, ctx) {
|
|
|
6640
6674
|
}
|
|
6641
6675
|
}
|
|
6642
6676
|
|
|
6643
|
-
//#endregion
|
|
6644
|
-
//#region src/routing/cron-router.ts
|
|
6645
|
-
const cronRegisterBodySchema = __sinclair_typebox.Type.Object({
|
|
6646
|
-
expression: __sinclair_typebox.Type.String(),
|
|
6647
|
-
timezone: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
6648
|
-
});
|
|
6649
|
-
const cronRouter = (0, itty_router.Router)({ base: `/_electric/cron` });
|
|
6650
|
-
cronRouter.post(`/register`, withSchema(cronRegisterBodySchema), registerCron);
|
|
6651
|
-
async function registerCron(request, ctx) {
|
|
6652
|
-
const parsed = routeBody(request);
|
|
6653
|
-
const streamPath = await ctx.entityManager.getOrCreateCronStream(parsed.expression, parsed.timezone);
|
|
6654
|
-
return (0, itty_router.json)({ streamUrl: streamPath });
|
|
6655
|
-
}
|
|
6656
|
-
|
|
6657
6677
|
//#endregion
|
|
6658
6678
|
//#region src/routing/electric-proxy-router.ts
|
|
6659
6679
|
const electricProxyRouter = (0, itty_router.Router)({ base: `/_electric/electric` });
|
|
@@ -6687,7 +6707,7 @@ async function proxyElectric(request, ctx) {
|
|
|
6687
6707
|
|
|
6688
6708
|
//#endregion
|
|
6689
6709
|
//#region src/routing/entities-router.ts
|
|
6690
|
-
const stringRecordSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.String());
|
|
6710
|
+
const stringRecordSchema$1 = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.String());
|
|
6691
6711
|
function writeTokenFromRequest(request) {
|
|
6692
6712
|
const electricClaimToken = request.headers.get(`electric-claim-token`)?.trim();
|
|
6693
6713
|
if (electricClaimToken) return electricClaimToken;
|
|
@@ -6704,7 +6724,7 @@ const wakeConditionSchema = __sinclair_typebox.Type.Union([__sinclair_typebox.Ty
|
|
|
6704
6724
|
})]);
|
|
6705
6725
|
const spawnBodySchema = __sinclair_typebox.Type.Object({
|
|
6706
6726
|
args: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
|
|
6707
|
-
tags: __sinclair_typebox.Type.Optional(stringRecordSchema),
|
|
6727
|
+
tags: __sinclair_typebox.Type.Optional(stringRecordSchema$1),
|
|
6708
6728
|
parent: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6709
6729
|
dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema),
|
|
6710
6730
|
initialMessage: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
|
|
@@ -6713,7 +6733,8 @@ const spawnBodySchema = __sinclair_typebox.Type.Object({
|
|
|
6713
6733
|
condition: wakeConditionSchema,
|
|
6714
6734
|
debounceMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
6715
6735
|
timeoutMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
6716
|
-
includeResponse: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean())
|
|
6736
|
+
includeResponse: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean()),
|
|
6737
|
+
manifestKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
6717
6738
|
}))
|
|
6718
6739
|
});
|
|
6719
6740
|
const sendBodySchema = __sinclair_typebox.Type.Object({
|
|
@@ -6779,10 +6800,24 @@ const scheduleBodySchema = __sinclair_typebox.Type.Union([__sinclair_typebox.Typ
|
|
|
6779
6800
|
messageType: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6780
6801
|
from: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
6781
6802
|
})]);
|
|
6782
|
-
const
|
|
6803
|
+
const subscriptionLifetimeSchema = __sinclair_typebox.Type.Union([
|
|
6804
|
+
__sinclair_typebox.Type.Object({ kind: __sinclair_typebox.Type.Literal(`until_entity_stopped`) }),
|
|
6805
|
+
__sinclair_typebox.Type.Object({
|
|
6806
|
+
kind: __sinclair_typebox.Type.Literal(`expires_at`),
|
|
6807
|
+
at: __sinclair_typebox.Type.String()
|
|
6808
|
+
}),
|
|
6809
|
+
__sinclair_typebox.Type.Object({ kind: __sinclair_typebox.Type.Literal(`manual`) })
|
|
6810
|
+
]);
|
|
6811
|
+
const eventSourceSubscriptionBodySchema = __sinclair_typebox.Type.Object({
|
|
6812
|
+
sourceKey: __sinclair_typebox.Type.String(),
|
|
6813
|
+
bucketKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6814
|
+
params: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
|
|
6815
|
+
filterKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6816
|
+
lifetime: __sinclair_typebox.Type.Optional(subscriptionLifetimeSchema),
|
|
6817
|
+
reason: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
6818
|
+
});
|
|
6783
6819
|
const entitiesRouter = (0, itty_router.Router)({ base: `/_electric/entities` });
|
|
6784
6820
|
entitiesRouter.get(`/`, listEntities);
|
|
6785
|
-
entitiesRouter.post(`/register`, withSchema(entitiesRegisterBodySchema), registerEntitiesSource);
|
|
6786
6821
|
entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), spawnEntity);
|
|
6787
6822
|
entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
|
|
6788
6823
|
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
|
|
@@ -6793,9 +6828,11 @@ entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity,
|
|
|
6793
6828
|
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
|
|
6794
6829
|
entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
|
|
6795
6830
|
entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
|
|
6796
|
-
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity,
|
|
6831
|
+
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, deleteTag);
|
|
6797
6832
|
entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), upsertSchedule);
|
|
6798
6833
|
entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, deleteSchedule);
|
|
6834
|
+
entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), upsertEventSourceSubscription);
|
|
6835
|
+
entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, deleteEventSourceSubscription);
|
|
6799
6836
|
function entityUrlFromSegments(type, instanceId) {
|
|
6800
6837
|
if (!type || !instanceId) return null;
|
|
6801
6838
|
if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
|
|
@@ -6859,11 +6896,6 @@ async function listEntities({ query }, ctx) {
|
|
|
6859
6896
|
});
|
|
6860
6897
|
return (0, itty_router.json)(entities$1.map((entity) => toPublicEntity(entity)));
|
|
6861
6898
|
}
|
|
6862
|
-
async function registerEntitiesSource(request, ctx) {
|
|
6863
|
-
const parsed = routeBody(request);
|
|
6864
|
-
const result = await ctx.entityManager.registerEntitiesSource(parsed.tags ?? {});
|
|
6865
|
-
return (0, itty_router.json)(result);
|
|
6866
|
-
}
|
|
6867
6899
|
async function upsertSchedule(request, ctx) {
|
|
6868
6900
|
const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
|
|
6869
6901
|
if (principalMutationError) return principalMutationError;
|
|
@@ -6902,8 +6934,49 @@ async function deleteSchedule(request, ctx) {
|
|
|
6902
6934
|
const result = await ctx.entityManager.deleteSchedule(entityUrl, { id: decodeURIComponent(request.params.scheduleId) });
|
|
6903
6935
|
return (0, itty_router.json)(result);
|
|
6904
6936
|
}
|
|
6937
|
+
async function upsertEventSourceSubscription(request, ctx) {
|
|
6938
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `subscribed to event sources`);
|
|
6939
|
+
if (principalMutationError) return principalMutationError;
|
|
6940
|
+
const catalog = ctx.eventSources;
|
|
6941
|
+
if (!catalog) return apiError(404, ErrCodeNotFound, `No event source catalog is configured`);
|
|
6942
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6943
|
+
const parsed = routeBody(request);
|
|
6944
|
+
const source = await catalog.getEventSource(parsed.sourceKey);
|
|
6945
|
+
if (!source) return apiError(404, ErrCodeNotFound, `Event source "${parsed.sourceKey}" not found`);
|
|
6946
|
+
if (parsed.lifetime?.kind === `expires_at`) {
|
|
6947
|
+
const expiresAt = new Date(parsed.lifetime.at);
|
|
6948
|
+
if (Number.isNaN(expiresAt.getTime())) return apiError(400, ErrCodeInvalidRequest, `Invalid expires_at lifetime timestamp`);
|
|
6949
|
+
}
|
|
6950
|
+
let resolved;
|
|
6951
|
+
try {
|
|
6952
|
+
resolved = (0, __electric_ax_agents_runtime.resolveEventSourceSubscription)({
|
|
6953
|
+
contract: source,
|
|
6954
|
+
entityUrl,
|
|
6955
|
+
request: {
|
|
6956
|
+
...parsed,
|
|
6957
|
+
id: decodeURIComponent(request.params.subscriptionId)
|
|
6958
|
+
},
|
|
6959
|
+
createdBy: `tool`
|
|
6960
|
+
});
|
|
6961
|
+
} catch (error) {
|
|
6962
|
+
return apiError(400, ErrCodeInvalidRequest, error instanceof Error ? error.message : String(error));
|
|
6963
|
+
}
|
|
6964
|
+
await ctx.ensureEventSourceWakeSource?.(resolved.subscription.sourceUrl);
|
|
6965
|
+
const result = await ctx.entityManager.upsertEventSourceSubscription(entityUrl, {
|
|
6966
|
+
subscription: resolved.subscription,
|
|
6967
|
+
manifest: (0, __electric_ax_agents_runtime.buildEventSourceManifestEntry)(resolved)
|
|
6968
|
+
});
|
|
6969
|
+
return (0, itty_router.json)(result);
|
|
6970
|
+
}
|
|
6971
|
+
async function deleteEventSourceSubscription(request, ctx) {
|
|
6972
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `unsubscribed from event sources`);
|
|
6973
|
+
if (principalMutationError) return principalMutationError;
|
|
6974
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6975
|
+
const result = await ctx.entityManager.deleteEventSourceSubscription(entityUrl, { id: decodeURIComponent(request.params.subscriptionId) });
|
|
6976
|
+
return (0, itty_router.json)(result);
|
|
6977
|
+
}
|
|
6905
6978
|
async function setTag(request, ctx) {
|
|
6906
|
-
const principalMutationError = rejectPrincipalEntityMutation(request, `
|
|
6979
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `tag updated`);
|
|
6907
6980
|
if (principalMutationError) return principalMutationError;
|
|
6908
6981
|
const parsed = routeBody(request);
|
|
6909
6982
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
@@ -6911,12 +6984,12 @@ async function setTag(request, ctx) {
|
|
|
6911
6984
|
const updated = await ctx.entityManager.setTag(entityUrl, decodeURIComponent(request.params.tagKey), { value: parsed.value }, token);
|
|
6912
6985
|
return (0, itty_router.json)(toPublicEntity(updated));
|
|
6913
6986
|
}
|
|
6914
|
-
async function
|
|
6915
|
-
const principalMutationError = rejectPrincipalEntityMutation(request, `
|
|
6987
|
+
async function deleteTag(request, ctx) {
|
|
6988
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `tag deleted`);
|
|
6916
6989
|
if (principalMutationError) return principalMutationError;
|
|
6917
6990
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6918
6991
|
const token = writeTokenFromRequest(request);
|
|
6919
|
-
const updated = await ctx.entityManager.
|
|
6992
|
+
const updated = await ctx.entityManager.deleteTag(entityUrl, decodeURIComponent(request.params.tagKey), token);
|
|
6920
6993
|
return (0, itty_router.json)(toPublicEntity(updated));
|
|
6921
6994
|
}
|
|
6922
6995
|
async function forkEntity(request, ctx) {
|
|
@@ -7048,17 +7121,13 @@ const registerEntityTypeBodySchema = __sinclair_typebox.Type.Object({
|
|
|
7048
7121
|
creation_schema: __sinclair_typebox.Type.Optional(jsonObjectSchema),
|
|
7049
7122
|
inbox_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
7050
7123
|
state_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
7051
|
-
input_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
7052
|
-
output_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
7053
7124
|
serve_endpoint: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7054
7125
|
default_dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema)
|
|
7055
|
-
});
|
|
7126
|
+
}, { additionalProperties: false });
|
|
7056
7127
|
const amendEntityTypeSchemasBodySchema = __sinclair_typebox.Type.Object({
|
|
7057
|
-
input_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
7058
|
-
output_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
7059
7128
|
inbox_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
7060
7129
|
state_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema)
|
|
7061
|
-
});
|
|
7130
|
+
}, { additionalProperties: false });
|
|
7062
7131
|
const entityTypesRouter = (0, itty_router.Router)({ base: `/_electric/entity-types` });
|
|
7063
7132
|
entityTypesRouter.get(`/`, listEntityTypes);
|
|
7064
7133
|
entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), registerEntityType);
|
|
@@ -7098,8 +7167,8 @@ async function getEntityType(request, ctx) {
|
|
|
7098
7167
|
async function amendSchemas(request, ctx) {
|
|
7099
7168
|
const parsed = routeBody(request);
|
|
7100
7169
|
const updated = await ctx.entityManager.amendSchemas(request.params.name, {
|
|
7101
|
-
inbox_schemas: parsed.inbox_schemas
|
|
7102
|
-
state_schemas: parsed.state_schemas
|
|
7170
|
+
inbox_schemas: parsed.inbox_schemas,
|
|
7171
|
+
state_schemas: parsed.state_schemas
|
|
7103
7172
|
});
|
|
7104
7173
|
return (0, itty_router.json)(toPublicEntityType(updated));
|
|
7105
7174
|
}
|
|
@@ -7109,13 +7178,12 @@ async function deleteEntityType(request, ctx) {
|
|
|
7109
7178
|
}
|
|
7110
7179
|
function normalizeEntityTypeRequest(parsed) {
|
|
7111
7180
|
const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
|
|
7112
|
-
const compatibilityFields = parsed;
|
|
7113
7181
|
return {
|
|
7114
7182
|
name: parsed.name ?? ``,
|
|
7115
7183
|
description: parsed.description ?? ``,
|
|
7116
7184
|
creation_schema: parsed.creation_schema,
|
|
7117
|
-
inbox_schemas: parsed.inbox_schemas
|
|
7118
|
-
state_schemas: parsed.state_schemas
|
|
7185
|
+
inbox_schemas: parsed.inbox_schemas,
|
|
7186
|
+
state_schemas: parsed.state_schemas,
|
|
7119
7187
|
serve_endpoint: serveEndpoint,
|
|
7120
7188
|
default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
|
|
7121
7189
|
type: `webhook`,
|
|
@@ -7126,8 +7194,6 @@ function normalizeEntityTypeRequest(parsed) {
|
|
|
7126
7194
|
function toPublicEntityType(entityType) {
|
|
7127
7195
|
return {
|
|
7128
7196
|
...entityType,
|
|
7129
|
-
input_schemas: entityType.inbox_schemas,
|
|
7130
|
-
output_schemas: entityType.state_schemas,
|
|
7131
7197
|
revision: entityType.revision
|
|
7132
7198
|
};
|
|
7133
7199
|
}
|
|
@@ -7211,13 +7277,35 @@ function errorMapper(err, req) {
|
|
|
7211
7277
|
function rejectIfShuttingDown(req, ctx) {
|
|
7212
7278
|
if (!ctx.isShuttingDown()) return void 0;
|
|
7213
7279
|
const path$2 = new URL(req.url).pathname;
|
|
7214
|
-
if (!path$2.startsWith(`/_electric/
|
|
7280
|
+
if (!path$2.startsWith(`/_electric/subscription-webhooks/`)) return void 0;
|
|
7215
7281
|
return apiError(503, `SERVER_STOPPING`, `Server is shutting down`);
|
|
7216
7282
|
}
|
|
7217
7283
|
function getRequestSpan(req) {
|
|
7218
7284
|
return carrier(req)[SPAN_KEY];
|
|
7219
7285
|
}
|
|
7220
7286
|
|
|
7287
|
+
//#endregion
|
|
7288
|
+
//#region src/routing/observations-router.ts
|
|
7289
|
+
const stringRecordSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.String());
|
|
7290
|
+
const ensureEntitiesMembershipStreamBodySchema = __sinclair_typebox.Type.Object({ tags: __sinclair_typebox.Type.Optional(stringRecordSchema) });
|
|
7291
|
+
const ensureCronStreamBodySchema = __sinclair_typebox.Type.Object({
|
|
7292
|
+
expression: __sinclair_typebox.Type.String(),
|
|
7293
|
+
timezone: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
7294
|
+
});
|
|
7295
|
+
const observationsRouter = (0, itty_router.Router)({ base: `/_electric/observations` });
|
|
7296
|
+
observationsRouter.post(`/entities/ensure-stream`, withSchema(ensureEntitiesMembershipStreamBodySchema), ensureEntitiesMembershipStream);
|
|
7297
|
+
observationsRouter.post(`/cron/ensure-stream`, withSchema(ensureCronStreamBodySchema), ensureCronStream);
|
|
7298
|
+
async function ensureEntitiesMembershipStream(request, ctx) {
|
|
7299
|
+
const parsed = routeBody(request);
|
|
7300
|
+
const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {});
|
|
7301
|
+
return (0, itty_router.json)(result);
|
|
7302
|
+
}
|
|
7303
|
+
async function ensureCronStream(request, ctx) {
|
|
7304
|
+
const parsed = routeBody(request);
|
|
7305
|
+
const streamPath = await ctx.entityManager.getOrCreateCronStream(parsed.expression, parsed.timezone);
|
|
7306
|
+
return (0, itty_router.json)({ streamUrl: streamPath });
|
|
7307
|
+
}
|
|
7308
|
+
|
|
7221
7309
|
//#endregion
|
|
7222
7310
|
//#region src/routing/tenant-stream-paths.ts
|
|
7223
7311
|
function withLeadingSlash(path$2) {
|
|
@@ -7556,7 +7644,7 @@ async function notificationFromClaim(ctx, input) {
|
|
|
7556
7644
|
wakeId: input.claim.wake_id,
|
|
7557
7645
|
streamPath: primaryStream,
|
|
7558
7646
|
streams: streams$1,
|
|
7559
|
-
callback: (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/
|
|
7647
|
+
callback: (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/wake-callbacks/${encodeURIComponent(input.claim.wake_id)}`),
|
|
7560
7648
|
claimToken: input.claim.token,
|
|
7561
7649
|
triggerEvent: `message_received`,
|
|
7562
7650
|
entity: {
|
|
@@ -7591,7 +7679,7 @@ const wakeRegistrationBodySchema = __sinclair_typebox.Type.Object({
|
|
|
7591
7679
|
includeResponse: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean()),
|
|
7592
7680
|
manifestKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
7593
7681
|
});
|
|
7594
|
-
const
|
|
7682
|
+
const subscriptionWebhookBodySchema = __sinclair_typebox.Type.Object({
|
|
7595
7683
|
subscription_id: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7596
7684
|
wake_id: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7597
7685
|
generation: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
@@ -7605,7 +7693,7 @@ const webhookForwardBodySchema = __sinclair_typebox.Type.Object({
|
|
|
7605
7693
|
consumer_id: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7606
7694
|
callback: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
7607
7695
|
}, { additionalProperties: true });
|
|
7608
|
-
const
|
|
7696
|
+
const wakeCallbackBodySchema = __sinclair_typebox.Type.Object({
|
|
7609
7697
|
epoch: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
7610
7698
|
generation: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
7611
7699
|
wakeId: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
@@ -7616,14 +7704,15 @@ const callbackForwardBodySchema = __sinclair_typebox.Type.Object({
|
|
|
7616
7704
|
const DS_SUBSCRIPTION_CALLBACK_PREFIX = `ds-subscription:`;
|
|
7617
7705
|
const internalRouter = (0, itty_router.Router)({ base: `/_electric` });
|
|
7618
7706
|
internalRouter.get(`/health`, () => (0, itty_router.json)({ status: `ok` }));
|
|
7707
|
+
internalRouter.get(`/event-sources`, listEventSources);
|
|
7619
7708
|
internalRouter.post(`/wake`, withSchema(wakeRegistrationBodySchema), registerWake);
|
|
7620
|
-
internalRouter.post(`/
|
|
7621
|
-
internalRouter.post(`/
|
|
7709
|
+
internalRouter.post(`/subscription-webhooks/:subscriptionId`, subscriptionWebhook);
|
|
7710
|
+
internalRouter.post(`/wake-callbacks/:consumerId`, wakeCallback);
|
|
7622
7711
|
internalRouter.all(`/runners`, runnersRouter.fetch);
|
|
7623
7712
|
internalRouter.all(`/runners/*`, runnersRouter.fetch);
|
|
7624
7713
|
internalRouter.all(`/entities/*`, entitiesRouter.fetch);
|
|
7625
7714
|
internalRouter.all(`/entity-types/*`, entityTypesRouter.fetch);
|
|
7626
|
-
internalRouter.all(`/
|
|
7715
|
+
internalRouter.all(`/observations/*`, observationsRouter.fetch);
|
|
7627
7716
|
internalRouter.get(`/electric/*`, electricProxyRouter.fetch);
|
|
7628
7717
|
internalRouter.all(`*`, () => (0, itty_router.status)(404));
|
|
7629
7718
|
function routeParam(request, name) {
|
|
@@ -7656,13 +7745,30 @@ function resolveWebhookSigner(ctx) {
|
|
|
7656
7745
|
return ctx.webhookSigner ?? getDefaultWebhookSigner();
|
|
7657
7746
|
}
|
|
7658
7747
|
function durableStreamsWebhookJwksUrl(ctx) {
|
|
7659
|
-
if (!ctx.durableStreamsRouting) return (
|
|
7748
|
+
if (!ctx.durableStreamsRouting) return appendPathToBackendUrl(ctx.durableStreamsUrl, `/__ds/jwks.json`);
|
|
7660
7749
|
return resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl).controlUrl({
|
|
7661
7750
|
durableStreamsUrl: ctx.durableStreamsUrl,
|
|
7662
7751
|
serviceId: ctx.service,
|
|
7663
7752
|
requestUrl: (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/__ds/jwks.json`)
|
|
7664
7753
|
}).toString();
|
|
7665
7754
|
}
|
|
7755
|
+
function appendPathToBackendUrl(baseUrl, path$2) {
|
|
7756
|
+
const base = new URL(baseUrl);
|
|
7757
|
+
const pathUrl = new URL(path$2, `http://electric-agents.local`);
|
|
7758
|
+
const basePath = base.pathname === `/` ? `` : base.pathname.replace(/\/+$/, ``);
|
|
7759
|
+
const suffix = pathUrl.pathname.startsWith(`/`) ? pathUrl.pathname : `/${pathUrl.pathname}`;
|
|
7760
|
+
const target = new URL(base);
|
|
7761
|
+
target.pathname = `${basePath}${suffix}`;
|
|
7762
|
+
target.search = ``;
|
|
7763
|
+
target.hash = pathUrl.hash;
|
|
7764
|
+
base.searchParams.forEach((value, key) => {
|
|
7765
|
+
target.searchParams.append(key, value);
|
|
7766
|
+
});
|
|
7767
|
+
pathUrl.searchParams.forEach((value, key) => {
|
|
7768
|
+
target.searchParams.append(key, value);
|
|
7769
|
+
});
|
|
7770
|
+
return target.toString();
|
|
7771
|
+
}
|
|
7666
7772
|
function durableStreamsJwksFetchClient(ctx) {
|
|
7667
7773
|
return async (input, init) => {
|
|
7668
7774
|
const headers = new Headers(init?.headers);
|
|
@@ -7719,10 +7825,17 @@ async function registerWake(request, ctx) {
|
|
|
7719
7825
|
await ctx.entityManager.registerWake(opts);
|
|
7720
7826
|
return (0, itty_router.status)(204);
|
|
7721
7827
|
}
|
|
7722
|
-
async function
|
|
7828
|
+
async function listEventSources(_request, ctx) {
|
|
7829
|
+
const eventSources = ctx.eventSources ? await ctx.eventSources.listEventSources() : [];
|
|
7830
|
+
return (0, itty_router.json)({ eventSources: eventSources.filter(isAgentVisibleEventSource) });
|
|
7831
|
+
}
|
|
7832
|
+
function isAgentVisibleEventSource(source) {
|
|
7833
|
+
return source.agentVisible === true && source.status === `active`;
|
|
7834
|
+
}
|
|
7835
|
+
async function subscriptionWebhook(request, ctx) {
|
|
7723
7836
|
const subscriptionId = routeParam(request, `subscriptionId`);
|
|
7724
7837
|
const rootSpan = getRequestSpan(request);
|
|
7725
|
-
rootSpan?.updateName(`webhook
|
|
7838
|
+
rootSpan?.updateName(`subscription-webhook`);
|
|
7726
7839
|
rootSpan?.setAttribute(`electric_agents.webhook.subscription_id`, subscriptionId);
|
|
7727
7840
|
const body = await readRequestBody(request);
|
|
7728
7841
|
const signatureError = await verifyDurableStreamsWebhook(request, ctx, body);
|
|
@@ -7736,7 +7849,7 @@ async function webhookForward(request, ctx) {
|
|
|
7736
7849
|
}
|
|
7737
7850
|
});
|
|
7738
7851
|
if (!targetWebhookUrl) return apiError(404, ErrCodeSubscriptionNotFound, `Unknown webhook subscription`);
|
|
7739
|
-
const parsedBodyResult = validateOptionalJsonBody(
|
|
7852
|
+
const parsedBodyResult = validateOptionalJsonBody(subscriptionWebhookBodySchema, body, request.headers.get(`content-type`));
|
|
7740
7853
|
if (!parsedBodyResult.ok) return parsedBodyResult.response;
|
|
7741
7854
|
let forwardBody = body;
|
|
7742
7855
|
let runningEntityUrl = null;
|
|
@@ -7782,7 +7895,7 @@ async function webhookForward(request, ctx) {
|
|
|
7782
7895
|
span.end();
|
|
7783
7896
|
}
|
|
7784
7897
|
}).catch((err) => {
|
|
7785
|
-
serverLog.warn(`[webhook
|
|
7898
|
+
serverLog.warn(`[subscription-webhook] consumerCallbacks upsert failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
7786
7899
|
}) : void 0;
|
|
7787
7900
|
const [entity, enriched] = await Promise.all([entityPromise, enrichPromise]);
|
|
7788
7901
|
if (entity?.status === `stopped` || entity?.status === `paused`) {
|
|
@@ -7803,7 +7916,7 @@ async function webhookForward(request, ctx) {
|
|
|
7803
7916
|
runningEntityUrl = entity.url;
|
|
7804
7917
|
}
|
|
7805
7918
|
if (consumerId && callbackUrl) {
|
|
7806
|
-
const callback = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/
|
|
7919
|
+
const callback = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/wake-callbacks/${encodeURIComponent(consumerId)}`);
|
|
7807
7920
|
enriched.callback = callback;
|
|
7808
7921
|
if (newWebhook) {
|
|
7809
7922
|
enriched.consumerId = newWebhook.wakeId;
|
|
@@ -7840,21 +7953,21 @@ async function webhookForward(request, ctx) {
|
|
|
7840
7953
|
});
|
|
7841
7954
|
} catch (err) {
|
|
7842
7955
|
if (runningEntityUrl) await ctx.entityManager.registry.updateStatus(runningEntityUrl, `idle`);
|
|
7843
|
-
return apiError(502, `
|
|
7956
|
+
return apiError(502, `SUBSCRIPTION_WEBHOOK_FAILED`, err instanceof Error ? err.message : String(err));
|
|
7844
7957
|
}
|
|
7845
7958
|
const responseBytes = upstream.body ? new Uint8Array(await upstream.arrayBuffer()) : new Uint8Array();
|
|
7846
7959
|
return responseFromUpstream(upstream, responseBytes);
|
|
7847
7960
|
}
|
|
7848
|
-
async function
|
|
7961
|
+
async function wakeCallback(request, ctx) {
|
|
7849
7962
|
const consumerId = routeParam(request, `consumerId`);
|
|
7850
7963
|
const rows = await ctx.pgDb.select().from(consumerCallbacks).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(consumerCallbacks.tenantId, ctx.service), (0, drizzle_orm.eq)(consumerCallbacks.consumerId, consumerId))).limit(1);
|
|
7851
7964
|
const target = rows[0] ? {
|
|
7852
7965
|
callbackUrl: rows[0].callbackUrl,
|
|
7853
7966
|
primaryStream: rows[0].primaryStream
|
|
7854
7967
|
} : void 0;
|
|
7855
|
-
if (!target) return apiError(404,
|
|
7968
|
+
if (!target) return apiError(404, ErrCodeWakeCallbackNotFound, `Unknown wake-callback consumer`);
|
|
7856
7969
|
const body = await readRequestBody(request);
|
|
7857
|
-
const parsedBodyResult = validateOptionalJsonBody(
|
|
7970
|
+
const parsedBodyResult = validateOptionalJsonBody(wakeCallbackBodySchema, body, request.headers.get(`content-type`));
|
|
7858
7971
|
if (!parsedBodyResult.ok) return parsedBodyResult.response;
|
|
7859
7972
|
const requestBody = parsedBodyResult.value;
|
|
7860
7973
|
const isClaimRequest = requestBody?.wakeId !== void 0 || requestBody?.wake_id !== void 0;
|
|
@@ -7872,14 +7985,14 @@ async function callbackForward(request, ctx) {
|
|
|
7872
7985
|
}
|
|
7873
7986
|
return (0, itty_router.json)(responseBody);
|
|
7874
7987
|
}
|
|
7875
|
-
const upstreamBody =
|
|
7988
|
+
const upstreamBody = encodeWakeCallbackBody(ctx.service, consumerId, requestBody, resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl));
|
|
7876
7989
|
let upstream;
|
|
7877
7990
|
try {
|
|
7878
7991
|
const subscriptionId = durableStreamsSubscriptionCallback(target.callbackUrl);
|
|
7879
7992
|
if (subscriptionId) {
|
|
7880
7993
|
const token = claimTokenFromRequest(request);
|
|
7881
7994
|
if (!token) return apiError(401, `UNAUTHORIZED`, `Missing claim token`);
|
|
7882
|
-
const upstreamPayload =
|
|
7995
|
+
const upstreamPayload = encodeWakeCallbackPayload(consumerId, requestBody, (stream) => stream.replace(/^\/+/, ``));
|
|
7883
7996
|
const result = await ctx.streamClient.ackSubscription(subscriptionId, token, upstreamPayload);
|
|
7884
7997
|
upstream = (0, itty_router.json)(result);
|
|
7885
7998
|
} else upstream = await fetch(target.callbackUrl, {
|
|
@@ -7888,7 +8001,7 @@ async function callbackForward(request, ctx) {
|
|
|
7888
8001
|
body: bodyFromBytes(upstreamBody)
|
|
7889
8002
|
});
|
|
7890
8003
|
} catch (err) {
|
|
7891
|
-
return apiError(502, `
|
|
8004
|
+
return apiError(502, `WAKE_CALLBACK_FAILED`, err instanceof Error ? err.message : String(err));
|
|
7892
8005
|
}
|
|
7893
8006
|
let responseBytes = upstream.body ? new Uint8Array(await upstream.arrayBuffer()) : new Uint8Array();
|
|
7894
8007
|
if (isClaimRequest && upstream.ok && target.primaryStream) {
|
|
@@ -7908,7 +8021,7 @@ async function callbackForward(request, ctx) {
|
|
|
7908
8021
|
epoch
|
|
7909
8022
|
});
|
|
7910
8023
|
if (upstream.ok && isDoneRequest && target.primaryStream) {
|
|
7911
|
-
serverLog.info(`[callback
|
|
8024
|
+
serverLog.info(`[wake-callback] done received for stream=${target.primaryStream} consumer=${consumerId}`);
|
|
7912
8025
|
const stillOwnsClaim = ctx.runtime.claimWriteTokens.owns(ctx.service, target.primaryStream, consumerId);
|
|
7913
8026
|
const entity = await ctx.entityManager.registry.getEntityByStream(target.primaryStream);
|
|
7914
8027
|
let entityCleared = false;
|
|
@@ -7930,13 +8043,13 @@ async function callbackForward(request, ctx) {
|
|
|
7930
8043
|
if (entity && (entityCleared || stillOwnsClaim)) {
|
|
7931
8044
|
await ctx.entityManager.registry.updateStatus(entity.url, entity.status === `stopping` ? `stopped` : `idle`);
|
|
7932
8045
|
await ctx.entityBridgeManager.onEntityChanged(entity.url);
|
|
7933
|
-
serverLog.info(`[callback
|
|
7934
|
-
} else if (!entity) serverLog.warn(`[callback
|
|
8046
|
+
serverLog.info(`[wake-callback] status updated after done for ${entity.url}`);
|
|
8047
|
+
} else if (!entity) serverLog.warn(`[wake-callback] done received but no entity found for stream=${target.primaryStream}`);
|
|
7935
8048
|
if (stillOwnsClaim) ctx.runtime.claimWriteTokens.clearStream(ctx.service, target.primaryStream);
|
|
7936
|
-
else if (entity) serverLog.info(`[callback
|
|
7937
|
-
} else if (requestBody?.done === true) serverLog.warn(`[callback
|
|
8049
|
+
else if (entity) serverLog.info(`[wake-callback] done arrived after in-memory token evicted (stream=${target.primaryStream} consumer=${consumerId})`);
|
|
8050
|
+
} else if (requestBody?.done === true) serverLog.warn(`[wake-callback] done received but skipped: upstream.ok=${upstream.ok} primaryStream=${target.primaryStream ?? `null`} consumer=${consumerId}`);
|
|
7938
8051
|
} catch (err) {
|
|
7939
|
-
serverLog.error(`[callback
|
|
8052
|
+
serverLog.error(`[wake-callback] error processing done for consumer=${consumerId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7940
8053
|
}
|
|
7941
8054
|
return responseFromUpstream(upstream, responseBytes);
|
|
7942
8055
|
}
|
|
@@ -7945,11 +8058,11 @@ async function mintClaimWriteToken(ctx, streamPath, consumerId) {
|
|
|
7945
8058
|
if (!entity) return void 0;
|
|
7946
8059
|
return ctx.runtime.claimWriteTokens.mint(ctx.service, streamPath, consumerId);
|
|
7947
8060
|
}
|
|
7948
|
-
function
|
|
7949
|
-
const payload =
|
|
8061
|
+
function encodeWakeCallbackBody(service, consumerId, body, routingAdapter) {
|
|
8062
|
+
const payload = encodeWakeCallbackPayload(consumerId, body, (stream) => routingAdapter.toBackendStreamPath(service, stream));
|
|
7950
8063
|
return new TextEncoder().encode(JSON.stringify(payload));
|
|
7951
8064
|
}
|
|
7952
|
-
function
|
|
8065
|
+
function encodeWakeCallbackPayload(consumerId, body, mapStream) {
|
|
7953
8066
|
if (!body) return {};
|
|
7954
8067
|
const generation = body.generation ?? body.epoch;
|
|
7955
8068
|
const wakeId = body.wake_id ?? body.wakeId ?? consumerId;
|