@electric-ax/agents-server 0.3.0
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/LICENSE +177 -0
- package/dist/chunk-Cl8Af3a2.js +11 -0
- package/dist/entrypoint.js +7319 -0
- package/dist/index.cjs +7090 -0
- package/dist/index.d.cts +4262 -0
- package/dist/index.d.ts +4263 -0
- package/dist/index.js +7053 -0
- package/drizzle/0000_baseline.sql +97 -0
- package/drizzle/0001_entity_tags_and_bridges.sql +45 -0
- package/drizzle/0002_tag_outbox_hardening.sql +14 -0
- package/drizzle/0003_entity_manifest_sources.sql +11 -0
- package/drizzle/0004_tenant_scoping.sql +139 -0
- package/drizzle/0005_pull_wake_control_plane.sql +156 -0
- package/drizzle/meta/0000_snapshot.json +593 -0
- package/drizzle/meta/_journal.json +48 -0
- package/package.json +89 -0
- package/src/authenticated-user-format.ts +17 -0
- package/src/claim-write-token-store.ts +74 -0
- package/src/db/index.ts +53 -0
- package/src/db/schema.ts +490 -0
- package/src/dev-asserted-auth.ts +46 -0
- package/src/dispatch-policy-schema.ts +52 -0
- package/src/electric-agents/adapter-types.ts +70 -0
- package/src/electric-agents/default-entity-schemas.ts +1 -0
- package/src/electric-agents/schema-validator.ts +143 -0
- package/src/electric-agents-http.ts +46 -0
- package/src/electric-agents-types.ts +335 -0
- package/src/entity-bridge-manager.ts +694 -0
- package/src/entity-manager.ts +2601 -0
- package/src/entity-projector.ts +765 -0
- package/src/entity-registry.ts +1162 -0
- package/src/entrypoint-lib.ts +295 -0
- package/src/entrypoint.ts +11 -0
- package/src/host.ts +323 -0
- package/src/index.ts +49 -0
- package/src/manifest-side-effects.ts +183 -0
- package/src/routing/agent-ui-router.ts +81 -0
- package/src/routing/context.ts +35 -0
- package/src/routing/cron-router.ts +45 -0
- package/src/routing/dispatch-policy.ts +248 -0
- package/src/routing/durable-streams-router.ts +407 -0
- package/src/routing/durable-streams-routing-adapter.ts +96 -0
- package/src/routing/electric-proxy-router.ts +61 -0
- package/src/routing/entities-router.ts +484 -0
- package/src/routing/entity-types-router.ts +229 -0
- package/src/routing/global-router.ts +33 -0
- package/src/routing/hooks.ts +123 -0
- package/src/routing/internal-router.ts +741 -0
- package/src/routing/oss-server-router.ts +56 -0
- package/src/routing/runners-router.ts +416 -0
- package/src/routing/schema.ts +141 -0
- package/src/routing/stream-append.ts +196 -0
- package/src/routing/tenant-stream-paths.ts +26 -0
- package/src/runtime-registry.ts +49 -0
- package/src/runtime.ts +537 -0
- package/src/scheduler.ts +788 -0
- package/src/schema-validation.ts +15 -0
- package/src/server.ts +374 -0
- package/src/standalone-runtime.ts +188 -0
- package/src/stream-client.ts +842 -0
- package/src/tag-stream-outbox-drainer.ts +188 -0
- package/src/tenant.ts +25 -0
- package/src/tracing.ts +57 -0
- package/src/utils/electric-url.ts +15 -0
- package/src/utils/log.ts +95 -0
- package/src/utils/server-utils.ts +245 -0
- package/src/utils/webhook-url.ts +33 -0
- package/src/wake-registry.ts +946 -0
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import { parseCronStreamPath } from '@electric-ax/agents-runtime'
|
|
2
|
+
import { and, eq } from 'drizzle-orm'
|
|
3
|
+
import { consumerCallbacks, wakeRegistrations } from './db/schema.js'
|
|
4
|
+
import { ClaimWriteTokenStore } from './claim-write-token-store.js'
|
|
5
|
+
import { PostgresRegistry } from './entity-registry.js'
|
|
6
|
+
import { EntityManager } from './entity-manager.js'
|
|
7
|
+
import {
|
|
8
|
+
buildManifestWakeRegistration,
|
|
9
|
+
extractManifestCronSpec,
|
|
10
|
+
} from './manifest-side-effects.js'
|
|
11
|
+
import { SchemaValidator } from './electric-agents/schema-validator.js'
|
|
12
|
+
import { serverLog } from './utils/log.js'
|
|
13
|
+
import { isPermanentElectricAgentsError } from './scheduler.js'
|
|
14
|
+
import { StreamClient, durableStreamsServiceUrl } from './stream-client.js'
|
|
15
|
+
import { DEFAULT_TENANT_ID } from './tenant.js'
|
|
16
|
+
import type { DrizzleDB } from './db/index.js'
|
|
17
|
+
import type { EntityBridgeCoordinator } from './entity-bridge-manager.js'
|
|
18
|
+
import type { DurableStreamsBearerProvider } from './stream-client.js'
|
|
19
|
+
import type {
|
|
20
|
+
CronTickPayload,
|
|
21
|
+
DelayedSendPayload,
|
|
22
|
+
SchedulerClient,
|
|
23
|
+
} from './scheduler.js'
|
|
24
|
+
import type { WakeRegistry } from './wake-registry.js'
|
|
25
|
+
|
|
26
|
+
function omitUndefined<T extends Record<string, unknown>>(value: T): T {
|
|
27
|
+
return Object.fromEntries(
|
|
28
|
+
Object.entries(value).filter(([, entry]) => entry !== undefined)
|
|
29
|
+
) as T
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ElectricAgentsTenantRuntimeOptions {
|
|
33
|
+
service?: string
|
|
34
|
+
tenantId?: string
|
|
35
|
+
db: DrizzleDB
|
|
36
|
+
registry?: PostgresRegistry
|
|
37
|
+
durableStreamsUrl?: string
|
|
38
|
+
durableStreamsBearer?: DurableStreamsBearerProvider
|
|
39
|
+
streamClient?: StreamClient
|
|
40
|
+
wakeRegistry: WakeRegistry
|
|
41
|
+
scheduler: SchedulerClient
|
|
42
|
+
entityBridgeManager: EntityBridgeCoordinator
|
|
43
|
+
claimWriteTokens?: ClaimWriteTokenStore
|
|
44
|
+
stopWakeRegistryOnShutdown?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class ElectricAgentsTenantRuntime {
|
|
48
|
+
readonly serviceId: string
|
|
49
|
+
readonly service: string
|
|
50
|
+
readonly db: DrizzleDB
|
|
51
|
+
readonly streamClient: StreamClient
|
|
52
|
+
readonly registry: PostgresRegistry
|
|
53
|
+
readonly wakeRegistry: WakeRegistry
|
|
54
|
+
readonly scheduler: SchedulerClient
|
|
55
|
+
readonly entityBridgeManager: EntityBridgeCoordinator
|
|
56
|
+
readonly claimWriteTokens: ClaimWriteTokenStore
|
|
57
|
+
readonly manager: EntityManager
|
|
58
|
+
|
|
59
|
+
constructor(options: ElectricAgentsTenantRuntimeOptions) {
|
|
60
|
+
this.serviceId = options.service ?? options.tenantId ?? DEFAULT_TENANT_ID
|
|
61
|
+
this.service = this.serviceId
|
|
62
|
+
this.db = options.db
|
|
63
|
+
if (options.streamClient) {
|
|
64
|
+
this.streamClient = options.streamClient
|
|
65
|
+
} else if (options.durableStreamsUrl) {
|
|
66
|
+
this.streamClient = new StreamClient(
|
|
67
|
+
durableStreamsServiceUrl(options.durableStreamsUrl, this.serviceId),
|
|
68
|
+
{ bearer: options.durableStreamsBearer }
|
|
69
|
+
)
|
|
70
|
+
} else {
|
|
71
|
+
throw new Error(`Either durableStreamsUrl or streamClient is required`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.registry =
|
|
75
|
+
options.registry ?? new PostgresRegistry(this.db, this.serviceId)
|
|
76
|
+
this.wakeRegistry = options.wakeRegistry
|
|
77
|
+
this.scheduler = options.scheduler
|
|
78
|
+
this.entityBridgeManager = options.entityBridgeManager
|
|
79
|
+
this.claimWriteTokens =
|
|
80
|
+
options.claimWriteTokens ?? new ClaimWriteTokenStore()
|
|
81
|
+
this.manager = new EntityManager({
|
|
82
|
+
registry: this.registry,
|
|
83
|
+
streamClient: this.streamClient,
|
|
84
|
+
validator: new SchemaValidator(),
|
|
85
|
+
wakeRegistry: this.wakeRegistry,
|
|
86
|
+
scheduler: this.scheduler,
|
|
87
|
+
entityBridgeManager: this.entityBridgeManager,
|
|
88
|
+
writeTokenValidator: (entity, token) =>
|
|
89
|
+
this.claimWriteTokens.isValid(
|
|
90
|
+
this.serviceId,
|
|
91
|
+
entity.streams.main,
|
|
92
|
+
token
|
|
93
|
+
),
|
|
94
|
+
stopWakeRegistryOnShutdown: options.stopWakeRegistryOnShutdown ?? false,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async stop(): Promise<void> {
|
|
99
|
+
await this.manager.shutdown()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async rehydrateCronSchedules(): Promise<void> {
|
|
103
|
+
const rows = await this.db
|
|
104
|
+
.select({ sourceUrl: wakeRegistrations.sourceUrl })
|
|
105
|
+
.from(wakeRegistrations)
|
|
106
|
+
.where(eq(wakeRegistrations.tenantId, this.serviceId))
|
|
107
|
+
const cronSpecs = new Map<
|
|
108
|
+
string,
|
|
109
|
+
{ expression: string; timezone: string }
|
|
110
|
+
>()
|
|
111
|
+
|
|
112
|
+
for (const row of rows) {
|
|
113
|
+
if (!row.sourceUrl.startsWith(`/_cron/`)) continue
|
|
114
|
+
try {
|
|
115
|
+
const spec = parseCronStreamPath(row.sourceUrl, { fallback: `utc` })
|
|
116
|
+
cronSpecs.set(JSON.stringify(spec), spec)
|
|
117
|
+
} catch (err) {
|
|
118
|
+
serverLog.warn(`[agent-server] invalid cron wake registration:`, err)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const spec of cronSpecs.values()) {
|
|
123
|
+
try {
|
|
124
|
+
await this.manager.getOrCreateCronStream(spec.expression, spec.timezone)
|
|
125
|
+
} catch (err) {
|
|
126
|
+
serverLog.warn(`[agent-server] cron rehydration failed:`, err)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const { entities } = await this.manager.registry.listEntities({
|
|
131
|
+
limit: 10_000,
|
|
132
|
+
})
|
|
133
|
+
await this.manager.registry.clearEntityManifestSources()
|
|
134
|
+
|
|
135
|
+
for (const entity of entities) {
|
|
136
|
+
try {
|
|
137
|
+
const events = await this.streamClient.readJson<
|
|
138
|
+
Record<string, unknown>
|
|
139
|
+
>(entity.streams.main)
|
|
140
|
+
const manifestEvents = new Map<string, Record<string, unknown>>()
|
|
141
|
+
|
|
142
|
+
for (const event of events) {
|
|
143
|
+
if (event.type !== `manifest` || typeof event.key !== `string`) {
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
manifestEvents.set(event.key, event)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const [manifestKey, event] of manifestEvents) {
|
|
150
|
+
const headers = event.headers as Record<string, unknown> | undefined
|
|
151
|
+
const operation = headers?.operation as string | undefined
|
|
152
|
+
const value = event.value as Record<string, unknown> | undefined
|
|
153
|
+
await this.applyManifestEntitySource(
|
|
154
|
+
entity.url,
|
|
155
|
+
manifestKey,
|
|
156
|
+
operation,
|
|
157
|
+
value
|
|
158
|
+
)
|
|
159
|
+
await this.applyManifestFutureSendSchedule(
|
|
160
|
+
entity.url,
|
|
161
|
+
manifestKey,
|
|
162
|
+
operation,
|
|
163
|
+
value
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
serverLog.warn(
|
|
168
|
+
`[agent-server] manifest future_send rehydration failed for ${entity.url}:`,
|
|
169
|
+
err
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async evaluateWakePayload(
|
|
176
|
+
sourceUrl: string,
|
|
177
|
+
event: Record<string, unknown> | Array<Record<string, unknown>>
|
|
178
|
+
): Promise<void> {
|
|
179
|
+
if (Array.isArray(event)) {
|
|
180
|
+
await Promise.all(
|
|
181
|
+
event.map((item) => this.manager.evaluateWakes(sourceUrl, item))
|
|
182
|
+
)
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
await this.manager.evaluateWakes(sourceUrl, event)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
checkRunFinished(
|
|
190
|
+
sourceUrl: string,
|
|
191
|
+
event: Record<string, unknown> | Array<Record<string, unknown>>
|
|
192
|
+
): void {
|
|
193
|
+
const events = Array.isArray(event) ? event : [event]
|
|
194
|
+
for (const item of events) {
|
|
195
|
+
if (item.type !== `run`) continue
|
|
196
|
+
const value = item.value as Record<string, unknown> | undefined
|
|
197
|
+
const headers = item.headers as Record<string, unknown> | undefined
|
|
198
|
+
const status = value?.status as string | undefined
|
|
199
|
+
const operation = headers?.operation as string | undefined
|
|
200
|
+
if (
|
|
201
|
+
operation === `update` &&
|
|
202
|
+
(status === `completed` || status === `failed`)
|
|
203
|
+
) {
|
|
204
|
+
void this.maybeMarkEntityIdleAfterRunFinished(sourceUrl)
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async syncManifestWakes(
|
|
211
|
+
subscriberUrl: string,
|
|
212
|
+
event: Record<string, unknown> | Array<Record<string, unknown>>
|
|
213
|
+
): Promise<void> {
|
|
214
|
+
const events = Array.isArray(event) ? event : [event]
|
|
215
|
+
for (const item of events) {
|
|
216
|
+
const eventType = item.type as string | undefined
|
|
217
|
+
if (eventType !== `manifest`) continue
|
|
218
|
+
|
|
219
|
+
const headers = item.headers as Record<string, unknown> | undefined
|
|
220
|
+
const operation = headers?.operation as string | undefined
|
|
221
|
+
const manifestKey = item.key as string | undefined
|
|
222
|
+
const value = item.value as Record<string, unknown> | undefined
|
|
223
|
+
|
|
224
|
+
if (!manifestKey) continue
|
|
225
|
+
|
|
226
|
+
if (operation === `delete`) {
|
|
227
|
+
await this.manager.wakeRegistry.unregisterByManifestKey(
|
|
228
|
+
subscriberUrl,
|
|
229
|
+
manifestKey,
|
|
230
|
+
this.serviceId
|
|
231
|
+
)
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
await this.manager.wakeRegistry.unregisterByManifestKey(
|
|
236
|
+
subscriberUrl,
|
|
237
|
+
manifestKey,
|
|
238
|
+
this.serviceId
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if (value) {
|
|
242
|
+
const reg = buildManifestWakeRegistration(
|
|
243
|
+
subscriberUrl,
|
|
244
|
+
value,
|
|
245
|
+
manifestKey
|
|
246
|
+
)
|
|
247
|
+
if (reg) {
|
|
248
|
+
reg.tenantId = this.serviceId
|
|
249
|
+
await this.manager.wakeRegistry.register(reg)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const cronSpec = extractManifestCronSpec(value)
|
|
253
|
+
if (cronSpec) {
|
|
254
|
+
void this.manager
|
|
255
|
+
.getOrCreateCronStream(cronSpec.expression, cronSpec.timezone)
|
|
256
|
+
.catch((err) =>
|
|
257
|
+
serverLog.warn(`[agent-server] cron schedule failed:`, err)
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async syncManifestEntitySources(
|
|
265
|
+
ownerEntityUrl: string,
|
|
266
|
+
event: Record<string, unknown> | Array<Record<string, unknown>>
|
|
267
|
+
): Promise<void> {
|
|
268
|
+
const events = Array.isArray(event) ? event : [event]
|
|
269
|
+
for (const item of events) {
|
|
270
|
+
if (item.type !== `manifest`) continue
|
|
271
|
+
|
|
272
|
+
const manifestKey = item.key as string | undefined
|
|
273
|
+
const headers = item.headers as Record<string, unknown> | undefined
|
|
274
|
+
const operation = headers?.operation as string | undefined
|
|
275
|
+
const value = item.value as Record<string, unknown> | undefined
|
|
276
|
+
|
|
277
|
+
if (!manifestKey) continue
|
|
278
|
+
await this.applyManifestEntitySource(
|
|
279
|
+
ownerEntityUrl,
|
|
280
|
+
manifestKey,
|
|
281
|
+
operation,
|
|
282
|
+
value
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async syncManifestSchedules(
|
|
288
|
+
ownerEntityUrl: string,
|
|
289
|
+
event: Record<string, unknown> | Array<Record<string, unknown>>
|
|
290
|
+
): Promise<void> {
|
|
291
|
+
const events = Array.isArray(event) ? event : [event]
|
|
292
|
+
for (const item of events) {
|
|
293
|
+
if (item.type !== `manifest`) continue
|
|
294
|
+
|
|
295
|
+
const manifestKey = item.key as string | undefined
|
|
296
|
+
const headers = item.headers as Record<string, unknown> | undefined
|
|
297
|
+
const operation = headers?.operation as string | undefined
|
|
298
|
+
const value = item.value as Record<string, unknown> | undefined
|
|
299
|
+
|
|
300
|
+
if (!manifestKey) continue
|
|
301
|
+
await this.applyManifestFutureSendSchedule(
|
|
302
|
+
ownerEntityUrl,
|
|
303
|
+
manifestKey,
|
|
304
|
+
operation,
|
|
305
|
+
value
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async executeDelayedSend(
|
|
311
|
+
payload: DelayedSendPayload,
|
|
312
|
+
taskId: number
|
|
313
|
+
): Promise<void> {
|
|
314
|
+
const producerId = payload.producerId ?? `scheduler-task-${taskId}`
|
|
315
|
+
try {
|
|
316
|
+
await this.manager.send(
|
|
317
|
+
payload.entityUrl,
|
|
318
|
+
{
|
|
319
|
+
from: payload.from,
|
|
320
|
+
payload: payload.payload,
|
|
321
|
+
key: payload.key ?? `scheduled-task-${taskId}`,
|
|
322
|
+
type: payload.type,
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
producerId,
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
if (payload.manifest) {
|
|
330
|
+
await this.manager.writeManifestEntry(
|
|
331
|
+
payload.manifest.ownerEntityUrl,
|
|
332
|
+
payload.manifest.key,
|
|
333
|
+
`update`,
|
|
334
|
+
omitUndefined({
|
|
335
|
+
...payload.manifest.entry,
|
|
336
|
+
status: `sent`,
|
|
337
|
+
sentAt: new Date().toISOString(),
|
|
338
|
+
failedAt: undefined,
|
|
339
|
+
lastError: undefined,
|
|
340
|
+
}),
|
|
341
|
+
{
|
|
342
|
+
producerId: `manifest-status-${producerId}-sent`,
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
} catch (err) {
|
|
347
|
+
if (payload.manifest && isPermanentElectricAgentsError(err)) {
|
|
348
|
+
await this.manager.writeManifestEntry(
|
|
349
|
+
payload.manifest.ownerEntityUrl,
|
|
350
|
+
payload.manifest.key,
|
|
351
|
+
`update`,
|
|
352
|
+
omitUndefined({
|
|
353
|
+
...payload.manifest.entry,
|
|
354
|
+
status: `failed`,
|
|
355
|
+
failedAt: new Date().toISOString(),
|
|
356
|
+
sentAt: undefined,
|
|
357
|
+
lastError: err instanceof Error ? err.message : String(err),
|
|
358
|
+
}),
|
|
359
|
+
{
|
|
360
|
+
producerId: `manifest-status-${producerId}-failed`,
|
|
361
|
+
}
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
throw err
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async executeCronTick(
|
|
369
|
+
payload: CronTickPayload,
|
|
370
|
+
tickNumber: number
|
|
371
|
+
): Promise<void> {
|
|
372
|
+
const streamPath = payload.streamPath
|
|
373
|
+
const encodedExpression = streamPath.split(`/`).at(-1)
|
|
374
|
+
const spec = parseCronStreamPath(streamPath, {
|
|
375
|
+
fallback: `utc`,
|
|
376
|
+
})
|
|
377
|
+
const tickEvent = {
|
|
378
|
+
type: `cron_tick`,
|
|
379
|
+
key: `tick-${tickNumber}`,
|
|
380
|
+
value: {
|
|
381
|
+
expression: spec.expression,
|
|
382
|
+
timezone: spec.timezone,
|
|
383
|
+
firedAt: new Date().toISOString(),
|
|
384
|
+
tickNumber,
|
|
385
|
+
},
|
|
386
|
+
headers: {
|
|
387
|
+
operation: `insert`,
|
|
388
|
+
timestamp: new Date().toISOString(),
|
|
389
|
+
},
|
|
390
|
+
}
|
|
391
|
+
await this.streamClient.appendIdempotent(
|
|
392
|
+
streamPath,
|
|
393
|
+
new TextEncoder().encode(JSON.stringify(tickEvent)),
|
|
394
|
+
{
|
|
395
|
+
producerId: `scheduler-cron-${encodedExpression}-${tickNumber}`,
|
|
396
|
+
}
|
|
397
|
+
)
|
|
398
|
+
await this.manager.evaluateWakes(streamPath, tickEvent)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private async applyManifestFutureSendSchedule(
|
|
402
|
+
ownerEntityUrl: string,
|
|
403
|
+
manifestKey: string,
|
|
404
|
+
operation: string | undefined,
|
|
405
|
+
value: Record<string, unknown> | undefined
|
|
406
|
+
): Promise<void> {
|
|
407
|
+
if (operation === `delete`) {
|
|
408
|
+
await this.scheduler.cancelManifestDelayedSend(
|
|
409
|
+
ownerEntityUrl,
|
|
410
|
+
manifestKey
|
|
411
|
+
)
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (
|
|
416
|
+
!value ||
|
|
417
|
+
value.kind !== `schedule` ||
|
|
418
|
+
value.scheduleType !== `future_send`
|
|
419
|
+
) {
|
|
420
|
+
await this.scheduler.cancelManifestDelayedSend(
|
|
421
|
+
ownerEntityUrl,
|
|
422
|
+
manifestKey
|
|
423
|
+
)
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (value.status !== undefined && value.status !== `pending`) {
|
|
428
|
+
await this.scheduler.cancelManifestDelayedSend(
|
|
429
|
+
ownerEntityUrl,
|
|
430
|
+
manifestKey
|
|
431
|
+
)
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const fireAtRaw = value.fireAt
|
|
436
|
+
const producerId = value.producerId
|
|
437
|
+
const targetUrl = value.targetUrl
|
|
438
|
+
if (
|
|
439
|
+
typeof fireAtRaw !== `string` ||
|
|
440
|
+
typeof producerId !== `string` ||
|
|
441
|
+
typeof targetUrl !== `string`
|
|
442
|
+
) {
|
|
443
|
+
serverLog.warn(
|
|
444
|
+
`[agent-server] invalid future_send manifest entry for ${ownerEntityUrl}/${manifestKey}`
|
|
445
|
+
)
|
|
446
|
+
return
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const fireAt = new Date(fireAtRaw)
|
|
450
|
+
if (Number.isNaN(fireAt.getTime())) {
|
|
451
|
+
serverLog.warn(
|
|
452
|
+
`[agent-server] invalid future_send fireAt for ${ownerEntityUrl}/${manifestKey}: ${fireAtRaw}`
|
|
453
|
+
)
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
await this.scheduler.syncManifestDelayedSend(
|
|
458
|
+
ownerEntityUrl,
|
|
459
|
+
manifestKey,
|
|
460
|
+
{
|
|
461
|
+
entityUrl: targetUrl,
|
|
462
|
+
from: typeof value.from === `string` ? value.from : ownerEntityUrl,
|
|
463
|
+
payload: value.payload,
|
|
464
|
+
key: `scheduled-${producerId}`,
|
|
465
|
+
type:
|
|
466
|
+
typeof value.messageType === `string` ? value.messageType : undefined,
|
|
467
|
+
producerId,
|
|
468
|
+
manifest: {
|
|
469
|
+
ownerEntityUrl,
|
|
470
|
+
key: manifestKey,
|
|
471
|
+
entry: omitUndefined({
|
|
472
|
+
...value,
|
|
473
|
+
key: manifestKey,
|
|
474
|
+
kind: `schedule`,
|
|
475
|
+
scheduleType: `future_send`,
|
|
476
|
+
targetUrl,
|
|
477
|
+
fireAt: fireAt.toISOString(),
|
|
478
|
+
producerId,
|
|
479
|
+
status: `pending`,
|
|
480
|
+
}),
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
fireAt
|
|
484
|
+
)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private async applyManifestEntitySource(
|
|
488
|
+
ownerEntityUrl: string,
|
|
489
|
+
manifestKey: string,
|
|
490
|
+
operation: string | undefined,
|
|
491
|
+
value: Record<string, unknown> | undefined
|
|
492
|
+
): Promise<void> {
|
|
493
|
+
const sourceRef =
|
|
494
|
+
operation === `delete` ? undefined : this.extractEntitiesSourceRef(value)
|
|
495
|
+
await this.manager.registry.replaceEntityManifestSource(
|
|
496
|
+
ownerEntityUrl,
|
|
497
|
+
manifestKey,
|
|
498
|
+
sourceRef
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private extractEntitiesSourceRef(
|
|
503
|
+
manifest: Record<string, unknown> | undefined
|
|
504
|
+
): string | undefined {
|
|
505
|
+
if (
|
|
506
|
+
manifest?.kind === `source` &&
|
|
507
|
+
manifest.sourceType === `entities` &&
|
|
508
|
+
typeof manifest.sourceRef === `string`
|
|
509
|
+
) {
|
|
510
|
+
return manifest.sourceRef
|
|
511
|
+
}
|
|
512
|
+
return undefined
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private async maybeMarkEntityIdleAfterRunFinished(
|
|
516
|
+
entityUrl: string
|
|
517
|
+
): Promise<void> {
|
|
518
|
+
const primaryStream = `${entityUrl}/main`
|
|
519
|
+
const callbacks = await this.db
|
|
520
|
+
.select()
|
|
521
|
+
.from(consumerCallbacks)
|
|
522
|
+
.where(
|
|
523
|
+
and(
|
|
524
|
+
eq(consumerCallbacks.tenantId, this.serviceId),
|
|
525
|
+
eq(consumerCallbacks.primaryStream, primaryStream)
|
|
526
|
+
)
|
|
527
|
+
)
|
|
528
|
+
.limit(1)
|
|
529
|
+
|
|
530
|
+
if (callbacks.length > 0) {
|
|
531
|
+
return
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
await this.manager.registry.updateStatus(entityUrl, `idle`)
|
|
535
|
+
await this.entityBridgeManager.onEntityChanged(entityUrl)
|
|
536
|
+
}
|
|
537
|
+
}
|