@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.js
CHANGED
|
@@ -8,7 +8,7 @@ import postgres from "postgres";
|
|
|
8
8
|
import { and, desc, eq, lt, ne, sql } from "drizzle-orm";
|
|
9
9
|
import { bigint, bigserial, boolean, check, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique } from "drizzle-orm/pg-core";
|
|
10
10
|
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomUUID, sign } from "node:crypto";
|
|
11
|
-
import { appendPathToUrl, assertTags, buildTagsIndex, entityStateSchema, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, sourceRefForTags, verifyWebhookSignature } from "@electric-ax/agents-runtime";
|
|
11
|
+
import { appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, getWebhookStreamPath, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForTags, verifyWebhookSignature } from "@electric-ax/agents-runtime";
|
|
12
12
|
import { DurableStream, DurableStreamError, FetchError, IdempotentProducer } from "@durable-streams/client";
|
|
13
13
|
import { ShapeStream, isChangeMessage, isControlMessage } from "@electric-sql/client";
|
|
14
14
|
import pino from "pino";
|
|
@@ -422,7 +422,7 @@ const ErrCodeForkInProgress = `FORK_IN_PROGRESS`;
|
|
|
422
422
|
const ErrCodeForkWaitTimeout = `FORK_WAIT_TIMEOUT`;
|
|
423
423
|
const ErrCodeEntityPersistFailed = `ENTITY_PERSIST_FAILED`;
|
|
424
424
|
const ErrCodeSubscriptionNotFound = `SUBSCRIPTION_NOT_FOUND`;
|
|
425
|
-
const
|
|
425
|
+
const ErrCodeWakeCallbackNotFound = `WAKE_CALLBACK_NOT_FOUND`;
|
|
426
426
|
|
|
427
427
|
//#endregion
|
|
428
428
|
//#region src/tenant.ts
|
|
@@ -2503,7 +2503,7 @@ async function linkStreamToTargetSubscription(ctx, target, entity, subscriptionI
|
|
|
2503
2503
|
}
|
|
2504
2504
|
const webhookUrl = rewriteLoopbackWebhookUrl(target.url);
|
|
2505
2505
|
if (!webhookUrl) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Webhook dispatch target must include a valid URL`, 400);
|
|
2506
|
-
const forwardUrl = appendPathToUrl(ctx.publicUrl, `/_electric/
|
|
2506
|
+
const forwardUrl = appendPathToUrl(ctx.publicUrl, `/_electric/subscription-webhooks/${encodeURIComponent(subscriptionId)}`);
|
|
2507
2507
|
await ensureSubscriptionIncludesStream(ctx, subscriptionId, streamPath, {
|
|
2508
2508
|
type: `webhook`,
|
|
2509
2509
|
streams: [streamPath],
|
|
@@ -2624,6 +2624,10 @@ function extractManifestSourceUrl(manifest) {
|
|
|
2624
2624
|
}
|
|
2625
2625
|
if (manifest.sourceType === `entities`) return typeof manifest.sourceRef === `string` ? `/_entities/${manifest.sourceRef}` : void 0;
|
|
2626
2626
|
if (manifest.sourceType === `db`) return typeof manifest.sourceRef === `string` ? getSharedStateStreamPath(manifest.sourceRef) : void 0;
|
|
2627
|
+
if (manifest.sourceType === `webhook`) {
|
|
2628
|
+
if (typeof config?.streamUrl === `string`) return config.streamUrl;
|
|
2629
|
+
if (typeof config?.endpointKey === `string`) return getWebhookStreamPath(config.endpointKey, typeof config.bucket === `string` ? config.bucket : void 0);
|
|
2630
|
+
}
|
|
2627
2631
|
return void 0;
|
|
2628
2632
|
}
|
|
2629
2633
|
if (manifest.kind === `shared-state`) return typeof manifest.id === `string` ? getSharedStateStreamPath(manifest.id) : void 0;
|
|
@@ -2910,7 +2914,8 @@ var EntityManager = class {
|
|
|
2910
2914
|
debounceMs: req.wake.debounceMs,
|
|
2911
2915
|
timeoutMs: req.wake.timeoutMs,
|
|
2912
2916
|
oneShot: false,
|
|
2913
|
-
includeResponse: req.wake.includeResponse
|
|
2917
|
+
includeResponse: req.wake.includeResponse,
|
|
2918
|
+
manifestKey: req.wake.manifestKey
|
|
2914
2919
|
});
|
|
2915
2920
|
const contentType = `application/json`;
|
|
2916
2921
|
const createdEvent = entityStateSchema.entityCreated.insert({
|
|
@@ -3583,7 +3588,7 @@ var EntityManager = class {
|
|
|
3583
3588
|
};
|
|
3584
3589
|
}
|
|
3585
3590
|
/**
|
|
3586
|
-
* Deliver a message to an entity's main stream, with optional
|
|
3591
|
+
* Deliver a message to an entity's main stream, with optional inbox schema
|
|
3587
3592
|
* validation.
|
|
3588
3593
|
*/
|
|
3589
3594
|
async send(entityUrl, req, opts) {
|
|
@@ -3671,7 +3676,7 @@ var EntityManager = class {
|
|
|
3671
3676
|
if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
3672
3677
|
return updated;
|
|
3673
3678
|
}
|
|
3674
|
-
async
|
|
3679
|
+
async deleteTag(entityUrl, key, token) {
|
|
3675
3680
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3676
3681
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3677
3682
|
if (!this.isValidWriteToken(entity, token)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Invalid write token`, 401);
|
|
@@ -3682,7 +3687,7 @@ var EntityManager = class {
|
|
|
3682
3687
|
if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
3683
3688
|
return updated;
|
|
3684
3689
|
}
|
|
3685
|
-
async
|
|
3690
|
+
async ensureEntitiesMembershipStream(tags) {
|
|
3686
3691
|
if (!this.entityBridgeManager) throw new Error(`Entity bridge manager not configured`);
|
|
3687
3692
|
return this.entityBridgeManager.register(this.validateTags(tags));
|
|
3688
3693
|
}
|
|
@@ -3804,6 +3809,35 @@ var EntityManager = class {
|
|
|
3804
3809
|
await this.writeManifestEntry(entityUrl, manifestKey, `delete`, void 0, { txid });
|
|
3805
3810
|
return { txid };
|
|
3806
3811
|
}
|
|
3812
|
+
async upsertEventSourceSubscription(entityUrl, req) {
|
|
3813
|
+
const manifestKey = req.subscription.manifestKey;
|
|
3814
|
+
const txid = randomUUID();
|
|
3815
|
+
await this.writeManifestEntry(entityUrl, manifestKey, `upsert`, req.manifest, { txid });
|
|
3816
|
+
await this.wakeRegistry.unregisterByManifestKey(entityUrl, manifestKey, this.tenantId);
|
|
3817
|
+
await this.wakeRegistry.register({
|
|
3818
|
+
tenantId: this.tenantId,
|
|
3819
|
+
subscriberUrl: entityUrl,
|
|
3820
|
+
sourceUrl: req.subscription.sourceUrl,
|
|
3821
|
+
condition: {
|
|
3822
|
+
on: `change`,
|
|
3823
|
+
collections: [`webhook_event`],
|
|
3824
|
+
ops: [`insert`]
|
|
3825
|
+
},
|
|
3826
|
+
oneShot: false,
|
|
3827
|
+
manifestKey
|
|
3828
|
+
});
|
|
3829
|
+
return {
|
|
3830
|
+
txid,
|
|
3831
|
+
subscription: req.subscription
|
|
3832
|
+
};
|
|
3833
|
+
}
|
|
3834
|
+
async deleteEventSourceSubscription(entityUrl, req) {
|
|
3835
|
+
const manifestKey = eventSourceSubscriptionManifestKey(req.id);
|
|
3836
|
+
const txid = randomUUID();
|
|
3837
|
+
await this.writeManifestEntry(entityUrl, manifestKey, `delete`, void 0, { txid });
|
|
3838
|
+
await this.wakeRegistry.unregisterByManifestKey(entityUrl, manifestKey, this.tenantId);
|
|
3839
|
+
return { txid };
|
|
3840
|
+
}
|
|
3807
3841
|
/**
|
|
3808
3842
|
* Register a wake subscription from a subscriber to a source entity.
|
|
3809
3843
|
*/
|
|
@@ -4082,7 +4116,7 @@ var EntityManager = class {
|
|
|
4082
4116
|
return null;
|
|
4083
4117
|
}
|
|
4084
4118
|
/**
|
|
4085
|
-
* Add new
|
|
4119
|
+
* Add new inbox/state schema keys to an entity type directly in Postgres.
|
|
4086
4120
|
*/
|
|
4087
4121
|
async amendSchemas(typeName, schemas) {
|
|
4088
4122
|
if (typeName === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be amended`, 400);
|
|
@@ -4122,7 +4156,7 @@ var EntityManager = class {
|
|
|
4122
4156
|
}
|
|
4123
4157
|
/**
|
|
4124
4158
|
* Enrich webhook payload with entity context.
|
|
4125
|
-
* Called by ElectricAgentsServer during webhook
|
|
4159
|
+
* Called by ElectricAgentsServer during subscription webhook dispatch to inject entity context.
|
|
4126
4160
|
*/
|
|
4127
4161
|
async enrichPayload(payload, consumer) {
|
|
4128
4162
|
const entity = await this.registry.getEntityByStream(consumer.primary_stream);
|
|
@@ -5381,7 +5415,7 @@ var WakeRegistry = class {
|
|
|
5381
5415
|
try {
|
|
5382
5416
|
for (const message of messages) {
|
|
5383
5417
|
await this.applyShapeMessage(message);
|
|
5384
|
-
if (!settled &&
|
|
5418
|
+
if (!settled && isControlMessage(message) && message.headers.control === `up-to-date`) {
|
|
5385
5419
|
settled = true;
|
|
5386
5420
|
resolve$1();
|
|
5387
5421
|
}
|
|
@@ -6168,7 +6202,7 @@ function validateParsedBody(schema, parsed) {
|
|
|
6168
6202
|
//#region src/routing/durable-streams-routing-adapter.ts
|
|
6169
6203
|
function appendSearch(target, source) {
|
|
6170
6204
|
source.searchParams.forEach((value, key) => {
|
|
6171
|
-
|
|
6205
|
+
target.searchParams.append(key, value);
|
|
6172
6206
|
});
|
|
6173
6207
|
return target;
|
|
6174
6208
|
}
|
|
@@ -6488,7 +6522,7 @@ async function rewriteSubscriptionRequestBody(request, ctx, subscriptionId, rout
|
|
|
6488
6522
|
let targetWebhookUrl = null;
|
|
6489
6523
|
if (payload.webhook?.url !== void 0) {
|
|
6490
6524
|
targetWebhookUrl = rewriteLoopbackWebhookUrl(payload.webhook.url) ?? null;
|
|
6491
|
-
payload.webhook.url = appendPathToUrl(ctx.publicUrl, `/_electric/
|
|
6525
|
+
payload.webhook.url = appendPathToUrl(ctx.publicUrl, `/_electric/subscription-webhooks/${encodeURIComponent(subscriptionId)}`);
|
|
6492
6526
|
}
|
|
6493
6527
|
rewriteSubscriptionBodyForBackend(payload, ctx.service, routingAdapter);
|
|
6494
6528
|
return {
|
|
@@ -6611,20 +6645,6 @@ async function proxyPassThrough(request, ctx) {
|
|
|
6611
6645
|
}
|
|
6612
6646
|
}
|
|
6613
6647
|
|
|
6614
|
-
//#endregion
|
|
6615
|
-
//#region src/routing/cron-router.ts
|
|
6616
|
-
const cronRegisterBodySchema = Type.Object({
|
|
6617
|
-
expression: Type.String(),
|
|
6618
|
-
timezone: Type.Optional(Type.String())
|
|
6619
|
-
});
|
|
6620
|
-
const cronRouter = Router({ base: `/_electric/cron` });
|
|
6621
|
-
cronRouter.post(`/register`, withSchema(cronRegisterBodySchema), registerCron);
|
|
6622
|
-
async function registerCron(request, ctx) {
|
|
6623
|
-
const parsed = routeBody(request);
|
|
6624
|
-
const streamPath = await ctx.entityManager.getOrCreateCronStream(parsed.expression, parsed.timezone);
|
|
6625
|
-
return json({ streamUrl: streamPath });
|
|
6626
|
-
}
|
|
6627
|
-
|
|
6628
6648
|
//#endregion
|
|
6629
6649
|
//#region src/routing/electric-proxy-router.ts
|
|
6630
6650
|
const electricProxyRouter = Router({ base: `/_electric/electric` });
|
|
@@ -6658,7 +6678,7 @@ async function proxyElectric(request, ctx) {
|
|
|
6658
6678
|
|
|
6659
6679
|
//#endregion
|
|
6660
6680
|
//#region src/routing/entities-router.ts
|
|
6661
|
-
const stringRecordSchema = Type.Record(Type.String(), Type.String());
|
|
6681
|
+
const stringRecordSchema$1 = Type.Record(Type.String(), Type.String());
|
|
6662
6682
|
function writeTokenFromRequest(request) {
|
|
6663
6683
|
const electricClaimToken = request.headers.get(`electric-claim-token`)?.trim();
|
|
6664
6684
|
if (electricClaimToken) return electricClaimToken;
|
|
@@ -6675,7 +6695,7 @@ const wakeConditionSchema = Type.Union([Type.Literal(`runFinished`), Type.Object
|
|
|
6675
6695
|
})]);
|
|
6676
6696
|
const spawnBodySchema = Type.Object({
|
|
6677
6697
|
args: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
6678
|
-
tags: Type.Optional(stringRecordSchema),
|
|
6698
|
+
tags: Type.Optional(stringRecordSchema$1),
|
|
6679
6699
|
parent: Type.Optional(Type.String()),
|
|
6680
6700
|
dispatch_policy: Type.Optional(dispatchPolicySchema),
|
|
6681
6701
|
initialMessage: Type.Optional(Type.Unknown()),
|
|
@@ -6684,7 +6704,8 @@ const spawnBodySchema = Type.Object({
|
|
|
6684
6704
|
condition: wakeConditionSchema,
|
|
6685
6705
|
debounceMs: Type.Optional(Type.Number()),
|
|
6686
6706
|
timeoutMs: Type.Optional(Type.Number()),
|
|
6687
|
-
includeResponse: Type.Optional(Type.Boolean())
|
|
6707
|
+
includeResponse: Type.Optional(Type.Boolean()),
|
|
6708
|
+
manifestKey: Type.Optional(Type.String())
|
|
6688
6709
|
}))
|
|
6689
6710
|
});
|
|
6690
6711
|
const sendBodySchema = Type.Object({
|
|
@@ -6750,10 +6771,24 @@ const scheduleBodySchema = Type.Union([Type.Object({
|
|
|
6750
6771
|
messageType: Type.Optional(Type.String()),
|
|
6751
6772
|
from: Type.Optional(Type.String())
|
|
6752
6773
|
})]);
|
|
6753
|
-
const
|
|
6774
|
+
const subscriptionLifetimeSchema = Type.Union([
|
|
6775
|
+
Type.Object({ kind: Type.Literal(`until_entity_stopped`) }),
|
|
6776
|
+
Type.Object({
|
|
6777
|
+
kind: Type.Literal(`expires_at`),
|
|
6778
|
+
at: Type.String()
|
|
6779
|
+
}),
|
|
6780
|
+
Type.Object({ kind: Type.Literal(`manual`) })
|
|
6781
|
+
]);
|
|
6782
|
+
const eventSourceSubscriptionBodySchema = Type.Object({
|
|
6783
|
+
sourceKey: Type.String(),
|
|
6784
|
+
bucketKey: Type.Optional(Type.String()),
|
|
6785
|
+
params: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
6786
|
+
filterKey: Type.Optional(Type.String()),
|
|
6787
|
+
lifetime: Type.Optional(subscriptionLifetimeSchema),
|
|
6788
|
+
reason: Type.Optional(Type.String())
|
|
6789
|
+
});
|
|
6754
6790
|
const entitiesRouter = Router({ base: `/_electric/entities` });
|
|
6755
6791
|
entitiesRouter.get(`/`, listEntities);
|
|
6756
|
-
entitiesRouter.post(`/register`, withSchema(entitiesRegisterBodySchema), registerEntitiesSource);
|
|
6757
6792
|
entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), spawnEntity);
|
|
6758
6793
|
entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
|
|
6759
6794
|
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
|
|
@@ -6764,9 +6799,11 @@ entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity,
|
|
|
6764
6799
|
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
|
|
6765
6800
|
entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
|
|
6766
6801
|
entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
|
|
6767
|
-
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity,
|
|
6802
|
+
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, deleteTag);
|
|
6768
6803
|
entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), upsertSchedule);
|
|
6769
6804
|
entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, deleteSchedule);
|
|
6805
|
+
entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), upsertEventSourceSubscription);
|
|
6806
|
+
entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, deleteEventSourceSubscription);
|
|
6770
6807
|
function entityUrlFromSegments(type, instanceId) {
|
|
6771
6808
|
if (!type || !instanceId) return null;
|
|
6772
6809
|
if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
|
|
@@ -6830,11 +6867,6 @@ async function listEntities({ query }, ctx) {
|
|
|
6830
6867
|
});
|
|
6831
6868
|
return json(entities$1.map((entity) => toPublicEntity(entity)));
|
|
6832
6869
|
}
|
|
6833
|
-
async function registerEntitiesSource(request, ctx) {
|
|
6834
|
-
const parsed = routeBody(request);
|
|
6835
|
-
const result = await ctx.entityManager.registerEntitiesSource(parsed.tags ?? {});
|
|
6836
|
-
return json(result);
|
|
6837
|
-
}
|
|
6838
6870
|
async function upsertSchedule(request, ctx) {
|
|
6839
6871
|
const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
|
|
6840
6872
|
if (principalMutationError) return principalMutationError;
|
|
@@ -6873,8 +6905,49 @@ async function deleteSchedule(request, ctx) {
|
|
|
6873
6905
|
const result = await ctx.entityManager.deleteSchedule(entityUrl, { id: decodeURIComponent(request.params.scheduleId) });
|
|
6874
6906
|
return json(result);
|
|
6875
6907
|
}
|
|
6908
|
+
async function upsertEventSourceSubscription(request, ctx) {
|
|
6909
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `subscribed to event sources`);
|
|
6910
|
+
if (principalMutationError) return principalMutationError;
|
|
6911
|
+
const catalog = ctx.eventSources;
|
|
6912
|
+
if (!catalog) return apiError(404, ErrCodeNotFound, `No event source catalog is configured`);
|
|
6913
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6914
|
+
const parsed = routeBody(request);
|
|
6915
|
+
const source = await catalog.getEventSource(parsed.sourceKey);
|
|
6916
|
+
if (!source) return apiError(404, ErrCodeNotFound, `Event source "${parsed.sourceKey}" not found`);
|
|
6917
|
+
if (parsed.lifetime?.kind === `expires_at`) {
|
|
6918
|
+
const expiresAt = new Date(parsed.lifetime.at);
|
|
6919
|
+
if (Number.isNaN(expiresAt.getTime())) return apiError(400, ErrCodeInvalidRequest, `Invalid expires_at lifetime timestamp`);
|
|
6920
|
+
}
|
|
6921
|
+
let resolved;
|
|
6922
|
+
try {
|
|
6923
|
+
resolved = resolveEventSourceSubscription({
|
|
6924
|
+
contract: source,
|
|
6925
|
+
entityUrl,
|
|
6926
|
+
request: {
|
|
6927
|
+
...parsed,
|
|
6928
|
+
id: decodeURIComponent(request.params.subscriptionId)
|
|
6929
|
+
},
|
|
6930
|
+
createdBy: `tool`
|
|
6931
|
+
});
|
|
6932
|
+
} catch (error) {
|
|
6933
|
+
return apiError(400, ErrCodeInvalidRequest, error instanceof Error ? error.message : String(error));
|
|
6934
|
+
}
|
|
6935
|
+
await ctx.ensureEventSourceWakeSource?.(resolved.subscription.sourceUrl);
|
|
6936
|
+
const result = await ctx.entityManager.upsertEventSourceSubscription(entityUrl, {
|
|
6937
|
+
subscription: resolved.subscription,
|
|
6938
|
+
manifest: buildEventSourceManifestEntry(resolved)
|
|
6939
|
+
});
|
|
6940
|
+
return json(result);
|
|
6941
|
+
}
|
|
6942
|
+
async function deleteEventSourceSubscription(request, ctx) {
|
|
6943
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `unsubscribed from event sources`);
|
|
6944
|
+
if (principalMutationError) return principalMutationError;
|
|
6945
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6946
|
+
const result = await ctx.entityManager.deleteEventSourceSubscription(entityUrl, { id: decodeURIComponent(request.params.subscriptionId) });
|
|
6947
|
+
return json(result);
|
|
6948
|
+
}
|
|
6876
6949
|
async function setTag(request, ctx) {
|
|
6877
|
-
const principalMutationError = rejectPrincipalEntityMutation(request, `
|
|
6950
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `tag updated`);
|
|
6878
6951
|
if (principalMutationError) return principalMutationError;
|
|
6879
6952
|
const parsed = routeBody(request);
|
|
6880
6953
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
@@ -6882,12 +6955,12 @@ async function setTag(request, ctx) {
|
|
|
6882
6955
|
const updated = await ctx.entityManager.setTag(entityUrl, decodeURIComponent(request.params.tagKey), { value: parsed.value }, token);
|
|
6883
6956
|
return json(toPublicEntity(updated));
|
|
6884
6957
|
}
|
|
6885
|
-
async function
|
|
6886
|
-
const principalMutationError = rejectPrincipalEntityMutation(request, `
|
|
6958
|
+
async function deleteTag(request, ctx) {
|
|
6959
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `tag deleted`);
|
|
6887
6960
|
if (principalMutationError) return principalMutationError;
|
|
6888
6961
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6889
6962
|
const token = writeTokenFromRequest(request);
|
|
6890
|
-
const updated = await ctx.entityManager.
|
|
6963
|
+
const updated = await ctx.entityManager.deleteTag(entityUrl, decodeURIComponent(request.params.tagKey), token);
|
|
6891
6964
|
return json(toPublicEntity(updated));
|
|
6892
6965
|
}
|
|
6893
6966
|
async function forkEntity(request, ctx) {
|
|
@@ -7019,17 +7092,13 @@ const registerEntityTypeBodySchema = Type.Object({
|
|
|
7019
7092
|
creation_schema: Type.Optional(jsonObjectSchema),
|
|
7020
7093
|
inbox_schemas: Type.Optional(schemaMapSchema),
|
|
7021
7094
|
state_schemas: Type.Optional(schemaMapSchema),
|
|
7022
|
-
input_schemas: Type.Optional(schemaMapSchema),
|
|
7023
|
-
output_schemas: Type.Optional(schemaMapSchema),
|
|
7024
7095
|
serve_endpoint: Type.Optional(Type.String()),
|
|
7025
7096
|
default_dispatch_policy: Type.Optional(dispatchPolicySchema)
|
|
7026
|
-
});
|
|
7097
|
+
}, { additionalProperties: false });
|
|
7027
7098
|
const amendEntityTypeSchemasBodySchema = Type.Object({
|
|
7028
|
-
input_schemas: Type.Optional(schemaMapSchema),
|
|
7029
|
-
output_schemas: Type.Optional(schemaMapSchema),
|
|
7030
7099
|
inbox_schemas: Type.Optional(schemaMapSchema),
|
|
7031
7100
|
state_schemas: Type.Optional(schemaMapSchema)
|
|
7032
|
-
});
|
|
7101
|
+
}, { additionalProperties: false });
|
|
7033
7102
|
const entityTypesRouter = Router({ base: `/_electric/entity-types` });
|
|
7034
7103
|
entityTypesRouter.get(`/`, listEntityTypes);
|
|
7035
7104
|
entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), registerEntityType);
|
|
@@ -7069,8 +7138,8 @@ async function getEntityType(request, ctx) {
|
|
|
7069
7138
|
async function amendSchemas(request, ctx) {
|
|
7070
7139
|
const parsed = routeBody(request);
|
|
7071
7140
|
const updated = await ctx.entityManager.amendSchemas(request.params.name, {
|
|
7072
|
-
inbox_schemas: parsed.inbox_schemas
|
|
7073
|
-
state_schemas: parsed.state_schemas
|
|
7141
|
+
inbox_schemas: parsed.inbox_schemas,
|
|
7142
|
+
state_schemas: parsed.state_schemas
|
|
7074
7143
|
});
|
|
7075
7144
|
return json(toPublicEntityType(updated));
|
|
7076
7145
|
}
|
|
@@ -7080,13 +7149,12 @@ async function deleteEntityType(request, ctx) {
|
|
|
7080
7149
|
}
|
|
7081
7150
|
function normalizeEntityTypeRequest(parsed) {
|
|
7082
7151
|
const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
|
|
7083
|
-
const compatibilityFields = parsed;
|
|
7084
7152
|
return {
|
|
7085
7153
|
name: parsed.name ?? ``,
|
|
7086
7154
|
description: parsed.description ?? ``,
|
|
7087
7155
|
creation_schema: parsed.creation_schema,
|
|
7088
|
-
inbox_schemas: parsed.inbox_schemas
|
|
7089
|
-
state_schemas: parsed.state_schemas
|
|
7156
|
+
inbox_schemas: parsed.inbox_schemas,
|
|
7157
|
+
state_schemas: parsed.state_schemas,
|
|
7090
7158
|
serve_endpoint: serveEndpoint,
|
|
7091
7159
|
default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
|
|
7092
7160
|
type: `webhook`,
|
|
@@ -7097,8 +7165,6 @@ function normalizeEntityTypeRequest(parsed) {
|
|
|
7097
7165
|
function toPublicEntityType(entityType) {
|
|
7098
7166
|
return {
|
|
7099
7167
|
...entityType,
|
|
7100
|
-
input_schemas: entityType.inbox_schemas,
|
|
7101
|
-
output_schemas: entityType.state_schemas,
|
|
7102
7168
|
revision: entityType.revision
|
|
7103
7169
|
};
|
|
7104
7170
|
}
|
|
@@ -7182,13 +7248,35 @@ function errorMapper(err, req) {
|
|
|
7182
7248
|
function rejectIfShuttingDown(req, ctx) {
|
|
7183
7249
|
if (!ctx.isShuttingDown()) return void 0;
|
|
7184
7250
|
const path$1 = new URL(req.url).pathname;
|
|
7185
|
-
if (!path$1.startsWith(`/_electric/
|
|
7251
|
+
if (!path$1.startsWith(`/_electric/subscription-webhooks/`)) return void 0;
|
|
7186
7252
|
return apiError(503, `SERVER_STOPPING`, `Server is shutting down`);
|
|
7187
7253
|
}
|
|
7188
7254
|
function getRequestSpan(req) {
|
|
7189
7255
|
return carrier(req)[SPAN_KEY];
|
|
7190
7256
|
}
|
|
7191
7257
|
|
|
7258
|
+
//#endregion
|
|
7259
|
+
//#region src/routing/observations-router.ts
|
|
7260
|
+
const stringRecordSchema = Type.Record(Type.String(), Type.String());
|
|
7261
|
+
const ensureEntitiesMembershipStreamBodySchema = Type.Object({ tags: Type.Optional(stringRecordSchema) });
|
|
7262
|
+
const ensureCronStreamBodySchema = Type.Object({
|
|
7263
|
+
expression: Type.String(),
|
|
7264
|
+
timezone: Type.Optional(Type.String())
|
|
7265
|
+
});
|
|
7266
|
+
const observationsRouter = Router({ base: `/_electric/observations` });
|
|
7267
|
+
observationsRouter.post(`/entities/ensure-stream`, withSchema(ensureEntitiesMembershipStreamBodySchema), ensureEntitiesMembershipStream);
|
|
7268
|
+
observationsRouter.post(`/cron/ensure-stream`, withSchema(ensureCronStreamBodySchema), ensureCronStream);
|
|
7269
|
+
async function ensureEntitiesMembershipStream(request, ctx) {
|
|
7270
|
+
const parsed = routeBody(request);
|
|
7271
|
+
const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {});
|
|
7272
|
+
return json(result);
|
|
7273
|
+
}
|
|
7274
|
+
async function ensureCronStream(request, ctx) {
|
|
7275
|
+
const parsed = routeBody(request);
|
|
7276
|
+
const streamPath = await ctx.entityManager.getOrCreateCronStream(parsed.expression, parsed.timezone);
|
|
7277
|
+
return json({ streamUrl: streamPath });
|
|
7278
|
+
}
|
|
7279
|
+
|
|
7192
7280
|
//#endregion
|
|
7193
7281
|
//#region src/routing/tenant-stream-paths.ts
|
|
7194
7282
|
function withLeadingSlash(path$1) {
|
|
@@ -7527,7 +7615,7 @@ async function notificationFromClaim(ctx, input) {
|
|
|
7527
7615
|
wakeId: input.claim.wake_id,
|
|
7528
7616
|
streamPath: primaryStream,
|
|
7529
7617
|
streams: streams$1,
|
|
7530
|
-
callback: appendPathToUrl(ctx.publicUrl, `/_electric/
|
|
7618
|
+
callback: appendPathToUrl(ctx.publicUrl, `/_electric/wake-callbacks/${encodeURIComponent(input.claim.wake_id)}`),
|
|
7531
7619
|
claimToken: input.claim.token,
|
|
7532
7620
|
triggerEvent: `message_received`,
|
|
7533
7621
|
entity: {
|
|
@@ -7562,7 +7650,7 @@ const wakeRegistrationBodySchema = Type.Object({
|
|
|
7562
7650
|
includeResponse: Type.Optional(Type.Boolean()),
|
|
7563
7651
|
manifestKey: Type.Optional(Type.String())
|
|
7564
7652
|
});
|
|
7565
|
-
const
|
|
7653
|
+
const subscriptionWebhookBodySchema = Type.Object({
|
|
7566
7654
|
subscription_id: Type.Optional(Type.String()),
|
|
7567
7655
|
wake_id: Type.Optional(Type.String()),
|
|
7568
7656
|
generation: Type.Optional(Type.Number()),
|
|
@@ -7576,7 +7664,7 @@ const webhookForwardBodySchema = Type.Object({
|
|
|
7576
7664
|
consumer_id: Type.Optional(Type.String()),
|
|
7577
7665
|
callback: Type.Optional(Type.String())
|
|
7578
7666
|
}, { additionalProperties: true });
|
|
7579
|
-
const
|
|
7667
|
+
const wakeCallbackBodySchema = Type.Object({
|
|
7580
7668
|
epoch: Type.Optional(Type.Number()),
|
|
7581
7669
|
generation: Type.Optional(Type.Number()),
|
|
7582
7670
|
wakeId: Type.Optional(Type.String()),
|
|
@@ -7587,14 +7675,15 @@ const callbackForwardBodySchema = Type.Object({
|
|
|
7587
7675
|
const DS_SUBSCRIPTION_CALLBACK_PREFIX = `ds-subscription:`;
|
|
7588
7676
|
const internalRouter = Router({ base: `/_electric` });
|
|
7589
7677
|
internalRouter.get(`/health`, () => json({ status: `ok` }));
|
|
7678
|
+
internalRouter.get(`/event-sources`, listEventSources);
|
|
7590
7679
|
internalRouter.post(`/wake`, withSchema(wakeRegistrationBodySchema), registerWake);
|
|
7591
|
-
internalRouter.post(`/
|
|
7592
|
-
internalRouter.post(`/
|
|
7680
|
+
internalRouter.post(`/subscription-webhooks/:subscriptionId`, subscriptionWebhook);
|
|
7681
|
+
internalRouter.post(`/wake-callbacks/:consumerId`, wakeCallback);
|
|
7593
7682
|
internalRouter.all(`/runners`, runnersRouter.fetch);
|
|
7594
7683
|
internalRouter.all(`/runners/*`, runnersRouter.fetch);
|
|
7595
7684
|
internalRouter.all(`/entities/*`, entitiesRouter.fetch);
|
|
7596
7685
|
internalRouter.all(`/entity-types/*`, entityTypesRouter.fetch);
|
|
7597
|
-
internalRouter.all(`/
|
|
7686
|
+
internalRouter.all(`/observations/*`, observationsRouter.fetch);
|
|
7598
7687
|
internalRouter.get(`/electric/*`, electricProxyRouter.fetch);
|
|
7599
7688
|
internalRouter.all(`*`, () => status(404));
|
|
7600
7689
|
function routeParam(request, name) {
|
|
@@ -7627,13 +7716,30 @@ function resolveWebhookSigner(ctx) {
|
|
|
7627
7716
|
return ctx.webhookSigner ?? getDefaultWebhookSigner();
|
|
7628
7717
|
}
|
|
7629
7718
|
function durableStreamsWebhookJwksUrl(ctx) {
|
|
7630
|
-
if (!ctx.durableStreamsRouting) return
|
|
7719
|
+
if (!ctx.durableStreamsRouting) return appendPathToBackendUrl(ctx.durableStreamsUrl, `/__ds/jwks.json`);
|
|
7631
7720
|
return resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl).controlUrl({
|
|
7632
7721
|
durableStreamsUrl: ctx.durableStreamsUrl,
|
|
7633
7722
|
serviceId: ctx.service,
|
|
7634
7723
|
requestUrl: appendPathToUrl(ctx.publicUrl, `/__ds/jwks.json`)
|
|
7635
7724
|
}).toString();
|
|
7636
7725
|
}
|
|
7726
|
+
function appendPathToBackendUrl(baseUrl, path$1) {
|
|
7727
|
+
const base = new URL(baseUrl);
|
|
7728
|
+
const pathUrl = new URL(path$1, `http://electric-agents.local`);
|
|
7729
|
+
const basePath = base.pathname === `/` ? `` : base.pathname.replace(/\/+$/, ``);
|
|
7730
|
+
const suffix = pathUrl.pathname.startsWith(`/`) ? pathUrl.pathname : `/${pathUrl.pathname}`;
|
|
7731
|
+
const target = new URL(base);
|
|
7732
|
+
target.pathname = `${basePath}${suffix}`;
|
|
7733
|
+
target.search = ``;
|
|
7734
|
+
target.hash = pathUrl.hash;
|
|
7735
|
+
base.searchParams.forEach((value, key) => {
|
|
7736
|
+
target.searchParams.append(key, value);
|
|
7737
|
+
});
|
|
7738
|
+
pathUrl.searchParams.forEach((value, key) => {
|
|
7739
|
+
target.searchParams.append(key, value);
|
|
7740
|
+
});
|
|
7741
|
+
return target.toString();
|
|
7742
|
+
}
|
|
7637
7743
|
function durableStreamsJwksFetchClient(ctx) {
|
|
7638
7744
|
return async (input, init) => {
|
|
7639
7745
|
const headers = new Headers(init?.headers);
|
|
@@ -7690,10 +7796,17 @@ async function registerWake(request, ctx) {
|
|
|
7690
7796
|
await ctx.entityManager.registerWake(opts);
|
|
7691
7797
|
return status(204);
|
|
7692
7798
|
}
|
|
7693
|
-
async function
|
|
7799
|
+
async function listEventSources(_request, ctx) {
|
|
7800
|
+
const eventSources = ctx.eventSources ? await ctx.eventSources.listEventSources() : [];
|
|
7801
|
+
return json({ eventSources: eventSources.filter(isAgentVisibleEventSource) });
|
|
7802
|
+
}
|
|
7803
|
+
function isAgentVisibleEventSource(source) {
|
|
7804
|
+
return source.agentVisible === true && source.status === `active`;
|
|
7805
|
+
}
|
|
7806
|
+
async function subscriptionWebhook(request, ctx) {
|
|
7694
7807
|
const subscriptionId = routeParam(request, `subscriptionId`);
|
|
7695
7808
|
const rootSpan = getRequestSpan(request);
|
|
7696
|
-
rootSpan?.updateName(`webhook
|
|
7809
|
+
rootSpan?.updateName(`subscription-webhook`);
|
|
7697
7810
|
rootSpan?.setAttribute(`electric_agents.webhook.subscription_id`, subscriptionId);
|
|
7698
7811
|
const body = await readRequestBody(request);
|
|
7699
7812
|
const signatureError = await verifyDurableStreamsWebhook(request, ctx, body);
|
|
@@ -7707,7 +7820,7 @@ async function webhookForward(request, ctx) {
|
|
|
7707
7820
|
}
|
|
7708
7821
|
});
|
|
7709
7822
|
if (!targetWebhookUrl) return apiError(404, ErrCodeSubscriptionNotFound, `Unknown webhook subscription`);
|
|
7710
|
-
const parsedBodyResult = validateOptionalJsonBody(
|
|
7823
|
+
const parsedBodyResult = validateOptionalJsonBody(subscriptionWebhookBodySchema, body, request.headers.get(`content-type`));
|
|
7711
7824
|
if (!parsedBodyResult.ok) return parsedBodyResult.response;
|
|
7712
7825
|
let forwardBody = body;
|
|
7713
7826
|
let runningEntityUrl = null;
|
|
@@ -7753,7 +7866,7 @@ async function webhookForward(request, ctx) {
|
|
|
7753
7866
|
span.end();
|
|
7754
7867
|
}
|
|
7755
7868
|
}).catch((err) => {
|
|
7756
|
-
serverLog.warn(`[webhook
|
|
7869
|
+
serverLog.warn(`[subscription-webhook] consumerCallbacks upsert failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
7757
7870
|
}) : void 0;
|
|
7758
7871
|
const [entity, enriched] = await Promise.all([entityPromise, enrichPromise]);
|
|
7759
7872
|
if (entity?.status === `stopped` || entity?.status === `paused`) {
|
|
@@ -7774,7 +7887,7 @@ async function webhookForward(request, ctx) {
|
|
|
7774
7887
|
runningEntityUrl = entity.url;
|
|
7775
7888
|
}
|
|
7776
7889
|
if (consumerId && callbackUrl) {
|
|
7777
|
-
const callback = appendPathToUrl(ctx.publicUrl, `/_electric/
|
|
7890
|
+
const callback = appendPathToUrl(ctx.publicUrl, `/_electric/wake-callbacks/${encodeURIComponent(consumerId)}`);
|
|
7778
7891
|
enriched.callback = callback;
|
|
7779
7892
|
if (newWebhook) {
|
|
7780
7893
|
enriched.consumerId = newWebhook.wakeId;
|
|
@@ -7811,21 +7924,21 @@ async function webhookForward(request, ctx) {
|
|
|
7811
7924
|
});
|
|
7812
7925
|
} catch (err) {
|
|
7813
7926
|
if (runningEntityUrl) await ctx.entityManager.registry.updateStatus(runningEntityUrl, `idle`);
|
|
7814
|
-
return apiError(502, `
|
|
7927
|
+
return apiError(502, `SUBSCRIPTION_WEBHOOK_FAILED`, err instanceof Error ? err.message : String(err));
|
|
7815
7928
|
}
|
|
7816
7929
|
const responseBytes = upstream.body ? new Uint8Array(await upstream.arrayBuffer()) : new Uint8Array();
|
|
7817
7930
|
return responseFromUpstream(upstream, responseBytes);
|
|
7818
7931
|
}
|
|
7819
|
-
async function
|
|
7932
|
+
async function wakeCallback(request, ctx) {
|
|
7820
7933
|
const consumerId = routeParam(request, `consumerId`);
|
|
7821
7934
|
const rows = await ctx.pgDb.select().from(consumerCallbacks).where(and(eq(consumerCallbacks.tenantId, ctx.service), eq(consumerCallbacks.consumerId, consumerId))).limit(1);
|
|
7822
7935
|
const target = rows[0] ? {
|
|
7823
7936
|
callbackUrl: rows[0].callbackUrl,
|
|
7824
7937
|
primaryStream: rows[0].primaryStream
|
|
7825
7938
|
} : void 0;
|
|
7826
|
-
if (!target) return apiError(404,
|
|
7939
|
+
if (!target) return apiError(404, ErrCodeWakeCallbackNotFound, `Unknown wake-callback consumer`);
|
|
7827
7940
|
const body = await readRequestBody(request);
|
|
7828
|
-
const parsedBodyResult = validateOptionalJsonBody(
|
|
7941
|
+
const parsedBodyResult = validateOptionalJsonBody(wakeCallbackBodySchema, body, request.headers.get(`content-type`));
|
|
7829
7942
|
if (!parsedBodyResult.ok) return parsedBodyResult.response;
|
|
7830
7943
|
const requestBody = parsedBodyResult.value;
|
|
7831
7944
|
const isClaimRequest = requestBody?.wakeId !== void 0 || requestBody?.wake_id !== void 0;
|
|
@@ -7843,14 +7956,14 @@ async function callbackForward(request, ctx) {
|
|
|
7843
7956
|
}
|
|
7844
7957
|
return json(responseBody);
|
|
7845
7958
|
}
|
|
7846
|
-
const upstreamBody =
|
|
7959
|
+
const upstreamBody = encodeWakeCallbackBody(ctx.service, consumerId, requestBody, resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl));
|
|
7847
7960
|
let upstream;
|
|
7848
7961
|
try {
|
|
7849
7962
|
const subscriptionId = durableStreamsSubscriptionCallback(target.callbackUrl);
|
|
7850
7963
|
if (subscriptionId) {
|
|
7851
7964
|
const token = claimTokenFromRequest(request);
|
|
7852
7965
|
if (!token) return apiError(401, `UNAUTHORIZED`, `Missing claim token`);
|
|
7853
|
-
const upstreamPayload =
|
|
7966
|
+
const upstreamPayload = encodeWakeCallbackPayload(consumerId, requestBody, (stream) => stream.replace(/^\/+/, ``));
|
|
7854
7967
|
const result = await ctx.streamClient.ackSubscription(subscriptionId, token, upstreamPayload);
|
|
7855
7968
|
upstream = json(result);
|
|
7856
7969
|
} else upstream = await fetch(target.callbackUrl, {
|
|
@@ -7859,7 +7972,7 @@ async function callbackForward(request, ctx) {
|
|
|
7859
7972
|
body: bodyFromBytes(upstreamBody)
|
|
7860
7973
|
});
|
|
7861
7974
|
} catch (err) {
|
|
7862
|
-
return apiError(502, `
|
|
7975
|
+
return apiError(502, `WAKE_CALLBACK_FAILED`, err instanceof Error ? err.message : String(err));
|
|
7863
7976
|
}
|
|
7864
7977
|
let responseBytes = upstream.body ? new Uint8Array(await upstream.arrayBuffer()) : new Uint8Array();
|
|
7865
7978
|
if (isClaimRequest && upstream.ok && target.primaryStream) {
|
|
@@ -7879,7 +7992,7 @@ async function callbackForward(request, ctx) {
|
|
|
7879
7992
|
epoch
|
|
7880
7993
|
});
|
|
7881
7994
|
if (upstream.ok && isDoneRequest && target.primaryStream) {
|
|
7882
|
-
serverLog.info(`[callback
|
|
7995
|
+
serverLog.info(`[wake-callback] done received for stream=${target.primaryStream} consumer=${consumerId}`);
|
|
7883
7996
|
const stillOwnsClaim = ctx.runtime.claimWriteTokens.owns(ctx.service, target.primaryStream, consumerId);
|
|
7884
7997
|
const entity = await ctx.entityManager.registry.getEntityByStream(target.primaryStream);
|
|
7885
7998
|
let entityCleared = false;
|
|
@@ -7901,13 +8014,13 @@ async function callbackForward(request, ctx) {
|
|
|
7901
8014
|
if (entity && (entityCleared || stillOwnsClaim)) {
|
|
7902
8015
|
await ctx.entityManager.registry.updateStatus(entity.url, entity.status === `stopping` ? `stopped` : `idle`);
|
|
7903
8016
|
await ctx.entityBridgeManager.onEntityChanged(entity.url);
|
|
7904
|
-
serverLog.info(`[callback
|
|
7905
|
-
} else if (!entity) serverLog.warn(`[callback
|
|
8017
|
+
serverLog.info(`[wake-callback] status updated after done for ${entity.url}`);
|
|
8018
|
+
} else if (!entity) serverLog.warn(`[wake-callback] done received but no entity found for stream=${target.primaryStream}`);
|
|
7906
8019
|
if (stillOwnsClaim) ctx.runtime.claimWriteTokens.clearStream(ctx.service, target.primaryStream);
|
|
7907
|
-
else if (entity) serverLog.info(`[callback
|
|
7908
|
-
} else if (requestBody?.done === true) serverLog.warn(`[callback
|
|
8020
|
+
else if (entity) serverLog.info(`[wake-callback] done arrived after in-memory token evicted (stream=${target.primaryStream} consumer=${consumerId})`);
|
|
8021
|
+
} else if (requestBody?.done === true) serverLog.warn(`[wake-callback] done received but skipped: upstream.ok=${upstream.ok} primaryStream=${target.primaryStream ?? `null`} consumer=${consumerId}`);
|
|
7909
8022
|
} catch (err) {
|
|
7910
|
-
serverLog.error(`[callback
|
|
8023
|
+
serverLog.error(`[wake-callback] error processing done for consumer=${consumerId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7911
8024
|
}
|
|
7912
8025
|
return responseFromUpstream(upstream, responseBytes);
|
|
7913
8026
|
}
|
|
@@ -7916,11 +8029,11 @@ async function mintClaimWriteToken(ctx, streamPath, consumerId) {
|
|
|
7916
8029
|
if (!entity) return void 0;
|
|
7917
8030
|
return ctx.runtime.claimWriteTokens.mint(ctx.service, streamPath, consumerId);
|
|
7918
8031
|
}
|
|
7919
|
-
function
|
|
7920
|
-
const payload =
|
|
8032
|
+
function encodeWakeCallbackBody(service, consumerId, body, routingAdapter) {
|
|
8033
|
+
const payload = encodeWakeCallbackPayload(consumerId, body, (stream) => routingAdapter.toBackendStreamPath(service, stream));
|
|
7921
8034
|
return new TextEncoder().encode(JSON.stringify(payload));
|
|
7922
8035
|
}
|
|
7923
|
-
function
|
|
8036
|
+
function encodeWakeCallbackPayload(consumerId, body, mapStream) {
|
|
7924
8037
|
if (!body) return {};
|
|
7925
8038
|
const generation = body.generation ?? body.epoch;
|
|
7926
8039
|
const wakeId = body.wake_id ?? body.wakeId ?? consumerId;
|
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.11",
|
|
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@5d5c217",
|
|
40
40
|
"@durable-streams/server": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/server@eac712f",
|
|
41
41
|
"@durable-streams/state": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/state@5d5c217",
|
|
42
|
-
"@electric-sql/client": "^1.5.
|
|
42
|
+
"@electric-sql/client": "^1.5.19",
|
|
43
43
|
"@mariozechner/pi-agent-core": "^0.70.2",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1",
|
|
45
45
|
"@sinclair/typebox": "^0.34.48",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"pino-pretty": "^13.0.0",
|
|
55
55
|
"postgres": "^3.4.0",
|
|
56
56
|
"undici": "^7.24.7",
|
|
57
|
-
"@electric-ax/agents-runtime": "0.3.
|
|
57
|
+
"@electric-ax/agents-runtime": "0.3.5"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^22.19.15",
|
|
@@ -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
|
|
69
|
-
"@electric-ax/agents": "0.4.
|
|
70
|
-
"@electric-ax/agents-server-
|
|
68
|
+
"@electric-ax/agents": "0.4.9",
|
|
69
|
+
"@electric-ax/agents-server-ui": "0.4.11",
|
|
70
|
+
"@electric-ax/agents-server-conformance-tests": "0.1.8"
|
|
71
71
|
},
|
|
72
72
|
"files": [
|
|
73
73
|
"dist",
|