@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.cjs
CHANGED
|
@@ -104,7 +104,7 @@ const entities = (0, drizzle_orm_pg_core.pgTable)(`entities`, {
|
|
|
104
104
|
(0, drizzle_orm_pg_core.index)(`idx_entities_parent`).on(table.tenantId, table.parent),
|
|
105
105
|
(0, drizzle_orm_pg_core.index)(`idx_entities_created_by`).on(table.tenantId, table.createdBy),
|
|
106
106
|
(0, drizzle_orm_pg_core.index)(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
|
|
107
|
-
(0, drizzle_orm_pg_core.check)(`chk_entities_status`, drizzle_orm.sql`${table.status} IN ('spawning', 'running', 'idle', 'stopped')`)
|
|
107
|
+
(0, drizzle_orm_pg_core.check)(`chk_entities_status`, drizzle_orm.sql`${table.status} IN ('spawning', 'running', 'idle', 'paused', 'stopping', 'stopped', 'killed')`)
|
|
108
108
|
]);
|
|
109
109
|
const users = (0, drizzle_orm_pg_core.pgTable)(`users`, {
|
|
110
110
|
tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
|
|
@@ -358,12 +358,25 @@ async function runMigrations(postgresUrl) {
|
|
|
358
358
|
|
|
359
359
|
//#endregion
|
|
360
360
|
//#region src/electric-agents-types.ts
|
|
361
|
+
const ENTITY_SIGNALS = [
|
|
362
|
+
`SIGINT`,
|
|
363
|
+
`SIGHUP`,
|
|
364
|
+
`SIGTERM`,
|
|
365
|
+
`SIGKILL`,
|
|
366
|
+
`SIGSTOP`,
|
|
367
|
+
`SIGCONT`,
|
|
368
|
+
`SIGUSR`
|
|
369
|
+
];
|
|
361
370
|
const VALID_ENTITY_STATUSES = new Set([
|
|
362
371
|
`spawning`,
|
|
363
372
|
`running`,
|
|
364
373
|
`idle`,
|
|
365
|
-
`
|
|
374
|
+
`paused`,
|
|
375
|
+
`stopping`,
|
|
376
|
+
`stopped`,
|
|
377
|
+
`killed`
|
|
366
378
|
]);
|
|
379
|
+
const VALID_ENTITY_SIGNALS = new Set(ENTITY_SIGNALS);
|
|
367
380
|
function assertEntityStatus(s) {
|
|
368
381
|
if (!VALID_ENTITY_STATUSES.has(s)) throw new Error(`Invalid entity status: "${s}"`);
|
|
369
382
|
return s;
|
|
@@ -384,6 +397,27 @@ function assertRunnerAdminStatus(s) {
|
|
|
384
397
|
if (!VALID_RUNNER_ADMIN_STATUSES.has(s)) throw new Error(`Invalid runner admin status: "${s}"`);
|
|
385
398
|
return s;
|
|
386
399
|
}
|
|
400
|
+
function assertEntitySignal(s) {
|
|
401
|
+
if (!VALID_ENTITY_SIGNALS.has(s)) throw new Error(`Invalid entity signal: "${s}"`);
|
|
402
|
+
return s;
|
|
403
|
+
}
|
|
404
|
+
function isTerminalEntityStatus(status$4) {
|
|
405
|
+
return status$4 === `stopped` || status$4 === `killed`;
|
|
406
|
+
}
|
|
407
|
+
function rejectsNormalWrites(status$4) {
|
|
408
|
+
return status$4 === `stopping` || isTerminalEntityStatus(status$4);
|
|
409
|
+
}
|
|
410
|
+
function expectedSignalStatus(status$4, signal) {
|
|
411
|
+
switch (signal) {
|
|
412
|
+
case `SIGKILL`: return `killed`;
|
|
413
|
+
case `SIGTERM`: return status$4 === `idle` ? `stopped` : `stopping`;
|
|
414
|
+
case `SIGSTOP`: return status$4 === `idle` ? `paused` : status$4;
|
|
415
|
+
case `SIGCONT`: return status$4 === `paused` ? `idle` : status$4;
|
|
416
|
+
case `SIGINT`:
|
|
417
|
+
case `SIGHUP`:
|
|
418
|
+
case `SIGUSR`: return status$4;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
387
421
|
/** Strip internal fields (write_token, subscription_id) from an entity. */
|
|
388
422
|
function toPublicEntity(entity) {
|
|
389
423
|
return {
|
|
@@ -405,6 +439,7 @@ const ErrCodeUnauthorized = `UNAUTHORIZED`;
|
|
|
405
439
|
const ErrCodeNotFound = `NOT_FOUND`;
|
|
406
440
|
const ErrCodeNotRunning = `NOT_RUNNING`;
|
|
407
441
|
const ErrCodeInvalidRequest = `INVALID_REQUEST`;
|
|
442
|
+
const ErrCodeInvalidSignal = `INVALID_SIGNAL`;
|
|
408
443
|
const ErrCodeUnknownEntityType = `UNKNOWN_ENTITY_TYPE`;
|
|
409
444
|
const ErrCodeSchemaValidationFailed = `SCHEMA_VALIDATION_FAILED`;
|
|
410
445
|
const ErrCodeUnknownMessageType = `UNKNOWN_MESSAGE_TYPE`;
|
|
@@ -806,7 +841,7 @@ var PostgresRegistry = class {
|
|
|
806
841
|
};
|
|
807
842
|
}
|
|
808
843
|
async updateStatus(entityUrl, status$4) {
|
|
809
|
-
const whereClause = status$4
|
|
844
|
+
const whereClause = isTerminalEntityStatus(status$4) ? this.entityWhere(entityUrl) : (0, drizzle_orm.and)(this.entityWhere(entityUrl), (0, drizzle_orm.ne)(entities.status, `stopped`), (0, drizzle_orm.ne)(entities.status, `killed`));
|
|
810
845
|
await this.db.update(entities).set({
|
|
811
846
|
status: status$4,
|
|
812
847
|
updatedAt: Date.now()
|
|
@@ -814,13 +849,17 @@ var PostgresRegistry = class {
|
|
|
814
849
|
}
|
|
815
850
|
async updateStatusWithTxid(entityUrl, status$4) {
|
|
816
851
|
return await this.db.transaction(async (tx) => {
|
|
817
|
-
const
|
|
818
|
-
await tx.update(entities).set({
|
|
852
|
+
const rows = await tx.update(entities).set({
|
|
819
853
|
status: status$4,
|
|
820
854
|
updatedAt: Date.now()
|
|
821
|
-
}).where(
|
|
822
|
-
|
|
823
|
-
|
|
855
|
+
}).where((0, drizzle_orm.and)(this.entityWhere(entityUrl), (0, drizzle_orm.ne)(entities.status, `stopped`), (0, drizzle_orm.ne)(entities.status, `killed`))).returning({ txid: drizzle_orm.sql`pg_current_xact_id()::xid::text` });
|
|
856
|
+
return rows[0] ? parseInt(rows[0].txid) : null;
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
async touchEntityWithTxid(entityUrl) {
|
|
860
|
+
return await this.db.transaction(async (tx) => {
|
|
861
|
+
const rows = await tx.update(entities).set({ updatedAt: Date.now() }).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entities.url, entityUrl), (0, drizzle_orm.ne)(entities.status, `stopped`), (0, drizzle_orm.ne)(entities.status, `killed`))).returning({ txid: drizzle_orm.sql`pg_current_xact_id()::xid::text` });
|
|
862
|
+
return rows[0] ? parseInt(rows[0].txid) : null;
|
|
824
863
|
});
|
|
825
864
|
}
|
|
826
865
|
async setEntityTag(url, key, value) {
|
|
@@ -2447,6 +2486,9 @@ async function assertDispatchPolicyAllowed(ctx, policy) {
|
|
|
2447
2486
|
if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
|
|
2448
2487
|
if (runner.owner_principal !== ctx.principal.url) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner dispatch requires the authenticated owner`, 403);
|
|
2449
2488
|
}
|
|
2489
|
+
function shouldLinkDispatchBeforeInitialMessage(policy) {
|
|
2490
|
+
return policy?.targets[0] !== void 0;
|
|
2491
|
+
}
|
|
2450
2492
|
async function linkEntityDispatchSubscription(ctx, entity) {
|
|
2451
2493
|
const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
|
|
2452
2494
|
const target = dispatchPolicy?.targets[0];
|
|
@@ -2611,6 +2653,10 @@ function extractManifestSourceUrl(manifest) {
|
|
|
2611
2653
|
}
|
|
2612
2654
|
if (manifest.sourceType === `entities`) return typeof manifest.sourceRef === `string` ? `/_entities/${manifest.sourceRef}` : void 0;
|
|
2613
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
|
+
}
|
|
2614
2660
|
return void 0;
|
|
2615
2661
|
}
|
|
2616
2662
|
if (manifest.kind === `shared-state`) return typeof manifest.id === `string` ? (0, __electric_ax_agents_runtime.getSharedStateStreamPath)(manifest.id) : void 0;
|
|
@@ -2672,6 +2718,7 @@ function createInitialQueuePosition(date) {
|
|
|
2672
2718
|
}
|
|
2673
2719
|
const DEFAULT_FORK_WAIT_TIMEOUT_MS = 12e4;
|
|
2674
2720
|
const DEFAULT_FORK_WAIT_POLL_MS = 250;
|
|
2721
|
+
const SERVER_SIGNAL_SENDER = `/_electric/server`;
|
|
2675
2722
|
function sleep(ms) {
|
|
2676
2723
|
return new Promise((resolve$1) => setTimeout(resolve$1, ms));
|
|
2677
2724
|
}
|
|
@@ -2896,7 +2943,8 @@ var EntityManager = class {
|
|
|
2896
2943
|
debounceMs: req.wake.debounceMs,
|
|
2897
2944
|
timeoutMs: req.wake.timeoutMs,
|
|
2898
2945
|
oneShot: false,
|
|
2899
|
-
includeResponse: req.wake.includeResponse
|
|
2946
|
+
includeResponse: req.wake.includeResponse,
|
|
2947
|
+
manifestKey: req.wake.manifestKey
|
|
2900
2948
|
});
|
|
2901
2949
|
const contentType = `application/json`;
|
|
2902
2950
|
const createdEvent = __electric_ax_agents_runtime.entityStateSchema.entityCreated.insert({
|
|
@@ -3123,16 +3171,16 @@ var EntityManager = class {
|
|
|
3123
3171
|
if (!root) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3124
3172
|
if (root.parent) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Only top-level entities can be forked`, 400);
|
|
3125
3173
|
const subtree = await this.listEntitySubtree(root);
|
|
3126
|
-
const stopped = subtree.find((entity) => entity.status
|
|
3127
|
-
if (stopped) throw new ElectricAgentsError(ErrCodeNotRunning, `Cannot fork
|
|
3128
|
-
let active = subtree.filter((entity) => entity.status !== `idle`);
|
|
3174
|
+
const stopped = subtree.find((entity) => isTerminalEntityStatus(entity.status));
|
|
3175
|
+
if (stopped) throw new ElectricAgentsError(ErrCodeNotRunning, `Cannot fork terminal entity "${stopped.url}"`, 409);
|
|
3176
|
+
let active = subtree.filter((entity) => entity.status !== `idle` && entity.status !== `paused`);
|
|
3129
3177
|
if (active.length === 0) {
|
|
3130
3178
|
this.addForkLocks(this.forkWorkLockedEntities, subtree.map((entity) => entity.url), workLocks);
|
|
3131
3179
|
const lockedRoot = await this.registry.getEntity(rootUrl);
|
|
3132
3180
|
if (!lockedRoot) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3133
3181
|
const lockedSubtree = await this.listEntitySubtree(lockedRoot);
|
|
3134
3182
|
this.addForkLocks(this.forkWorkLockedEntities, lockedSubtree.map((entity) => entity.url), workLocks);
|
|
3135
|
-
const lockedActive = lockedSubtree.filter((entity) => entity.status !== `idle`);
|
|
3183
|
+
const lockedActive = lockedSubtree.filter((entity) => entity.status !== `idle` && entity.status !== `paused`);
|
|
3136
3184
|
if (lockedActive.length === 0) return lockedSubtree;
|
|
3137
3185
|
this.releaseForkLocks(this.forkWorkLockedEntities, workLocks);
|
|
3138
3186
|
active = lockedActive;
|
|
@@ -3588,6 +3636,11 @@ var EntityManager = class {
|
|
|
3588
3636
|
if (req.position) value.position = req.position;
|
|
3589
3637
|
else if (value.mode === `queued` || value.mode === `paused`) value.position = createInitialQueuePosition(new Date(now));
|
|
3590
3638
|
if (value.status === `processed`) value.processed_at = now;
|
|
3639
|
+
const wakePausedEntity = entity.status === `paused` && req.mode !== `paused`;
|
|
3640
|
+
if (wakePausedEntity) {
|
|
3641
|
+
await this.registry.updateStatus(entityUrl, `idle`);
|
|
3642
|
+
await this.entityBridgeManager?.onEntityChanged(entityUrl);
|
|
3643
|
+
}
|
|
3591
3644
|
const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.insert({
|
|
3592
3645
|
key,
|
|
3593
3646
|
value
|
|
@@ -3615,7 +3668,7 @@ var EntityManager = class {
|
|
|
3615
3668
|
async updateInboxMessage(entityUrl, key, req) {
|
|
3616
3669
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3617
3670
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3618
|
-
if (entity.status
|
|
3671
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
3619
3672
|
const now = new Date().toISOString();
|
|
3620
3673
|
const value = {};
|
|
3621
3674
|
if (`payload` in req) value.payload = req.payload;
|
|
@@ -3636,7 +3689,7 @@ var EntityManager = class {
|
|
|
3636
3689
|
async deleteInboxMessage(entityUrl, key) {
|
|
3637
3690
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3638
3691
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3639
|
-
if (entity.status
|
|
3692
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
3640
3693
|
const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.delete({ key });
|
|
3641
3694
|
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
|
|
3642
3695
|
}
|
|
@@ -3644,7 +3697,7 @@ var EntityManager = class {
|
|
|
3644
3697
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3645
3698
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3646
3699
|
if (!this.isValidWriteToken(entity, token)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Invalid write token`, 401);
|
|
3647
|
-
if (entity.status
|
|
3700
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
3648
3701
|
if (typeof req.value !== `string`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Tag values must be strings`, 400);
|
|
3649
3702
|
const result = await this.registry.setEntityTag(entityUrl, key, req.value);
|
|
3650
3703
|
const updated = result.entity;
|
|
@@ -3656,7 +3709,7 @@ var EntityManager = class {
|
|
|
3656
3709
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3657
3710
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3658
3711
|
if (!this.isValidWriteToken(entity, token)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Invalid write token`, 401);
|
|
3659
|
-
if (entity.status
|
|
3712
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
3660
3713
|
const result = await this.registry.removeEntityTag(entityUrl, key);
|
|
3661
3714
|
const updated = result.entity;
|
|
3662
3715
|
if (!updated) throw new ElectricAgentsError(ErrCodeEntityPersistFailed, `Entity not found after tag delete`, 500);
|
|
@@ -3785,6 +3838,35 @@ var EntityManager = class {
|
|
|
3785
3838
|
await this.writeManifestEntry(entityUrl, manifestKey, `delete`, void 0, { txid });
|
|
3786
3839
|
return { txid };
|
|
3787
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
|
+
}
|
|
3788
3870
|
/**
|
|
3789
3871
|
* Register a wake subscription from a subscriber to a source entity.
|
|
3790
3872
|
*/
|
|
@@ -3909,26 +3991,131 @@ var EntityManager = class {
|
|
|
3909
3991
|
}
|
|
3910
3992
|
};
|
|
3911
3993
|
}
|
|
3912
|
-
async
|
|
3994
|
+
async signal(entityUrl, req) {
|
|
3913
3995
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3914
3996
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
const
|
|
3918
|
-
|
|
3919
|
-
const
|
|
3920
|
-
|
|
3921
|
-
|
|
3997
|
+
if (isTerminalEntityStatus(entity.status)) throw new ElectricAgentsError(ErrCodeInvalidSignal, `Cannot signal a ${entity.status} entity`, 409);
|
|
3998
|
+
const now = new Date();
|
|
3999
|
+
const previousState = entity.status;
|
|
4000
|
+
const handling = this.serverHandlingForSignal(previousState, req.signal);
|
|
4001
|
+
const txid = handling.status === previousState ? await this.registry.touchEntityWithTxid(entityUrl) : await this.registry.updateStatusWithTxid(entityUrl, handling.status);
|
|
4002
|
+
if (txid === null) throw new ElectricAgentsError(ErrCodeInvalidSignal, `Cannot signal entity because it is already terminal`, 409);
|
|
4003
|
+
const key = `sig-${now.getTime()}-${(0, node_crypto.randomUUID)().slice(0, 8)}`;
|
|
4004
|
+
const signalValue = {
|
|
4005
|
+
signal: req.signal,
|
|
4006
|
+
status: handling.handled ? `handled` : `unhandled`,
|
|
4007
|
+
sender: SERVER_SIGNAL_SENDER,
|
|
4008
|
+
timestamp: now.toISOString()
|
|
4009
|
+
};
|
|
4010
|
+
if (req.reason !== void 0) signalValue.reason = req.reason;
|
|
4011
|
+
if (req.payload !== void 0) signalValue.payload = req.payload;
|
|
4012
|
+
if (handling.handled) {
|
|
4013
|
+
signalValue.handled_at = now.toISOString();
|
|
4014
|
+
signalValue.handled_by = SERVER_SIGNAL_SENDER;
|
|
4015
|
+
signalValue.outcome = handling.outcome;
|
|
4016
|
+
signalValue.previous_state = previousState;
|
|
4017
|
+
signalValue.new_state = handling.status;
|
|
4018
|
+
}
|
|
4019
|
+
const signalEvent = {
|
|
4020
|
+
type: `signal`,
|
|
4021
|
+
key,
|
|
4022
|
+
value: signalValue,
|
|
4023
|
+
headers: {
|
|
4024
|
+
operation: `insert`,
|
|
4025
|
+
timestamp: now.toISOString(),
|
|
4026
|
+
txid: String(txid)
|
|
4027
|
+
}
|
|
4028
|
+
};
|
|
4029
|
+
const shouldCloseStreams = isTerminalEntityStatus(handling.status);
|
|
4030
|
+
await this.appendSignalEvent(entity, signalEvent, shouldCloseStreams);
|
|
4031
|
+
if (!shouldCloseStreams) await this.evaluateWakes(entityUrl, signalEvent);
|
|
4032
|
+
if (handling.unregisterWakes) {
|
|
4033
|
+
await this.wakeRegistry.unregisterBySubscriber(entityUrl, this.tenantId);
|
|
4034
|
+
await this.wakeRegistry.unregisterBySource(entityUrl, this.tenantId);
|
|
4035
|
+
}
|
|
4036
|
+
if (handling.status !== previousState && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
4037
|
+
return {
|
|
4038
|
+
url: entityUrl,
|
|
4039
|
+
signal: req.signal,
|
|
4040
|
+
previous_state: previousState,
|
|
4041
|
+
new_state: handling.status,
|
|
4042
|
+
created_at: now.getTime(),
|
|
4043
|
+
txid
|
|
4044
|
+
};
|
|
4045
|
+
}
|
|
4046
|
+
async kill(entityUrl) {
|
|
4047
|
+
const response = await this.signal(entityUrl, {
|
|
4048
|
+
signal: `SIGKILL`,
|
|
4049
|
+
reason: `Legacy kill command`
|
|
3922
4050
|
});
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
4051
|
+
return { txid: response.txid };
|
|
4052
|
+
}
|
|
4053
|
+
serverHandlingForSignal(status$4, signal) {
|
|
4054
|
+
if (signal === `SIGKILL`) return {
|
|
4055
|
+
status: `killed`,
|
|
4056
|
+
handled: true,
|
|
4057
|
+
outcome: `transitioned`,
|
|
4058
|
+
unregisterWakes: true
|
|
4059
|
+
};
|
|
4060
|
+
if (signal === `SIGTERM`) {
|
|
4061
|
+
if (status$4 === `idle` || status$4 === `paused`) return {
|
|
4062
|
+
status: `stopped`,
|
|
4063
|
+
handled: true,
|
|
4064
|
+
outcome: `transitioned`,
|
|
4065
|
+
unregisterWakes: true
|
|
4066
|
+
};
|
|
4067
|
+
if (status$4 === `running`) return {
|
|
4068
|
+
status: `stopping`,
|
|
4069
|
+
handled: false,
|
|
4070
|
+
outcome: `transitioned`,
|
|
4071
|
+
unregisterWakes: false
|
|
4072
|
+
};
|
|
4073
|
+
}
|
|
4074
|
+
if (status$4 === `paused` && signal !== `SIGCONT`) return {
|
|
4075
|
+
status: status$4,
|
|
4076
|
+
handled: true,
|
|
4077
|
+
outcome: `ignored`,
|
|
4078
|
+
unregisterWakes: false
|
|
4079
|
+
};
|
|
4080
|
+
if (signal === `SIGSTOP` && (status$4 === `idle` || status$4 === `running`)) return {
|
|
4081
|
+
status: `paused`,
|
|
4082
|
+
handled: status$4 === `idle`,
|
|
4083
|
+
outcome: `transitioned`,
|
|
4084
|
+
unregisterWakes: false
|
|
4085
|
+
};
|
|
4086
|
+
if (signal === `SIGCONT` && status$4 === `paused`) return {
|
|
4087
|
+
status: `idle`,
|
|
4088
|
+
handled: false,
|
|
4089
|
+
outcome: `transitioned`,
|
|
4090
|
+
unregisterWakes: false
|
|
4091
|
+
};
|
|
4092
|
+
return {
|
|
4093
|
+
status: status$4,
|
|
4094
|
+
handled: false,
|
|
4095
|
+
outcome: `ignored`,
|
|
4096
|
+
unregisterWakes: false
|
|
4097
|
+
};
|
|
4098
|
+
}
|
|
4099
|
+
async appendSignalEvent(entity, signalEvent, closeStreams) {
|
|
4100
|
+
const signalData = this.encodeChangeEvent(signalEvent);
|
|
4101
|
+
if (!closeStreams) {
|
|
4102
|
+
await this.streamClient.append(entity.streams.main, signalData);
|
|
4103
|
+
return;
|
|
4104
|
+
}
|
|
4105
|
+
const errorCloseEvent = {
|
|
4106
|
+
type: `signal`,
|
|
4107
|
+
key: signalEvent.key,
|
|
4108
|
+
value: signalEvent.value,
|
|
4109
|
+
headers: signalEvent.headers
|
|
4110
|
+
};
|
|
4111
|
+
const errorSignalData = this.encodeChangeEvent(errorCloseEvent);
|
|
4112
|
+
for (const [streamPath, data] of [[entity.streams.main, signalData], [entity.streams.error, errorSignalData]]) try {
|
|
4113
|
+
await this.streamClient.append(streamPath, data, { close: true });
|
|
3926
4114
|
} catch (err) {
|
|
3927
4115
|
const message = err instanceof Error ? err.message : String(err);
|
|
3928
4116
|
if (/closed/i.test(message) || /not found/i.test(message) || /404/.test(message) || /409/.test(message)) continue;
|
|
3929
4117
|
throw err;
|
|
3930
4118
|
}
|
|
3931
|
-
return { txid };
|
|
3932
4119
|
}
|
|
3933
4120
|
async validateWriteEvent(entity, event) {
|
|
3934
4121
|
if (!entity.type) return null;
|
|
@@ -4044,7 +4231,7 @@ var EntityManager = class {
|
|
|
4044
4231
|
async validateSendRequest(entityUrl, req) {
|
|
4045
4232
|
const entity = await this.registry.getEntity(entityUrl);
|
|
4046
4233
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
4047
|
-
if (entity.status
|
|
4234
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
4048
4235
|
if (req.type && entity.type) {
|
|
4049
4236
|
const { inboxSchemas } = await this.getEffectiveSchemas(entity);
|
|
4050
4237
|
if (inboxSchemas) {
|
|
@@ -5009,7 +5196,8 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
5009
5196
|
const primaryStream = `${entityUrl}/main`;
|
|
5010
5197
|
const callbacks = await this.db.select().from(consumerCallbacks).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(consumerCallbacks.tenantId, this.serviceId), (0, drizzle_orm.eq)(consumerCallbacks.primaryStream, primaryStream))).limit(1);
|
|
5011
5198
|
if (callbacks.length > 0) return;
|
|
5012
|
-
await this.manager.registry.
|
|
5199
|
+
const entity = await this.manager.registry.getEntity(entityUrl);
|
|
5200
|
+
await this.manager.registry.updateStatus(entityUrl, entity?.status === `stopping` ? `stopped` : `idle`);
|
|
5013
5201
|
await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
5014
5202
|
}
|
|
5015
5203
|
};
|
|
@@ -6559,7 +6747,8 @@ const spawnBodySchema = __sinclair_typebox.Type.Object({
|
|
|
6559
6747
|
condition: wakeConditionSchema,
|
|
6560
6748
|
debounceMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
6561
6749
|
timeoutMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
6562
|
-
includeResponse: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean())
|
|
6750
|
+
includeResponse: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean()),
|
|
6751
|
+
manifestKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
6563
6752
|
}))
|
|
6564
6753
|
});
|
|
6565
6754
|
const sendBodySchema = __sinclair_typebox.Type.Object({
|
|
@@ -6596,6 +6785,20 @@ const forkBodySchema = __sinclair_typebox.Type.Object({
|
|
|
6596
6785
|
waitTimeoutMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number())
|
|
6597
6786
|
});
|
|
6598
6787
|
const setTagBodySchema = __sinclair_typebox.Type.Object({ value: __sinclair_typebox.Type.String() });
|
|
6788
|
+
const entitySignalSchema = __sinclair_typebox.Type.Union([
|
|
6789
|
+
__sinclair_typebox.Type.Literal(`SIGINT`),
|
|
6790
|
+
__sinclair_typebox.Type.Literal(`SIGHUP`),
|
|
6791
|
+
__sinclair_typebox.Type.Literal(`SIGTERM`),
|
|
6792
|
+
__sinclair_typebox.Type.Literal(`SIGKILL`),
|
|
6793
|
+
__sinclair_typebox.Type.Literal(`SIGSTOP`),
|
|
6794
|
+
__sinclair_typebox.Type.Literal(`SIGCONT`),
|
|
6795
|
+
__sinclair_typebox.Type.Literal(`SIGUSR`)
|
|
6796
|
+
]);
|
|
6797
|
+
const signalBodySchema = __sinclair_typebox.Type.Object({
|
|
6798
|
+
signal: entitySignalSchema,
|
|
6799
|
+
reason: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6800
|
+
payload: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown())
|
|
6801
|
+
});
|
|
6599
6802
|
const scheduleBodySchema = __sinclair_typebox.Type.Union([__sinclair_typebox.Type.Object({
|
|
6600
6803
|
scheduleType: __sinclair_typebox.Type.Literal(`cron`),
|
|
6601
6804
|
expression: __sinclair_typebox.Type.String(),
|
|
@@ -6611,6 +6814,22 @@ const scheduleBodySchema = __sinclair_typebox.Type.Union([__sinclair_typebox.Typ
|
|
|
6611
6814
|
messageType: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6612
6815
|
from: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
6613
6816
|
})]);
|
|
6817
|
+
const subscriptionLifetimeSchema = __sinclair_typebox.Type.Union([
|
|
6818
|
+
__sinclair_typebox.Type.Object({ kind: __sinclair_typebox.Type.Literal(`until_entity_stopped`) }),
|
|
6819
|
+
__sinclair_typebox.Type.Object({
|
|
6820
|
+
kind: __sinclair_typebox.Type.Literal(`expires_at`),
|
|
6821
|
+
at: __sinclair_typebox.Type.String()
|
|
6822
|
+
}),
|
|
6823
|
+
__sinclair_typebox.Type.Object({ kind: __sinclair_typebox.Type.Literal(`manual`) })
|
|
6824
|
+
]);
|
|
6825
|
+
const eventSourceSubscriptionBodySchema = __sinclair_typebox.Type.Object({
|
|
6826
|
+
sourceKey: __sinclair_typebox.Type.String(),
|
|
6827
|
+
bucketKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6828
|
+
params: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
|
|
6829
|
+
filterKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6830
|
+
lifetime: __sinclair_typebox.Type.Optional(subscriptionLifetimeSchema),
|
|
6831
|
+
reason: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
6832
|
+
});
|
|
6614
6833
|
const entitiesRegisterBodySchema = __sinclair_typebox.Type.Object({ tags: __sinclair_typebox.Type.Optional(stringRecordSchema) });
|
|
6615
6834
|
const entitiesRouter = (0, itty_router.Router)({ base: `/_electric/entities` });
|
|
6616
6835
|
entitiesRouter.get(`/`, listEntities);
|
|
@@ -6619,6 +6838,7 @@ entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spa
|
|
|
6619
6838
|
entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
|
|
6620
6839
|
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
|
|
6621
6840
|
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
|
|
6841
|
+
entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), signalEntity);
|
|
6622
6842
|
entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
|
|
6623
6843
|
entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
|
|
6624
6844
|
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
|
|
@@ -6627,6 +6847,8 @@ entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withS
|
|
|
6627
6847
|
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, removeTag);
|
|
6628
6848
|
entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), upsertSchedule);
|
|
6629
6849
|
entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, deleteSchedule);
|
|
6850
|
+
entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), upsertEventSourceSubscription);
|
|
6851
|
+
entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, deleteEventSourceSubscription);
|
|
6630
6852
|
function entityUrlFromSegments(type, instanceId) {
|
|
6631
6853
|
if (!type || !instanceId) return null;
|
|
6632
6854
|
if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
|
|
@@ -6733,6 +6955,47 @@ async function deleteSchedule(request, ctx) {
|
|
|
6733
6955
|
const result = await ctx.entityManager.deleteSchedule(entityUrl, { id: decodeURIComponent(request.params.scheduleId) });
|
|
6734
6956
|
return (0, itty_router.json)(result);
|
|
6735
6957
|
}
|
|
6958
|
+
async function upsertEventSourceSubscription(request, ctx) {
|
|
6959
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `subscribed to event sources`);
|
|
6960
|
+
if (principalMutationError) return principalMutationError;
|
|
6961
|
+
const catalog = ctx.eventSources;
|
|
6962
|
+
if (!catalog) return apiError(404, ErrCodeNotFound, `No event source catalog is configured`);
|
|
6963
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6964
|
+
const parsed = routeBody(request);
|
|
6965
|
+
const source = await catalog.getEventSource(parsed.sourceKey);
|
|
6966
|
+
if (!source) return apiError(404, ErrCodeNotFound, `Event source "${parsed.sourceKey}" not found`);
|
|
6967
|
+
if (parsed.lifetime?.kind === `expires_at`) {
|
|
6968
|
+
const expiresAt = new Date(parsed.lifetime.at);
|
|
6969
|
+
if (Number.isNaN(expiresAt.getTime())) return apiError(400, ErrCodeInvalidRequest, `Invalid expires_at lifetime timestamp`);
|
|
6970
|
+
}
|
|
6971
|
+
let resolved;
|
|
6972
|
+
try {
|
|
6973
|
+
resolved = (0, __electric_ax_agents_runtime.resolveEventSourceSubscription)({
|
|
6974
|
+
contract: source,
|
|
6975
|
+
entityUrl,
|
|
6976
|
+
request: {
|
|
6977
|
+
...parsed,
|
|
6978
|
+
id: decodeURIComponent(request.params.subscriptionId)
|
|
6979
|
+
},
|
|
6980
|
+
createdBy: `tool`
|
|
6981
|
+
});
|
|
6982
|
+
} catch (error) {
|
|
6983
|
+
return apiError(400, ErrCodeInvalidRequest, error instanceof Error ? error.message : String(error));
|
|
6984
|
+
}
|
|
6985
|
+
await ctx.ensureEventSourceWakeSource?.(resolved.subscription.sourceUrl);
|
|
6986
|
+
const result = await ctx.entityManager.upsertEventSourceSubscription(entityUrl, {
|
|
6987
|
+
subscription: resolved.subscription,
|
|
6988
|
+
manifest: (0, __electric_ax_agents_runtime.buildEventSourceManifestEntry)(resolved)
|
|
6989
|
+
});
|
|
6990
|
+
return (0, itty_router.json)(result);
|
|
6991
|
+
}
|
|
6992
|
+
async function deleteEventSourceSubscription(request, ctx) {
|
|
6993
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `unsubscribed from event sources`);
|
|
6994
|
+
if (principalMutationError) return principalMutationError;
|
|
6995
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6996
|
+
const result = await ctx.entityManager.deleteEventSourceSubscription(entityUrl, { id: decodeURIComponent(request.params.subscriptionId) });
|
|
6997
|
+
return (0, itty_router.json)(result);
|
|
6998
|
+
}
|
|
6736
6999
|
async function setTag(request, ctx) {
|
|
6737
7000
|
const principalMutationError = rejectPrincipalEntityMutation(request, `tagged`);
|
|
6738
7001
|
if (principalMutationError) return principalMutationError;
|
|
@@ -6822,11 +7085,13 @@ async function spawnEntity(request, ctx) {
|
|
|
6822
7085
|
wake: parsed.wake,
|
|
6823
7086
|
created_by: principal.url
|
|
6824
7087
|
});
|
|
6825
|
-
|
|
7088
|
+
const linkBeforeInitialMessage = parsed.initialMessage !== void 0 && shouldLinkDispatchBeforeInitialMessage(dispatchPolicy);
|
|
7089
|
+
if (linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
|
|
6826
7090
|
if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
|
|
6827
7091
|
from: principal.url,
|
|
6828
7092
|
payload: parsed.initialMessage
|
|
6829
7093
|
});
|
|
7094
|
+
if (!linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
|
|
6830
7095
|
return (0, itty_router.json)({
|
|
6831
7096
|
...toPublicEntity(entity),
|
|
6832
7097
|
txid: entity.txid
|
|
@@ -6850,6 +7115,22 @@ async function killEntity(request, ctx) {
|
|
|
6850
7115
|
ctx.runtime.claimWriteTokens.clearStream(ctx.service, entity.streams.main);
|
|
6851
7116
|
return (0, itty_router.json)(result);
|
|
6852
7117
|
}
|
|
7118
|
+
async function signalEntity(request, ctx) {
|
|
7119
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `signaled`);
|
|
7120
|
+
if (principalMutationError) return principalMutationError;
|
|
7121
|
+
const parsed = routeBody(request);
|
|
7122
|
+
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
7123
|
+
const result = await ctx.entityManager.signal(entityUrl, {
|
|
7124
|
+
signal: parsed.signal,
|
|
7125
|
+
reason: parsed.reason,
|
|
7126
|
+
payload: parsed.payload
|
|
7127
|
+
});
|
|
7128
|
+
if (result.new_state === `stopped` || result.new_state === `killed`) {
|
|
7129
|
+
await unlinkEntityDispatchSubscription(ctx, entity);
|
|
7130
|
+
ctx.runtime.claimWriteTokens.clearStream(ctx.service, entity.streams.main);
|
|
7131
|
+
}
|
|
7132
|
+
return (0, itty_router.json)(result);
|
|
7133
|
+
}
|
|
6853
7134
|
|
|
6854
7135
|
//#endregion
|
|
6855
7136
|
//#region src/routing/entity-types-router.ts
|
|
@@ -7330,7 +7611,7 @@ async function notificationFromClaim(ctx, input) {
|
|
|
7330
7611
|
const primaryStream = withLeadingSlash(primary.path);
|
|
7331
7612
|
const entity = await ctx.entityManager.registry.getEntityByStream(primaryStream);
|
|
7332
7613
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Claim stream is not attached to an entity`, 404);
|
|
7333
|
-
if (entity.status === `stopped`) {
|
|
7614
|
+
if (entity.status === `stopped` || entity.status === `paused`) {
|
|
7334
7615
|
await ctx.streamClient.releaseSubscription(input.subscriptionId, input.claim.token, {
|
|
7335
7616
|
wake_id: input.claim.wake_id,
|
|
7336
7617
|
generation: input.claim.generation
|
|
@@ -7429,6 +7710,7 @@ const callbackForwardBodySchema = __sinclair_typebox.Type.Object({
|
|
|
7429
7710
|
const DS_SUBSCRIPTION_CALLBACK_PREFIX = `ds-subscription:`;
|
|
7430
7711
|
const internalRouter = (0, itty_router.Router)({ base: `/_electric` });
|
|
7431
7712
|
internalRouter.get(`/health`, () => (0, itty_router.json)({ status: `ok` }));
|
|
7713
|
+
internalRouter.get(`/event-sources`, listEventSources);
|
|
7432
7714
|
internalRouter.post(`/wake`, withSchema(wakeRegistrationBodySchema), registerWake);
|
|
7433
7715
|
internalRouter.post(`/webhook-forward/:subscriptionId`, webhookForward);
|
|
7434
7716
|
internalRouter.post(`/callback-forward/:consumerId`, callbackForward);
|
|
@@ -7532,6 +7814,13 @@ async function registerWake(request, ctx) {
|
|
|
7532
7814
|
await ctx.entityManager.registerWake(opts);
|
|
7533
7815
|
return (0, itty_router.status)(204);
|
|
7534
7816
|
}
|
|
7817
|
+
async function listEventSources(_request, ctx) {
|
|
7818
|
+
const eventSources = ctx.eventSources ? await ctx.eventSources.listEventSources() : [];
|
|
7819
|
+
return (0, itty_router.json)({ eventSources: eventSources.filter(isAgentVisibleEventSource) });
|
|
7820
|
+
}
|
|
7821
|
+
function isAgentVisibleEventSource(source) {
|
|
7822
|
+
return source.agentVisible === true && source.status === `active`;
|
|
7823
|
+
}
|
|
7535
7824
|
async function webhookForward(request, ctx) {
|
|
7536
7825
|
const subscriptionId = routeParam(request, `subscriptionId`);
|
|
7537
7826
|
const rootSpan = getRequestSpan(request);
|
|
@@ -7598,7 +7887,7 @@ async function webhookForward(request, ctx) {
|
|
|
7598
7887
|
serverLog.warn(`[webhook-forward] consumerCallbacks upsert failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
7599
7888
|
}) : void 0;
|
|
7600
7889
|
const [entity, enriched] = await Promise.all([entityPromise, enrichPromise]);
|
|
7601
|
-
if (entity?.status === `stopped`) {
|
|
7890
|
+
if (entity?.status === `stopped` || entity?.status === `paused`) {
|
|
7602
7891
|
if (upsertPromise) await upsertPromise;
|
|
7603
7892
|
return (0, itty_router.json)({ done: true });
|
|
7604
7893
|
}
|
|
@@ -7741,9 +8030,9 @@ async function callbackForward(request, ctx) {
|
|
|
7741
8030
|
entityCleared = result?.entityCleared ?? false;
|
|
7742
8031
|
}
|
|
7743
8032
|
if (entity && (entityCleared || stillOwnsClaim)) {
|
|
7744
|
-
await ctx.entityManager.registry.updateStatus(entity.url, `idle`);
|
|
8033
|
+
await ctx.entityManager.registry.updateStatus(entity.url, entity.status === `stopping` ? `stopped` : `idle`);
|
|
7745
8034
|
await ctx.entityBridgeManager.onEntityChanged(entity.url);
|
|
7746
|
-
serverLog.info(`[callback-forward] status updated
|
|
8035
|
+
serverLog.info(`[callback-forward] status updated after done for ${entity.url}`);
|
|
7747
8036
|
} else if (!entity) serverLog.warn(`[callback-forward] done received but no entity found for stream=${target.primaryStream}`);
|
|
7748
8037
|
if (stillOwnsClaim) ctx.runtime.claimWriteTokens.clearStream(ctx.service, target.primaryStream);
|
|
7749
8038
|
else if (entity) serverLog.info(`[callback-forward] done arrived after in-memory token evicted (stream=${target.primaryStream} consumer=${consumerId})`);
|
|
@@ -7803,13 +8092,19 @@ exports.AgentsHost = AgentsHost
|
|
|
7803
8092
|
exports.DEFAULT_TENANT_ID = DEFAULT_TENANT_ID
|
|
7804
8093
|
exports.StreamClient = StreamClient
|
|
7805
8094
|
exports.UnregisteredTenantError = UnregisteredTenantError
|
|
8095
|
+
exports.assertEntitySignal = assertEntitySignal
|
|
8096
|
+
exports.assertEntityStatus = assertEntityStatus
|
|
7806
8097
|
exports.createDb = createDb
|
|
7807
8098
|
exports.createEd25519WebhookSigner = createEd25519WebhookSigner
|
|
8099
|
+
exports.expectedSignalStatus = expectedSignalStatus
|
|
7808
8100
|
exports.getDefaultWebhookSigner = getDefaultWebhookSigner
|
|
7809
8101
|
exports.globalRouter = globalRouter
|
|
8102
|
+
exports.isTerminalEntityStatus = isTerminalEntityStatus
|
|
7810
8103
|
exports.isUnregisteredTenantError = isUnregisteredTenantError
|
|
7811
8104
|
exports.pathPrefixedSingleTenantDurableStreamsRoutingAdapter = pathPrefixedSingleTenantDurableStreamsRoutingAdapter
|
|
8105
|
+
exports.rejectsNormalWrites = rejectsNormalWrites
|
|
7812
8106
|
exports.runMigrations = runMigrations
|
|
7813
8107
|
exports.streamRootDurableStreamsRoutingAdapter = streamRootDurableStreamsRoutingAdapter
|
|
7814
8108
|
exports.tenantRootDurableStreamsRoutingAdapter = tenantRootDurableStreamsRoutingAdapter
|
|
8109
|
+
exports.toPublicEntity = toPublicEntity
|
|
7815
8110
|
exports.webhookSigningMetadata = webhookSigningMetadata
|