@electric-ax/agents-server 0.4.6 → 0.4.9
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 +314 -38
- package/dist/index.cjs +332 -37
- package/dist/index.d.cts +72 -5
- package/dist/index.d.ts +72 -5
- package/dist/index.js +328 -39
- package/drizzle/0009_entity_signal_statuses.sql +3 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +6 -6
- package/src/db/schema.ts +1 -1
- package/src/electric-agents-types.ts +77 -1
- package/src/entity-manager.ts +320 -33
- package/src/entity-registry.ts +40 -16
- package/src/index.ts +29 -1
- package/src/manifest-side-effects.ts +11 -0
- package/src/routing/context.ts +18 -1
- package/src/routing/dispatch-policy.ts +6 -0
- package/src/routing/entities-router.ts +187 -1
- package/src/routing/internal-router.ts +25 -4
- package/src/routing/runners-router.ts +1 -1
- package/src/runtime.ts +5 -1
- package/src/server.ts +12 -0
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";
|
|
@@ -75,7 +75,7 @@ const entities = pgTable(`entities`, {
|
|
|
75
75
|
index(`idx_entities_parent`).on(table.tenantId, table.parent),
|
|
76
76
|
index(`idx_entities_created_by`).on(table.tenantId, table.createdBy),
|
|
77
77
|
index(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
|
|
78
|
-
check(`chk_entities_status`, sql`${table.status} IN ('spawning', 'running', 'idle', 'stopped')`)
|
|
78
|
+
check(`chk_entities_status`, sql`${table.status} IN ('spawning', 'running', 'idle', 'paused', 'stopping', 'stopped', 'killed')`)
|
|
79
79
|
]);
|
|
80
80
|
const users = pgTable(`users`, {
|
|
81
81
|
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
@@ -329,12 +329,25 @@ async function runMigrations(postgresUrl) {
|
|
|
329
329
|
|
|
330
330
|
//#endregion
|
|
331
331
|
//#region src/electric-agents-types.ts
|
|
332
|
+
const ENTITY_SIGNALS = [
|
|
333
|
+
`SIGINT`,
|
|
334
|
+
`SIGHUP`,
|
|
335
|
+
`SIGTERM`,
|
|
336
|
+
`SIGKILL`,
|
|
337
|
+
`SIGSTOP`,
|
|
338
|
+
`SIGCONT`,
|
|
339
|
+
`SIGUSR`
|
|
340
|
+
];
|
|
332
341
|
const VALID_ENTITY_STATUSES = new Set([
|
|
333
342
|
`spawning`,
|
|
334
343
|
`running`,
|
|
335
344
|
`idle`,
|
|
336
|
-
`
|
|
345
|
+
`paused`,
|
|
346
|
+
`stopping`,
|
|
347
|
+
`stopped`,
|
|
348
|
+
`killed`
|
|
337
349
|
]);
|
|
350
|
+
const VALID_ENTITY_SIGNALS = new Set(ENTITY_SIGNALS);
|
|
338
351
|
function assertEntityStatus(s) {
|
|
339
352
|
if (!VALID_ENTITY_STATUSES.has(s)) throw new Error(`Invalid entity status: "${s}"`);
|
|
340
353
|
return s;
|
|
@@ -355,6 +368,27 @@ function assertRunnerAdminStatus(s) {
|
|
|
355
368
|
if (!VALID_RUNNER_ADMIN_STATUSES.has(s)) throw new Error(`Invalid runner admin status: "${s}"`);
|
|
356
369
|
return s;
|
|
357
370
|
}
|
|
371
|
+
function assertEntitySignal(s) {
|
|
372
|
+
if (!VALID_ENTITY_SIGNALS.has(s)) throw new Error(`Invalid entity signal: "${s}"`);
|
|
373
|
+
return s;
|
|
374
|
+
}
|
|
375
|
+
function isTerminalEntityStatus(status$1) {
|
|
376
|
+
return status$1 === `stopped` || status$1 === `killed`;
|
|
377
|
+
}
|
|
378
|
+
function rejectsNormalWrites(status$1) {
|
|
379
|
+
return status$1 === `stopping` || isTerminalEntityStatus(status$1);
|
|
380
|
+
}
|
|
381
|
+
function expectedSignalStatus(status$1, signal) {
|
|
382
|
+
switch (signal) {
|
|
383
|
+
case `SIGKILL`: return `killed`;
|
|
384
|
+
case `SIGTERM`: return status$1 === `idle` ? `stopped` : `stopping`;
|
|
385
|
+
case `SIGSTOP`: return status$1 === `idle` ? `paused` : status$1;
|
|
386
|
+
case `SIGCONT`: return status$1 === `paused` ? `idle` : status$1;
|
|
387
|
+
case `SIGINT`:
|
|
388
|
+
case `SIGHUP`:
|
|
389
|
+
case `SIGUSR`: return status$1;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
358
392
|
/** Strip internal fields (write_token, subscription_id) from an entity. */
|
|
359
393
|
function toPublicEntity(entity) {
|
|
360
394
|
return {
|
|
@@ -376,6 +410,7 @@ const ErrCodeUnauthorized = `UNAUTHORIZED`;
|
|
|
376
410
|
const ErrCodeNotFound = `NOT_FOUND`;
|
|
377
411
|
const ErrCodeNotRunning = `NOT_RUNNING`;
|
|
378
412
|
const ErrCodeInvalidRequest = `INVALID_REQUEST`;
|
|
413
|
+
const ErrCodeInvalidSignal = `INVALID_SIGNAL`;
|
|
379
414
|
const ErrCodeUnknownEntityType = `UNKNOWN_ENTITY_TYPE`;
|
|
380
415
|
const ErrCodeSchemaValidationFailed = `SCHEMA_VALIDATION_FAILED`;
|
|
381
416
|
const ErrCodeUnknownMessageType = `UNKNOWN_MESSAGE_TYPE`;
|
|
@@ -777,7 +812,7 @@ var PostgresRegistry = class {
|
|
|
777
812
|
};
|
|
778
813
|
}
|
|
779
814
|
async updateStatus(entityUrl, status$1) {
|
|
780
|
-
const whereClause = status$1
|
|
815
|
+
const whereClause = isTerminalEntityStatus(status$1) ? this.entityWhere(entityUrl) : and(this.entityWhere(entityUrl), ne(entities.status, `stopped`), ne(entities.status, `killed`));
|
|
781
816
|
await this.db.update(entities).set({
|
|
782
817
|
status: status$1,
|
|
783
818
|
updatedAt: Date.now()
|
|
@@ -785,13 +820,17 @@ var PostgresRegistry = class {
|
|
|
785
820
|
}
|
|
786
821
|
async updateStatusWithTxid(entityUrl, status$1) {
|
|
787
822
|
return await this.db.transaction(async (tx) => {
|
|
788
|
-
const
|
|
789
|
-
await tx.update(entities).set({
|
|
823
|
+
const rows = await tx.update(entities).set({
|
|
790
824
|
status: status$1,
|
|
791
825
|
updatedAt: Date.now()
|
|
792
|
-
}).where(
|
|
793
|
-
|
|
794
|
-
|
|
826
|
+
}).where(and(this.entityWhere(entityUrl), ne(entities.status, `stopped`), ne(entities.status, `killed`))).returning({ txid: sql`pg_current_xact_id()::xid::text` });
|
|
827
|
+
return rows[0] ? parseInt(rows[0].txid) : null;
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
async touchEntityWithTxid(entityUrl) {
|
|
831
|
+
return await this.db.transaction(async (tx) => {
|
|
832
|
+
const rows = await tx.update(entities).set({ updatedAt: Date.now() }).where(and(eq(entities.url, entityUrl), ne(entities.status, `stopped`), ne(entities.status, `killed`))).returning({ txid: sql`pg_current_xact_id()::xid::text` });
|
|
833
|
+
return rows[0] ? parseInt(rows[0].txid) : null;
|
|
795
834
|
});
|
|
796
835
|
}
|
|
797
836
|
async setEntityTag(url, key, value) {
|
|
@@ -2418,6 +2457,9 @@ async function assertDispatchPolicyAllowed(ctx, policy) {
|
|
|
2418
2457
|
if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
|
|
2419
2458
|
if (runner.owner_principal !== ctx.principal.url) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner dispatch requires the authenticated owner`, 403);
|
|
2420
2459
|
}
|
|
2460
|
+
function shouldLinkDispatchBeforeInitialMessage(policy) {
|
|
2461
|
+
return policy?.targets[0] !== void 0;
|
|
2462
|
+
}
|
|
2421
2463
|
async function linkEntityDispatchSubscription(ctx, entity) {
|
|
2422
2464
|
const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
|
|
2423
2465
|
const target = dispatchPolicy?.targets[0];
|
|
@@ -2582,6 +2624,10 @@ function extractManifestSourceUrl(manifest) {
|
|
|
2582
2624
|
}
|
|
2583
2625
|
if (manifest.sourceType === `entities`) return typeof manifest.sourceRef === `string` ? `/_entities/${manifest.sourceRef}` : void 0;
|
|
2584
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
|
+
}
|
|
2585
2631
|
return void 0;
|
|
2586
2632
|
}
|
|
2587
2633
|
if (manifest.kind === `shared-state`) return typeof manifest.id === `string` ? getSharedStateStreamPath(manifest.id) : void 0;
|
|
@@ -2643,6 +2689,7 @@ function createInitialQueuePosition(date) {
|
|
|
2643
2689
|
}
|
|
2644
2690
|
const DEFAULT_FORK_WAIT_TIMEOUT_MS = 12e4;
|
|
2645
2691
|
const DEFAULT_FORK_WAIT_POLL_MS = 250;
|
|
2692
|
+
const SERVER_SIGNAL_SENDER = `/_electric/server`;
|
|
2646
2693
|
function sleep(ms) {
|
|
2647
2694
|
return new Promise((resolve$1) => setTimeout(resolve$1, ms));
|
|
2648
2695
|
}
|
|
@@ -2867,7 +2914,8 @@ var EntityManager = class {
|
|
|
2867
2914
|
debounceMs: req.wake.debounceMs,
|
|
2868
2915
|
timeoutMs: req.wake.timeoutMs,
|
|
2869
2916
|
oneShot: false,
|
|
2870
|
-
includeResponse: req.wake.includeResponse
|
|
2917
|
+
includeResponse: req.wake.includeResponse,
|
|
2918
|
+
manifestKey: req.wake.manifestKey
|
|
2871
2919
|
});
|
|
2872
2920
|
const contentType = `application/json`;
|
|
2873
2921
|
const createdEvent = entityStateSchema.entityCreated.insert({
|
|
@@ -3094,16 +3142,16 @@ var EntityManager = class {
|
|
|
3094
3142
|
if (!root) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3095
3143
|
if (root.parent) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Only top-level entities can be forked`, 400);
|
|
3096
3144
|
const subtree = await this.listEntitySubtree(root);
|
|
3097
|
-
const stopped = subtree.find((entity) => entity.status
|
|
3098
|
-
if (stopped) throw new ElectricAgentsError(ErrCodeNotRunning, `Cannot fork
|
|
3099
|
-
let active = subtree.filter((entity) => entity.status !== `idle`);
|
|
3145
|
+
const stopped = subtree.find((entity) => isTerminalEntityStatus(entity.status));
|
|
3146
|
+
if (stopped) throw new ElectricAgentsError(ErrCodeNotRunning, `Cannot fork terminal entity "${stopped.url}"`, 409);
|
|
3147
|
+
let active = subtree.filter((entity) => entity.status !== `idle` && entity.status !== `paused`);
|
|
3100
3148
|
if (active.length === 0) {
|
|
3101
3149
|
this.addForkLocks(this.forkWorkLockedEntities, subtree.map((entity) => entity.url), workLocks);
|
|
3102
3150
|
const lockedRoot = await this.registry.getEntity(rootUrl);
|
|
3103
3151
|
if (!lockedRoot) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3104
3152
|
const lockedSubtree = await this.listEntitySubtree(lockedRoot);
|
|
3105
3153
|
this.addForkLocks(this.forkWorkLockedEntities, lockedSubtree.map((entity) => entity.url), workLocks);
|
|
3106
|
-
const lockedActive = lockedSubtree.filter((entity) => entity.status !== `idle`);
|
|
3154
|
+
const lockedActive = lockedSubtree.filter((entity) => entity.status !== `idle` && entity.status !== `paused`);
|
|
3107
3155
|
if (lockedActive.length === 0) return lockedSubtree;
|
|
3108
3156
|
this.releaseForkLocks(this.forkWorkLockedEntities, workLocks);
|
|
3109
3157
|
active = lockedActive;
|
|
@@ -3559,6 +3607,11 @@ var EntityManager = class {
|
|
|
3559
3607
|
if (req.position) value.position = req.position;
|
|
3560
3608
|
else if (value.mode === `queued` || value.mode === `paused`) value.position = createInitialQueuePosition(new Date(now));
|
|
3561
3609
|
if (value.status === `processed`) value.processed_at = now;
|
|
3610
|
+
const wakePausedEntity = entity.status === `paused` && req.mode !== `paused`;
|
|
3611
|
+
if (wakePausedEntity) {
|
|
3612
|
+
await this.registry.updateStatus(entityUrl, `idle`);
|
|
3613
|
+
await this.entityBridgeManager?.onEntityChanged(entityUrl);
|
|
3614
|
+
}
|
|
3562
3615
|
const envelope = entityStateSchema.inbox.insert({
|
|
3563
3616
|
key,
|
|
3564
3617
|
value
|
|
@@ -3586,7 +3639,7 @@ var EntityManager = class {
|
|
|
3586
3639
|
async updateInboxMessage(entityUrl, key, req) {
|
|
3587
3640
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3588
3641
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3589
|
-
if (entity.status
|
|
3642
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
3590
3643
|
const now = new Date().toISOString();
|
|
3591
3644
|
const value = {};
|
|
3592
3645
|
if (`payload` in req) value.payload = req.payload;
|
|
@@ -3607,7 +3660,7 @@ var EntityManager = class {
|
|
|
3607
3660
|
async deleteInboxMessage(entityUrl, key) {
|
|
3608
3661
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3609
3662
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3610
|
-
if (entity.status
|
|
3663
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
3611
3664
|
const envelope = entityStateSchema.inbox.delete({ key });
|
|
3612
3665
|
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
|
|
3613
3666
|
}
|
|
@@ -3615,7 +3668,7 @@ var EntityManager = class {
|
|
|
3615
3668
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3616
3669
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3617
3670
|
if (!this.isValidWriteToken(entity, token)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Invalid write token`, 401);
|
|
3618
|
-
if (entity.status
|
|
3671
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
3619
3672
|
if (typeof req.value !== `string`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Tag values must be strings`, 400);
|
|
3620
3673
|
const result = await this.registry.setEntityTag(entityUrl, key, req.value);
|
|
3621
3674
|
const updated = result.entity;
|
|
@@ -3627,7 +3680,7 @@ var EntityManager = class {
|
|
|
3627
3680
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3628
3681
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3629
3682
|
if (!this.isValidWriteToken(entity, token)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Invalid write token`, 401);
|
|
3630
|
-
if (entity.status
|
|
3683
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
3631
3684
|
const result = await this.registry.removeEntityTag(entityUrl, key);
|
|
3632
3685
|
const updated = result.entity;
|
|
3633
3686
|
if (!updated) throw new ElectricAgentsError(ErrCodeEntityPersistFailed, `Entity not found after tag delete`, 500);
|
|
@@ -3756,6 +3809,35 @@ var EntityManager = class {
|
|
|
3756
3809
|
await this.writeManifestEntry(entityUrl, manifestKey, `delete`, void 0, { txid });
|
|
3757
3810
|
return { txid };
|
|
3758
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
|
+
}
|
|
3759
3841
|
/**
|
|
3760
3842
|
* Register a wake subscription from a subscriber to a source entity.
|
|
3761
3843
|
*/
|
|
@@ -3880,26 +3962,131 @@ var EntityManager = class {
|
|
|
3880
3962
|
}
|
|
3881
3963
|
};
|
|
3882
3964
|
}
|
|
3883
|
-
async
|
|
3965
|
+
async signal(entityUrl, req) {
|
|
3884
3966
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3885
3967
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
const
|
|
3889
|
-
|
|
3890
|
-
const
|
|
3891
|
-
|
|
3892
|
-
|
|
3968
|
+
if (isTerminalEntityStatus(entity.status)) throw new ElectricAgentsError(ErrCodeInvalidSignal, `Cannot signal a ${entity.status} entity`, 409);
|
|
3969
|
+
const now = new Date();
|
|
3970
|
+
const previousState = entity.status;
|
|
3971
|
+
const handling = this.serverHandlingForSignal(previousState, req.signal);
|
|
3972
|
+
const txid = handling.status === previousState ? await this.registry.touchEntityWithTxid(entityUrl) : await this.registry.updateStatusWithTxid(entityUrl, handling.status);
|
|
3973
|
+
if (txid === null) throw new ElectricAgentsError(ErrCodeInvalidSignal, `Cannot signal entity because it is already terminal`, 409);
|
|
3974
|
+
const key = `sig-${now.getTime()}-${randomUUID().slice(0, 8)}`;
|
|
3975
|
+
const signalValue = {
|
|
3976
|
+
signal: req.signal,
|
|
3977
|
+
status: handling.handled ? `handled` : `unhandled`,
|
|
3978
|
+
sender: SERVER_SIGNAL_SENDER,
|
|
3979
|
+
timestamp: now.toISOString()
|
|
3980
|
+
};
|
|
3981
|
+
if (req.reason !== void 0) signalValue.reason = req.reason;
|
|
3982
|
+
if (req.payload !== void 0) signalValue.payload = req.payload;
|
|
3983
|
+
if (handling.handled) {
|
|
3984
|
+
signalValue.handled_at = now.toISOString();
|
|
3985
|
+
signalValue.handled_by = SERVER_SIGNAL_SENDER;
|
|
3986
|
+
signalValue.outcome = handling.outcome;
|
|
3987
|
+
signalValue.previous_state = previousState;
|
|
3988
|
+
signalValue.new_state = handling.status;
|
|
3989
|
+
}
|
|
3990
|
+
const signalEvent = {
|
|
3991
|
+
type: `signal`,
|
|
3992
|
+
key,
|
|
3993
|
+
value: signalValue,
|
|
3994
|
+
headers: {
|
|
3995
|
+
operation: `insert`,
|
|
3996
|
+
timestamp: now.toISOString(),
|
|
3997
|
+
txid: String(txid)
|
|
3998
|
+
}
|
|
3999
|
+
};
|
|
4000
|
+
const shouldCloseStreams = isTerminalEntityStatus(handling.status);
|
|
4001
|
+
await this.appendSignalEvent(entity, signalEvent, shouldCloseStreams);
|
|
4002
|
+
if (!shouldCloseStreams) await this.evaluateWakes(entityUrl, signalEvent);
|
|
4003
|
+
if (handling.unregisterWakes) {
|
|
4004
|
+
await this.wakeRegistry.unregisterBySubscriber(entityUrl, this.tenantId);
|
|
4005
|
+
await this.wakeRegistry.unregisterBySource(entityUrl, this.tenantId);
|
|
4006
|
+
}
|
|
4007
|
+
if (handling.status !== previousState && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
4008
|
+
return {
|
|
4009
|
+
url: entityUrl,
|
|
4010
|
+
signal: req.signal,
|
|
4011
|
+
previous_state: previousState,
|
|
4012
|
+
new_state: handling.status,
|
|
4013
|
+
created_at: now.getTime(),
|
|
4014
|
+
txid
|
|
4015
|
+
};
|
|
4016
|
+
}
|
|
4017
|
+
async kill(entityUrl) {
|
|
4018
|
+
const response = await this.signal(entityUrl, {
|
|
4019
|
+
signal: `SIGKILL`,
|
|
4020
|
+
reason: `Legacy kill command`
|
|
3893
4021
|
});
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
4022
|
+
return { txid: response.txid };
|
|
4023
|
+
}
|
|
4024
|
+
serverHandlingForSignal(status$1, signal) {
|
|
4025
|
+
if (signal === `SIGKILL`) return {
|
|
4026
|
+
status: `killed`,
|
|
4027
|
+
handled: true,
|
|
4028
|
+
outcome: `transitioned`,
|
|
4029
|
+
unregisterWakes: true
|
|
4030
|
+
};
|
|
4031
|
+
if (signal === `SIGTERM`) {
|
|
4032
|
+
if (status$1 === `idle` || status$1 === `paused`) return {
|
|
4033
|
+
status: `stopped`,
|
|
4034
|
+
handled: true,
|
|
4035
|
+
outcome: `transitioned`,
|
|
4036
|
+
unregisterWakes: true
|
|
4037
|
+
};
|
|
4038
|
+
if (status$1 === `running`) return {
|
|
4039
|
+
status: `stopping`,
|
|
4040
|
+
handled: false,
|
|
4041
|
+
outcome: `transitioned`,
|
|
4042
|
+
unregisterWakes: false
|
|
4043
|
+
};
|
|
4044
|
+
}
|
|
4045
|
+
if (status$1 === `paused` && signal !== `SIGCONT`) return {
|
|
4046
|
+
status: status$1,
|
|
4047
|
+
handled: true,
|
|
4048
|
+
outcome: `ignored`,
|
|
4049
|
+
unregisterWakes: false
|
|
4050
|
+
};
|
|
4051
|
+
if (signal === `SIGSTOP` && (status$1 === `idle` || status$1 === `running`)) return {
|
|
4052
|
+
status: `paused`,
|
|
4053
|
+
handled: status$1 === `idle`,
|
|
4054
|
+
outcome: `transitioned`,
|
|
4055
|
+
unregisterWakes: false
|
|
4056
|
+
};
|
|
4057
|
+
if (signal === `SIGCONT` && status$1 === `paused`) return {
|
|
4058
|
+
status: `idle`,
|
|
4059
|
+
handled: false,
|
|
4060
|
+
outcome: `transitioned`,
|
|
4061
|
+
unregisterWakes: false
|
|
4062
|
+
};
|
|
4063
|
+
return {
|
|
4064
|
+
status: status$1,
|
|
4065
|
+
handled: false,
|
|
4066
|
+
outcome: `ignored`,
|
|
4067
|
+
unregisterWakes: false
|
|
4068
|
+
};
|
|
4069
|
+
}
|
|
4070
|
+
async appendSignalEvent(entity, signalEvent, closeStreams) {
|
|
4071
|
+
const signalData = this.encodeChangeEvent(signalEvent);
|
|
4072
|
+
if (!closeStreams) {
|
|
4073
|
+
await this.streamClient.append(entity.streams.main, signalData);
|
|
4074
|
+
return;
|
|
4075
|
+
}
|
|
4076
|
+
const errorCloseEvent = {
|
|
4077
|
+
type: `signal`,
|
|
4078
|
+
key: signalEvent.key,
|
|
4079
|
+
value: signalEvent.value,
|
|
4080
|
+
headers: signalEvent.headers
|
|
4081
|
+
};
|
|
4082
|
+
const errorSignalData = this.encodeChangeEvent(errorCloseEvent);
|
|
4083
|
+
for (const [streamPath, data] of [[entity.streams.main, signalData], [entity.streams.error, errorSignalData]]) try {
|
|
4084
|
+
await this.streamClient.append(streamPath, data, { close: true });
|
|
3897
4085
|
} catch (err) {
|
|
3898
4086
|
const message = err instanceof Error ? err.message : String(err);
|
|
3899
4087
|
if (/closed/i.test(message) || /not found/i.test(message) || /404/.test(message) || /409/.test(message)) continue;
|
|
3900
4088
|
throw err;
|
|
3901
4089
|
}
|
|
3902
|
-
return { txid };
|
|
3903
4090
|
}
|
|
3904
4091
|
async validateWriteEvent(entity, event) {
|
|
3905
4092
|
if (!entity.type) return null;
|
|
@@ -4015,7 +4202,7 @@ var EntityManager = class {
|
|
|
4015
4202
|
async validateSendRequest(entityUrl, req) {
|
|
4016
4203
|
const entity = await this.registry.getEntity(entityUrl);
|
|
4017
4204
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
4018
|
-
if (entity.status
|
|
4205
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
4019
4206
|
if (req.type && entity.type) {
|
|
4020
4207
|
const { inboxSchemas } = await this.getEffectiveSchemas(entity);
|
|
4021
4208
|
if (inboxSchemas) {
|
|
@@ -4980,7 +5167,8 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
4980
5167
|
const primaryStream = `${entityUrl}/main`;
|
|
4981
5168
|
const callbacks = await this.db.select().from(consumerCallbacks).where(and(eq(consumerCallbacks.tenantId, this.serviceId), eq(consumerCallbacks.primaryStream, primaryStream))).limit(1);
|
|
4982
5169
|
if (callbacks.length > 0) return;
|
|
4983
|
-
await this.manager.registry.
|
|
5170
|
+
const entity = await this.manager.registry.getEntity(entityUrl);
|
|
5171
|
+
await this.manager.registry.updateStatus(entityUrl, entity?.status === `stopping` ? `stopped` : `idle`);
|
|
4984
5172
|
await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
4985
5173
|
}
|
|
4986
5174
|
};
|
|
@@ -6530,7 +6718,8 @@ const spawnBodySchema = Type.Object({
|
|
|
6530
6718
|
condition: wakeConditionSchema,
|
|
6531
6719
|
debounceMs: Type.Optional(Type.Number()),
|
|
6532
6720
|
timeoutMs: Type.Optional(Type.Number()),
|
|
6533
|
-
includeResponse: Type.Optional(Type.Boolean())
|
|
6721
|
+
includeResponse: Type.Optional(Type.Boolean()),
|
|
6722
|
+
manifestKey: Type.Optional(Type.String())
|
|
6534
6723
|
}))
|
|
6535
6724
|
});
|
|
6536
6725
|
const sendBodySchema = Type.Object({
|
|
@@ -6567,6 +6756,20 @@ const forkBodySchema = Type.Object({
|
|
|
6567
6756
|
waitTimeoutMs: Type.Optional(Type.Number())
|
|
6568
6757
|
});
|
|
6569
6758
|
const setTagBodySchema = Type.Object({ value: Type.String() });
|
|
6759
|
+
const entitySignalSchema = Type.Union([
|
|
6760
|
+
Type.Literal(`SIGINT`),
|
|
6761
|
+
Type.Literal(`SIGHUP`),
|
|
6762
|
+
Type.Literal(`SIGTERM`),
|
|
6763
|
+
Type.Literal(`SIGKILL`),
|
|
6764
|
+
Type.Literal(`SIGSTOP`),
|
|
6765
|
+
Type.Literal(`SIGCONT`),
|
|
6766
|
+
Type.Literal(`SIGUSR`)
|
|
6767
|
+
]);
|
|
6768
|
+
const signalBodySchema = Type.Object({
|
|
6769
|
+
signal: entitySignalSchema,
|
|
6770
|
+
reason: Type.Optional(Type.String()),
|
|
6771
|
+
payload: Type.Optional(Type.Unknown())
|
|
6772
|
+
});
|
|
6570
6773
|
const scheduleBodySchema = Type.Union([Type.Object({
|
|
6571
6774
|
scheduleType: Type.Literal(`cron`),
|
|
6572
6775
|
expression: Type.String(),
|
|
@@ -6582,6 +6785,22 @@ const scheduleBodySchema = Type.Union([Type.Object({
|
|
|
6582
6785
|
messageType: Type.Optional(Type.String()),
|
|
6583
6786
|
from: Type.Optional(Type.String())
|
|
6584
6787
|
})]);
|
|
6788
|
+
const subscriptionLifetimeSchema = Type.Union([
|
|
6789
|
+
Type.Object({ kind: Type.Literal(`until_entity_stopped`) }),
|
|
6790
|
+
Type.Object({
|
|
6791
|
+
kind: Type.Literal(`expires_at`),
|
|
6792
|
+
at: Type.String()
|
|
6793
|
+
}),
|
|
6794
|
+
Type.Object({ kind: Type.Literal(`manual`) })
|
|
6795
|
+
]);
|
|
6796
|
+
const eventSourceSubscriptionBodySchema = Type.Object({
|
|
6797
|
+
sourceKey: Type.String(),
|
|
6798
|
+
bucketKey: Type.Optional(Type.String()),
|
|
6799
|
+
params: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
6800
|
+
filterKey: Type.Optional(Type.String()),
|
|
6801
|
+
lifetime: Type.Optional(subscriptionLifetimeSchema),
|
|
6802
|
+
reason: Type.Optional(Type.String())
|
|
6803
|
+
});
|
|
6585
6804
|
const entitiesRegisterBodySchema = Type.Object({ tags: Type.Optional(stringRecordSchema) });
|
|
6586
6805
|
const entitiesRouter = Router({ base: `/_electric/entities` });
|
|
6587
6806
|
entitiesRouter.get(`/`, listEntities);
|
|
@@ -6590,6 +6809,7 @@ entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spa
|
|
|
6590
6809
|
entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
|
|
6591
6810
|
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
|
|
6592
6811
|
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
|
|
6812
|
+
entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), signalEntity);
|
|
6593
6813
|
entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
|
|
6594
6814
|
entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
|
|
6595
6815
|
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
|
|
@@ -6598,6 +6818,8 @@ entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withS
|
|
|
6598
6818
|
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, removeTag);
|
|
6599
6819
|
entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), upsertSchedule);
|
|
6600
6820
|
entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, deleteSchedule);
|
|
6821
|
+
entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), upsertEventSourceSubscription);
|
|
6822
|
+
entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, deleteEventSourceSubscription);
|
|
6601
6823
|
function entityUrlFromSegments(type, instanceId) {
|
|
6602
6824
|
if (!type || !instanceId) return null;
|
|
6603
6825
|
if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
|
|
@@ -6704,6 +6926,47 @@ async function deleteSchedule(request, ctx) {
|
|
|
6704
6926
|
const result = await ctx.entityManager.deleteSchedule(entityUrl, { id: decodeURIComponent(request.params.scheduleId) });
|
|
6705
6927
|
return json(result);
|
|
6706
6928
|
}
|
|
6929
|
+
async function upsertEventSourceSubscription(request, ctx) {
|
|
6930
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `subscribed to event sources`);
|
|
6931
|
+
if (principalMutationError) return principalMutationError;
|
|
6932
|
+
const catalog = ctx.eventSources;
|
|
6933
|
+
if (!catalog) return apiError(404, ErrCodeNotFound, `No event source catalog is configured`);
|
|
6934
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6935
|
+
const parsed = routeBody(request);
|
|
6936
|
+
const source = await catalog.getEventSource(parsed.sourceKey);
|
|
6937
|
+
if (!source) return apiError(404, ErrCodeNotFound, `Event source "${parsed.sourceKey}" not found`);
|
|
6938
|
+
if (parsed.lifetime?.kind === `expires_at`) {
|
|
6939
|
+
const expiresAt = new Date(parsed.lifetime.at);
|
|
6940
|
+
if (Number.isNaN(expiresAt.getTime())) return apiError(400, ErrCodeInvalidRequest, `Invalid expires_at lifetime timestamp`);
|
|
6941
|
+
}
|
|
6942
|
+
let resolved;
|
|
6943
|
+
try {
|
|
6944
|
+
resolved = resolveEventSourceSubscription({
|
|
6945
|
+
contract: source,
|
|
6946
|
+
entityUrl,
|
|
6947
|
+
request: {
|
|
6948
|
+
...parsed,
|
|
6949
|
+
id: decodeURIComponent(request.params.subscriptionId)
|
|
6950
|
+
},
|
|
6951
|
+
createdBy: `tool`
|
|
6952
|
+
});
|
|
6953
|
+
} catch (error) {
|
|
6954
|
+
return apiError(400, ErrCodeInvalidRequest, error instanceof Error ? error.message : String(error));
|
|
6955
|
+
}
|
|
6956
|
+
await ctx.ensureEventSourceWakeSource?.(resolved.subscription.sourceUrl);
|
|
6957
|
+
const result = await ctx.entityManager.upsertEventSourceSubscription(entityUrl, {
|
|
6958
|
+
subscription: resolved.subscription,
|
|
6959
|
+
manifest: buildEventSourceManifestEntry(resolved)
|
|
6960
|
+
});
|
|
6961
|
+
return json(result);
|
|
6962
|
+
}
|
|
6963
|
+
async function deleteEventSourceSubscription(request, ctx) {
|
|
6964
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `unsubscribed from event sources`);
|
|
6965
|
+
if (principalMutationError) return principalMutationError;
|
|
6966
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6967
|
+
const result = await ctx.entityManager.deleteEventSourceSubscription(entityUrl, { id: decodeURIComponent(request.params.subscriptionId) });
|
|
6968
|
+
return json(result);
|
|
6969
|
+
}
|
|
6707
6970
|
async function setTag(request, ctx) {
|
|
6708
6971
|
const principalMutationError = rejectPrincipalEntityMutation(request, `tagged`);
|
|
6709
6972
|
if (principalMutationError) return principalMutationError;
|
|
@@ -6793,11 +7056,13 @@ async function spawnEntity(request, ctx) {
|
|
|
6793
7056
|
wake: parsed.wake,
|
|
6794
7057
|
created_by: principal.url
|
|
6795
7058
|
});
|
|
6796
|
-
|
|
7059
|
+
const linkBeforeInitialMessage = parsed.initialMessage !== void 0 && shouldLinkDispatchBeforeInitialMessage(dispatchPolicy);
|
|
7060
|
+
if (linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
|
|
6797
7061
|
if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
|
|
6798
7062
|
from: principal.url,
|
|
6799
7063
|
payload: parsed.initialMessage
|
|
6800
7064
|
});
|
|
7065
|
+
if (!linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
|
|
6801
7066
|
return json({
|
|
6802
7067
|
...toPublicEntity(entity),
|
|
6803
7068
|
txid: entity.txid
|
|
@@ -6821,6 +7086,22 @@ async function killEntity(request, ctx) {
|
|
|
6821
7086
|
ctx.runtime.claimWriteTokens.clearStream(ctx.service, entity.streams.main);
|
|
6822
7087
|
return json(result);
|
|
6823
7088
|
}
|
|
7089
|
+
async function signalEntity(request, ctx) {
|
|
7090
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `signaled`);
|
|
7091
|
+
if (principalMutationError) return principalMutationError;
|
|
7092
|
+
const parsed = routeBody(request);
|
|
7093
|
+
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
7094
|
+
const result = await ctx.entityManager.signal(entityUrl, {
|
|
7095
|
+
signal: parsed.signal,
|
|
7096
|
+
reason: parsed.reason,
|
|
7097
|
+
payload: parsed.payload
|
|
7098
|
+
});
|
|
7099
|
+
if (result.new_state === `stopped` || result.new_state === `killed`) {
|
|
7100
|
+
await unlinkEntityDispatchSubscription(ctx, entity);
|
|
7101
|
+
ctx.runtime.claimWriteTokens.clearStream(ctx.service, entity.streams.main);
|
|
7102
|
+
}
|
|
7103
|
+
return json(result);
|
|
7104
|
+
}
|
|
6824
7105
|
|
|
6825
7106
|
//#endregion
|
|
6826
7107
|
//#region src/routing/entity-types-router.ts
|
|
@@ -7301,7 +7582,7 @@ async function notificationFromClaim(ctx, input) {
|
|
|
7301
7582
|
const primaryStream = withLeadingSlash(primary.path);
|
|
7302
7583
|
const entity = await ctx.entityManager.registry.getEntityByStream(primaryStream);
|
|
7303
7584
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Claim stream is not attached to an entity`, 404);
|
|
7304
|
-
if (entity.status === `stopped`) {
|
|
7585
|
+
if (entity.status === `stopped` || entity.status === `paused`) {
|
|
7305
7586
|
await ctx.streamClient.releaseSubscription(input.subscriptionId, input.claim.token, {
|
|
7306
7587
|
wake_id: input.claim.wake_id,
|
|
7307
7588
|
generation: input.claim.generation
|
|
@@ -7400,6 +7681,7 @@ const callbackForwardBodySchema = Type.Object({
|
|
|
7400
7681
|
const DS_SUBSCRIPTION_CALLBACK_PREFIX = `ds-subscription:`;
|
|
7401
7682
|
const internalRouter = Router({ base: `/_electric` });
|
|
7402
7683
|
internalRouter.get(`/health`, () => json({ status: `ok` }));
|
|
7684
|
+
internalRouter.get(`/event-sources`, listEventSources);
|
|
7403
7685
|
internalRouter.post(`/wake`, withSchema(wakeRegistrationBodySchema), registerWake);
|
|
7404
7686
|
internalRouter.post(`/webhook-forward/:subscriptionId`, webhookForward);
|
|
7405
7687
|
internalRouter.post(`/callback-forward/:consumerId`, callbackForward);
|
|
@@ -7503,6 +7785,13 @@ async function registerWake(request, ctx) {
|
|
|
7503
7785
|
await ctx.entityManager.registerWake(opts);
|
|
7504
7786
|
return status(204);
|
|
7505
7787
|
}
|
|
7788
|
+
async function listEventSources(_request, ctx) {
|
|
7789
|
+
const eventSources = ctx.eventSources ? await ctx.eventSources.listEventSources() : [];
|
|
7790
|
+
return json({ eventSources: eventSources.filter(isAgentVisibleEventSource) });
|
|
7791
|
+
}
|
|
7792
|
+
function isAgentVisibleEventSource(source) {
|
|
7793
|
+
return source.agentVisible === true && source.status === `active`;
|
|
7794
|
+
}
|
|
7506
7795
|
async function webhookForward(request, ctx) {
|
|
7507
7796
|
const subscriptionId = routeParam(request, `subscriptionId`);
|
|
7508
7797
|
const rootSpan = getRequestSpan(request);
|
|
@@ -7569,7 +7858,7 @@ async function webhookForward(request, ctx) {
|
|
|
7569
7858
|
serverLog.warn(`[webhook-forward] consumerCallbacks upsert failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
7570
7859
|
}) : void 0;
|
|
7571
7860
|
const [entity, enriched] = await Promise.all([entityPromise, enrichPromise]);
|
|
7572
|
-
if (entity?.status === `stopped`) {
|
|
7861
|
+
if (entity?.status === `stopped` || entity?.status === `paused`) {
|
|
7573
7862
|
if (upsertPromise) await upsertPromise;
|
|
7574
7863
|
return json({ done: true });
|
|
7575
7864
|
}
|
|
@@ -7712,9 +8001,9 @@ async function callbackForward(request, ctx) {
|
|
|
7712
8001
|
entityCleared = result?.entityCleared ?? false;
|
|
7713
8002
|
}
|
|
7714
8003
|
if (entity && (entityCleared || stillOwnsClaim)) {
|
|
7715
|
-
await ctx.entityManager.registry.updateStatus(entity.url, `idle`);
|
|
8004
|
+
await ctx.entityManager.registry.updateStatus(entity.url, entity.status === `stopping` ? `stopped` : `idle`);
|
|
7716
8005
|
await ctx.entityBridgeManager.onEntityChanged(entity.url);
|
|
7717
|
-
serverLog.info(`[callback-forward] status updated
|
|
8006
|
+
serverLog.info(`[callback-forward] status updated after done for ${entity.url}`);
|
|
7718
8007
|
} else if (!entity) serverLog.warn(`[callback-forward] done received but no entity found for stream=${target.primaryStream}`);
|
|
7719
8008
|
if (stillOwnsClaim) ctx.runtime.claimWriteTokens.clearStream(ctx.service, target.primaryStream);
|
|
7720
8009
|
else if (entity) serverLog.info(`[callback-forward] done arrived after in-memory token evicted (stream=${target.primaryStream} consumer=${consumerId})`);
|
|
@@ -7770,4 +8059,4 @@ globalRouter.all(`/_electric/*`, internalRouter.fetch);
|
|
|
7770
8059
|
globalRouter.all(`*`, durableStreamsRouter.fetch);
|
|
7771
8060
|
|
|
7772
8061
|
//#endregion
|
|
7773
|
-
export { AgentsHost, DEFAULT_TENANT_ID, StreamClient, UnregisteredTenantError, createDb, createEd25519WebhookSigner, getDefaultWebhookSigner, globalRouter, isUnregisteredTenantError, pathPrefixedSingleTenantDurableStreamsRoutingAdapter, runMigrations, streamRootDurableStreamsRoutingAdapter, tenantRootDurableStreamsRoutingAdapter, webhookSigningMetadata };
|
|
8062
|
+
export { AgentsHost, DEFAULT_TENANT_ID, StreamClient, UnregisteredTenantError, assertEntitySignal, assertEntityStatus, createDb, createEd25519WebhookSigner, expectedSignalStatus, getDefaultWebhookSigner, globalRouter, isTerminalEntityStatus, isUnregisteredTenantError, pathPrefixedSingleTenantDurableStreamsRoutingAdapter, rejectsNormalWrites, runMigrations, streamRootDurableStreamsRoutingAdapter, tenantRootDurableStreamsRoutingAdapter, toPublicEntity, webhookSigningMetadata };
|