@electric-ax/agents-server 0.4.9 → 0.4.12
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 +91 -80
- package/dist/index.cjs +91 -80
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +91 -80
- package/package.json +5 -5
- package/src/electric-agents/default-entity-schemas.ts +1 -1
- package/src/electric-agents-types.ts +1 -1
- package/src/entity-manager.ts +5 -5
- package/src/routing/dispatch-policy.ts +1 -1
- package/src/routing/durable-streams-router.ts +1 -1
- package/src/routing/durable-streams-routing-adapter.ts +1 -3
- package/src/routing/entities-router.ts +5 -26
- package/src/routing/entity-types-router.ts +23 -26
- package/src/routing/hooks.ts +1 -1
- package/src/routing/internal-router.ts +64 -37
- package/src/routing/observations-router.ts +74 -0
- package/src/routing/runners-router.ts +1 -1
- package/src/wake-registry.ts +1 -1
- package/src/routing/cron-router.ts +0 -45
|
@@ -193,10 +193,6 @@ const eventSourceSubscriptionBodySchema = Type.Object({
|
|
|
193
193
|
reason: Type.Optional(Type.String()),
|
|
194
194
|
})
|
|
195
195
|
|
|
196
|
-
const entitiesRegisterBodySchema = Type.Object({
|
|
197
|
-
tags: Type.Optional(stringRecordSchema),
|
|
198
|
-
})
|
|
199
|
-
|
|
200
196
|
type SpawnBody = Static<typeof spawnBodySchema>
|
|
201
197
|
type SendBody = Static<typeof sendBodySchema>
|
|
202
198
|
type InboxMessageBody = Static<typeof inboxMessageBodySchema>
|
|
@@ -207,7 +203,6 @@ type ScheduleBody = Static<typeof scheduleBodySchema>
|
|
|
207
203
|
type EventSourceSubscriptionBody = Static<
|
|
208
204
|
typeof eventSourceSubscriptionBodySchema
|
|
209
205
|
>
|
|
210
|
-
type EntitiesRegisterBody = Static<typeof entitiesRegisterBodySchema>
|
|
211
206
|
|
|
212
207
|
export const entitiesRouter: EntitiesRoutes = Router<
|
|
213
208
|
AgentsRouteRequest,
|
|
@@ -218,11 +213,6 @@ export const entitiesRouter: EntitiesRoutes = Router<
|
|
|
218
213
|
})
|
|
219
214
|
|
|
220
215
|
entitiesRouter.get(`/`, listEntities)
|
|
221
|
-
entitiesRouter.post(
|
|
222
|
-
`/register`,
|
|
223
|
-
withSchema(entitiesRegisterBodySchema),
|
|
224
|
-
registerEntitiesSource
|
|
225
|
-
)
|
|
226
216
|
entitiesRouter.put(
|
|
227
217
|
`/:type/:instanceId`,
|
|
228
218
|
withSpawnableEntityType,
|
|
@@ -270,7 +260,7 @@ entitiesRouter.post(
|
|
|
270
260
|
entitiesRouter.delete(
|
|
271
261
|
`/:type/:instanceId/tags/:tagKey`,
|
|
272
262
|
withExistingEntity,
|
|
273
|
-
|
|
263
|
+
deleteTag
|
|
274
264
|
)
|
|
275
265
|
entitiesRouter.put(
|
|
276
266
|
`/:type/:instanceId/schedules/:scheduleId`,
|
|
@@ -429,17 +419,6 @@ async function listEntities(
|
|
|
429
419
|
return json(entities.map((entity) => toPublicEntity(entity)))
|
|
430
420
|
}
|
|
431
421
|
|
|
432
|
-
async function registerEntitiesSource(
|
|
433
|
-
request: AgentsRouteRequest,
|
|
434
|
-
ctx: TenantContext
|
|
435
|
-
): Promise<Response> {
|
|
436
|
-
const parsed = routeBody<EntitiesRegisterBody>(request)
|
|
437
|
-
const result = await ctx.entityManager.registerEntitiesSource(
|
|
438
|
-
parsed.tags ?? {}
|
|
439
|
-
)
|
|
440
|
-
return json(result)
|
|
441
|
-
}
|
|
442
|
-
|
|
443
422
|
async function upsertSchedule(
|
|
444
423
|
request: AgentsRouteRequest,
|
|
445
424
|
ctx: TenantContext
|
|
@@ -603,7 +582,7 @@ async function setTag(
|
|
|
603
582
|
): Promise<Response> {
|
|
604
583
|
const principalMutationError = rejectPrincipalEntityMutation(
|
|
605
584
|
request,
|
|
606
|
-
`
|
|
585
|
+
`tag updated`
|
|
607
586
|
)
|
|
608
587
|
if (principalMutationError) return principalMutationError
|
|
609
588
|
|
|
@@ -619,19 +598,19 @@ async function setTag(
|
|
|
619
598
|
return json(toPublicEntity(updated))
|
|
620
599
|
}
|
|
621
600
|
|
|
622
|
-
async function
|
|
601
|
+
async function deleteTag(
|
|
623
602
|
request: AgentsRouteRequest,
|
|
624
603
|
ctx: TenantContext
|
|
625
604
|
): Promise<Response> {
|
|
626
605
|
const principalMutationError = rejectPrincipalEntityMutation(
|
|
627
606
|
request,
|
|
628
|
-
`
|
|
607
|
+
`tag deleted`
|
|
629
608
|
)
|
|
630
609
|
if (principalMutationError) return principalMutationError
|
|
631
610
|
|
|
632
611
|
const { entityUrl } = requireExistingEntityRoute(request)
|
|
633
612
|
const token = writeTokenFromRequest(request)
|
|
634
|
-
const updated = await ctx.entityManager.
|
|
613
|
+
const updated = await ctx.entityManager.deleteTag(
|
|
635
614
|
entityUrl,
|
|
636
615
|
decodeURIComponent(request.params.tagKey),
|
|
637
616
|
token
|
|
@@ -35,32 +35,32 @@ export type ElectricAgentsEntityTypeRoutes = RouterType<
|
|
|
35
35
|
>
|
|
36
36
|
|
|
37
37
|
type PublicEntityTypeResponse = ElectricAgentsEntityType & {
|
|
38
|
-
input_schemas?: Record<string, Record<string, unknown>>
|
|
39
|
-
output_schemas?: Record<string, Record<string, unknown>>
|
|
40
38
|
revision: number
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
const jsonObjectSchema = Type.Record(Type.String(), Type.Unknown())
|
|
44
42
|
const schemaMapSchema = Type.Record(Type.String(), jsonObjectSchema)
|
|
45
43
|
|
|
46
|
-
const registerEntityTypeBodySchema = Type.Object(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
44
|
+
const registerEntityTypeBodySchema = Type.Object(
|
|
45
|
+
{
|
|
46
|
+
name: Type.Optional(Type.String()),
|
|
47
|
+
description: Type.Optional(Type.String()),
|
|
48
|
+
creation_schema: Type.Optional(jsonObjectSchema),
|
|
49
|
+
inbox_schemas: Type.Optional(schemaMapSchema),
|
|
50
|
+
state_schemas: Type.Optional(schemaMapSchema),
|
|
51
|
+
serve_endpoint: Type.Optional(Type.String()),
|
|
52
|
+
default_dispatch_policy: Type.Optional(dispatchPolicySchema),
|
|
53
|
+
},
|
|
54
|
+
{ additionalProperties: false }
|
|
55
|
+
)
|
|
57
56
|
|
|
58
|
-
const amendEntityTypeSchemasBodySchema = Type.Object(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
57
|
+
const amendEntityTypeSchemasBodySchema = Type.Object(
|
|
58
|
+
{
|
|
59
|
+
inbox_schemas: Type.Optional(schemaMapSchema),
|
|
60
|
+
state_schemas: Type.Optional(schemaMapSchema),
|
|
61
|
+
},
|
|
62
|
+
{ additionalProperties: false }
|
|
63
|
+
)
|
|
64
64
|
|
|
65
65
|
type RegisterEntityTypeBody = Static<typeof registerEntityTypeBodySchema>
|
|
66
66
|
type AmendEntityTypeSchemasBody = Static<
|
|
@@ -181,8 +181,8 @@ async function amendSchemas(
|
|
|
181
181
|
const parsed = routeBody<AmendEntityTypeSchemasBody>(request)
|
|
182
182
|
|
|
183
183
|
const updated = await ctx.entityManager.amendSchemas(request.params.name, {
|
|
184
|
-
inbox_schemas: parsed.inbox_schemas
|
|
185
|
-
state_schemas: parsed.state_schemas
|
|
184
|
+
inbox_schemas: parsed.inbox_schemas,
|
|
185
|
+
state_schemas: parsed.state_schemas,
|
|
186
186
|
})
|
|
187
187
|
return json(toPublicEntityType(updated))
|
|
188
188
|
}
|
|
@@ -199,13 +199,12 @@ function normalizeEntityTypeRequest(
|
|
|
199
199
|
parsed: RegisterEntityTypeBody | RegisterEntityTypeRequest
|
|
200
200
|
): RegisterEntityTypeRequest {
|
|
201
201
|
const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint)
|
|
202
|
-
const compatibilityFields = parsed as RegisterEntityTypeBody
|
|
203
202
|
return {
|
|
204
203
|
name: parsed.name ?? ``,
|
|
205
204
|
description: parsed.description ?? ``,
|
|
206
205
|
creation_schema: parsed.creation_schema,
|
|
207
|
-
inbox_schemas: parsed.inbox_schemas
|
|
208
|
-
state_schemas: parsed.state_schemas
|
|
206
|
+
inbox_schemas: parsed.inbox_schemas,
|
|
207
|
+
state_schemas: parsed.state_schemas,
|
|
209
208
|
serve_endpoint: serveEndpoint,
|
|
210
209
|
default_dispatch_policy:
|
|
211
210
|
parsed.default_dispatch_policy ??
|
|
@@ -222,8 +221,6 @@ function toPublicEntityType(
|
|
|
222
221
|
): PublicEntityTypeResponse {
|
|
223
222
|
return {
|
|
224
223
|
...entityType,
|
|
225
|
-
input_schemas: entityType.inbox_schemas,
|
|
226
|
-
output_schemas: entityType.state_schemas,
|
|
227
224
|
revision: entityType.revision,
|
|
228
225
|
}
|
|
229
226
|
}
|
package/src/routing/hooks.ts
CHANGED
|
@@ -121,7 +121,7 @@ export function rejectIfShuttingDown(
|
|
|
121
121
|
): Response | undefined {
|
|
122
122
|
if (!ctx.isShuttingDown()) return undefined
|
|
123
123
|
const path = new URL(req.url).pathname
|
|
124
|
-
if (!path.startsWith(`/_electric/
|
|
124
|
+
if (!path.startsWith(`/_electric/subscription-webhooks/`)) return undefined
|
|
125
125
|
return apiError(503, `SERVER_STOPPING`, `Server is shutting down`)
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from '../electric-agents-http.js'
|
|
17
17
|
import { consumerCallbacks, subscriptionWebhooks } from '../db/schema.js'
|
|
18
18
|
import {
|
|
19
|
-
|
|
19
|
+
ErrCodeWakeCallbackNotFound,
|
|
20
20
|
ErrCodeForkInProgress,
|
|
21
21
|
ErrCodeSubscriptionNotFound,
|
|
22
22
|
ErrCodeUnauthorized,
|
|
@@ -26,12 +26,12 @@ import { decodeJsonObject } from '../utils/server-utils.js'
|
|
|
26
26
|
import { serverLog } from '../utils/log.js'
|
|
27
27
|
import { applyDurableStreamsBearer } from '../stream-client.js'
|
|
28
28
|
import { getDefaultWebhookSigner } from '../webhook-signing.js'
|
|
29
|
-
import { cronRouter } from './cron-router.js'
|
|
30
29
|
import { resolveDurableStreamsRoutingAdapter } from './durable-streams-routing-adapter.js'
|
|
31
30
|
import { electricProxyRouter } from './electric-proxy-router.js'
|
|
32
31
|
import { entitiesRouter } from './entities-router.js'
|
|
33
32
|
import { entityTypesRouter } from './entity-types-router.js'
|
|
34
33
|
import { getRequestSpan } from './hooks.js'
|
|
34
|
+
import { observationsRouter } from './observations-router.js'
|
|
35
35
|
import { runnersRouter } from './runners-router.js'
|
|
36
36
|
import { routeBody, validateOptionalJsonBody, withSchema } from './schema.js'
|
|
37
37
|
import { withLeadingSlash } from './tenant-stream-paths.js'
|
|
@@ -69,7 +69,7 @@ const wakeRegistrationBodySchema = Type.Object({
|
|
|
69
69
|
manifestKey: Type.Optional(Type.String()),
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
-
const
|
|
72
|
+
const subscriptionWebhookBodySchema = Type.Object(
|
|
73
73
|
{
|
|
74
74
|
subscription_id: Type.Optional(Type.String()),
|
|
75
75
|
wake_id: Type.Optional(Type.String()),
|
|
@@ -87,7 +87,7 @@ const webhookForwardBodySchema = Type.Object(
|
|
|
87
87
|
{ additionalProperties: true }
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
const
|
|
90
|
+
const wakeCallbackBodySchema = Type.Object(
|
|
91
91
|
{
|
|
92
92
|
epoch: Type.Optional(Type.Number()),
|
|
93
93
|
generation: Type.Optional(Type.Number()),
|
|
@@ -100,8 +100,8 @@ const callbackForwardBodySchema = Type.Object(
|
|
|
100
100
|
)
|
|
101
101
|
|
|
102
102
|
type WakeRegistrationBody = Static<typeof wakeRegistrationBodySchema>
|
|
103
|
-
type
|
|
104
|
-
type
|
|
103
|
+
type SubscriptionWebhookBody = Static<typeof subscriptionWebhookBodySchema>
|
|
104
|
+
type WakeCallbackBody = Static<typeof wakeCallbackBodySchema>
|
|
105
105
|
|
|
106
106
|
const DS_SUBSCRIPTION_CALLBACK_PREFIX = `ds-subscription:`
|
|
107
107
|
|
|
@@ -126,13 +126,16 @@ internalRouter.post(
|
|
|
126
126
|
withSchema(wakeRegistrationBodySchema),
|
|
127
127
|
registerWake
|
|
128
128
|
)
|
|
129
|
-
internalRouter.post(
|
|
130
|
-
|
|
129
|
+
internalRouter.post(
|
|
130
|
+
`/subscription-webhooks/:subscriptionId`,
|
|
131
|
+
subscriptionWebhook
|
|
132
|
+
)
|
|
133
|
+
internalRouter.post(`/wake-callbacks/:consumerId`, wakeCallback)
|
|
131
134
|
internalRouter.all(`/runners`, runnersRouter.fetch)
|
|
132
135
|
internalRouter.all(`/runners/*`, runnersRouter.fetch)
|
|
133
136
|
internalRouter.all(`/entities/*`, entitiesRouter.fetch)
|
|
134
137
|
internalRouter.all(`/entity-types/*`, entityTypesRouter.fetch)
|
|
135
|
-
internalRouter.all(`/
|
|
138
|
+
internalRouter.all(`/observations/*`, observationsRouter.fetch)
|
|
136
139
|
internalRouter.get(`/electric/*`, electricProxyRouter.fetch)
|
|
137
140
|
internalRouter.all(`*`, () => status(404))
|
|
138
141
|
|
|
@@ -183,7 +186,7 @@ function resolveWebhookSigner(ctx: TenantContext): WebhookSigner {
|
|
|
183
186
|
|
|
184
187
|
function durableStreamsWebhookJwksUrl(ctx: TenantContext): string {
|
|
185
188
|
if (!ctx.durableStreamsRouting) {
|
|
186
|
-
return
|
|
189
|
+
return appendPathToBackendUrl(ctx.durableStreamsUrl, `/__ds/jwks.json`)
|
|
187
190
|
}
|
|
188
191
|
|
|
189
192
|
return resolveDurableStreamsRoutingAdapter(
|
|
@@ -198,6 +201,28 @@ function durableStreamsWebhookJwksUrl(ctx: TenantContext): string {
|
|
|
198
201
|
.toString()
|
|
199
202
|
}
|
|
200
203
|
|
|
204
|
+
function appendPathToBackendUrl(baseUrl: string, path: string): string {
|
|
205
|
+
const base = new URL(baseUrl)
|
|
206
|
+
const pathUrl = new URL(path, `http://electric-agents.local`)
|
|
207
|
+
const basePath =
|
|
208
|
+
base.pathname === `/` ? `` : base.pathname.replace(/\/+$/, ``)
|
|
209
|
+
const suffix = pathUrl.pathname.startsWith(`/`)
|
|
210
|
+
? pathUrl.pathname
|
|
211
|
+
: `/${pathUrl.pathname}`
|
|
212
|
+
const target = new URL(base)
|
|
213
|
+
|
|
214
|
+
target.pathname = `${basePath}${suffix}`
|
|
215
|
+
target.search = ``
|
|
216
|
+
target.hash = pathUrl.hash
|
|
217
|
+
base.searchParams.forEach((value, key) => {
|
|
218
|
+
target.searchParams.append(key, value)
|
|
219
|
+
})
|
|
220
|
+
pathUrl.searchParams.forEach((value, key) => {
|
|
221
|
+
target.searchParams.append(key, value)
|
|
222
|
+
})
|
|
223
|
+
return target.toString()
|
|
224
|
+
}
|
|
225
|
+
|
|
201
226
|
function durableStreamsJwksFetchClient(ctx: TenantContext): typeof fetch {
|
|
202
227
|
return async (input, init) => {
|
|
203
228
|
const headers = new Headers(init?.headers)
|
|
@@ -269,7 +294,7 @@ function claimTokenFromRequest(request: IRequest): string | undefined {
|
|
|
269
294
|
)
|
|
270
295
|
}
|
|
271
296
|
|
|
272
|
-
function newWebhookPayload(body:
|
|
297
|
+
function newWebhookPayload(body: SubscriptionWebhookBody | undefined): {
|
|
273
298
|
wakeId: string
|
|
274
299
|
generation: number
|
|
275
300
|
primaryStream: string
|
|
@@ -353,13 +378,13 @@ function isAgentVisibleEventSource(source: EventSourceContract): boolean {
|
|
|
353
378
|
return source.agentVisible === true && source.status === `active`
|
|
354
379
|
}
|
|
355
380
|
|
|
356
|
-
async function
|
|
381
|
+
async function subscriptionWebhook(
|
|
357
382
|
request: IRequest,
|
|
358
383
|
ctx: TenantContext
|
|
359
384
|
): Promise<Response> {
|
|
360
385
|
const subscriptionId = routeParam(request, `subscriptionId`)
|
|
361
386
|
const rootSpan = getRequestSpan(request)
|
|
362
|
-
rootSpan?.updateName(`webhook
|
|
387
|
+
rootSpan?.updateName(`subscription-webhook`)
|
|
363
388
|
rootSpan?.setAttribute(
|
|
364
389
|
`electric_agents.webhook.subscription_id`,
|
|
365
390
|
subscriptionId
|
|
@@ -398,7 +423,7 @@ async function webhookForward(
|
|
|
398
423
|
)
|
|
399
424
|
}
|
|
400
425
|
const parsedBodyResult = validateOptionalJsonBody(
|
|
401
|
-
|
|
426
|
+
subscriptionWebhookBodySchema,
|
|
402
427
|
body,
|
|
403
428
|
request.headers.get(`content-type`)
|
|
404
429
|
)
|
|
@@ -406,7 +431,9 @@ async function webhookForward(
|
|
|
406
431
|
|
|
407
432
|
let forwardBody = body
|
|
408
433
|
let runningEntityUrl: string | null = null
|
|
409
|
-
const parsedBody = parsedBodyResult.value as
|
|
434
|
+
const parsedBody = parsedBodyResult.value as
|
|
435
|
+
| SubscriptionWebhookBody
|
|
436
|
+
| undefined
|
|
410
437
|
const newWebhook = newWebhookPayload(parsedBody)
|
|
411
438
|
const routingAdapter = resolveDurableStreamsRoutingAdapter(
|
|
412
439
|
ctx.durableStreamsRouting,
|
|
@@ -485,7 +512,7 @@ async function webhookForward(
|
|
|
485
512
|
})
|
|
486
513
|
.catch((err) => {
|
|
487
514
|
serverLog.warn(
|
|
488
|
-
`[webhook
|
|
515
|
+
`[subscription-webhook] consumerCallbacks upsert failed (non-fatal): ${
|
|
489
516
|
err instanceof Error ? err.message : String(err)
|
|
490
517
|
}`
|
|
491
518
|
)
|
|
@@ -533,7 +560,7 @@ async function webhookForward(
|
|
|
533
560
|
if (consumerId && callbackUrl) {
|
|
534
561
|
const callback = appendPathToUrl(
|
|
535
562
|
ctx.publicUrl,
|
|
536
|
-
`/_electric/
|
|
563
|
+
`/_electric/wake-callbacks/${encodeURIComponent(consumerId)}`
|
|
537
564
|
)
|
|
538
565
|
enriched.callback = callback
|
|
539
566
|
if (newWebhook) {
|
|
@@ -582,7 +609,7 @@ async function webhookForward(
|
|
|
582
609
|
}
|
|
583
610
|
return apiError(
|
|
584
611
|
502,
|
|
585
|
-
`
|
|
612
|
+
`SUBSCRIPTION_WEBHOOK_FAILED`,
|
|
586
613
|
err instanceof Error ? err.message : String(err)
|
|
587
614
|
)
|
|
588
615
|
}
|
|
@@ -593,7 +620,7 @@ async function webhookForward(
|
|
|
593
620
|
return responseFromUpstream(upstream, responseBytes)
|
|
594
621
|
}
|
|
595
622
|
|
|
596
|
-
async function
|
|
623
|
+
async function wakeCallback(
|
|
597
624
|
request: IRequest,
|
|
598
625
|
ctx: TenantContext
|
|
599
626
|
): Promise<Response> {
|
|
@@ -618,19 +645,19 @@ async function callbackForward(
|
|
|
618
645
|
if (!target) {
|
|
619
646
|
return apiError(
|
|
620
647
|
404,
|
|
621
|
-
|
|
622
|
-
`Unknown callback
|
|
648
|
+
ErrCodeWakeCallbackNotFound,
|
|
649
|
+
`Unknown wake-callback consumer`
|
|
623
650
|
)
|
|
624
651
|
}
|
|
625
652
|
|
|
626
653
|
const body = await readRequestBody(request as Request)
|
|
627
654
|
const parsedBodyResult = validateOptionalJsonBody(
|
|
628
|
-
|
|
655
|
+
wakeCallbackBodySchema,
|
|
629
656
|
body,
|
|
630
657
|
request.headers.get(`content-type`)
|
|
631
658
|
)
|
|
632
659
|
if (!parsedBodyResult.ok) return parsedBodyResult.response
|
|
633
|
-
const requestBody = parsedBodyResult.value as
|
|
660
|
+
const requestBody = parsedBodyResult.value as WakeCallbackBody | undefined
|
|
634
661
|
const isClaimRequest =
|
|
635
662
|
requestBody?.wakeId !== undefined || requestBody?.wake_id !== undefined
|
|
636
663
|
const isDoneRequest = requestBody?.done === true
|
|
@@ -653,7 +680,7 @@ async function callbackForward(
|
|
|
653
680
|
return json(responseBody)
|
|
654
681
|
}
|
|
655
682
|
|
|
656
|
-
const upstreamBody =
|
|
683
|
+
const upstreamBody = encodeWakeCallbackBody(
|
|
657
684
|
ctx.service,
|
|
658
685
|
consumerId,
|
|
659
686
|
requestBody,
|
|
@@ -673,7 +700,7 @@ async function callbackForward(
|
|
|
673
700
|
if (!token) {
|
|
674
701
|
return apiError(401, `UNAUTHORIZED`, `Missing claim token`)
|
|
675
702
|
}
|
|
676
|
-
const upstreamPayload =
|
|
703
|
+
const upstreamPayload = encodeWakeCallbackPayload(
|
|
677
704
|
consumerId,
|
|
678
705
|
requestBody,
|
|
679
706
|
(stream) => stream.replace(/^\/+/, ``)
|
|
@@ -694,7 +721,7 @@ async function callbackForward(
|
|
|
694
721
|
} catch (err) {
|
|
695
722
|
return apiError(
|
|
696
723
|
502,
|
|
697
|
-
`
|
|
724
|
+
`WAKE_CALLBACK_FAILED`,
|
|
698
725
|
err instanceof Error ? err.message : String(err)
|
|
699
726
|
)
|
|
700
727
|
}
|
|
@@ -733,7 +760,7 @@ async function callbackForward(
|
|
|
733
760
|
}
|
|
734
761
|
if (upstream.ok && isDoneRequest && target.primaryStream) {
|
|
735
762
|
serverLog.info(
|
|
736
|
-
`[callback
|
|
763
|
+
`[wake-callback] done received for stream=${target.primaryStream} consumer=${consumerId}`
|
|
737
764
|
)
|
|
738
765
|
const stillOwnsClaim = ctx.runtime.claimWriteTokens.owns(
|
|
739
766
|
ctx.service,
|
|
@@ -788,11 +815,11 @@ async function callbackForward(
|
|
|
788
815
|
)
|
|
789
816
|
await ctx.entityBridgeManager.onEntityChanged(entity.url)
|
|
790
817
|
serverLog.info(
|
|
791
|
-
`[callback
|
|
818
|
+
`[wake-callback] status updated after done for ${entity.url}`
|
|
792
819
|
)
|
|
793
820
|
} else if (!entity) {
|
|
794
821
|
serverLog.warn(
|
|
795
|
-
`[callback
|
|
822
|
+
`[wake-callback] done received but no entity found for stream=${target.primaryStream}`
|
|
796
823
|
)
|
|
797
824
|
}
|
|
798
825
|
|
|
@@ -806,19 +833,19 @@ async function callbackForward(
|
|
|
806
833
|
)
|
|
807
834
|
} else if (entity) {
|
|
808
835
|
serverLog.info(
|
|
809
|
-
`[callback
|
|
836
|
+
`[wake-callback] done arrived after in-memory token evicted (stream=${target.primaryStream} consumer=${consumerId})`
|
|
810
837
|
)
|
|
811
838
|
}
|
|
812
839
|
} else if (requestBody?.done === true) {
|
|
813
840
|
serverLog.warn(
|
|
814
|
-
`[callback
|
|
841
|
+
`[wake-callback] done received but skipped: upstream.ok=${upstream.ok} primaryStream=${
|
|
815
842
|
target.primaryStream ?? `null`
|
|
816
843
|
} consumer=${consumerId}`
|
|
817
844
|
)
|
|
818
845
|
}
|
|
819
846
|
} catch (err) {
|
|
820
847
|
serverLog.error(
|
|
821
|
-
`[callback
|
|
848
|
+
`[wake-callback] error processing done for consumer=${consumerId}: ${
|
|
822
849
|
err instanceof Error ? err.message : String(err)
|
|
823
850
|
}`
|
|
824
851
|
)
|
|
@@ -838,21 +865,21 @@ async function mintClaimWriteToken(
|
|
|
838
865
|
return ctx.runtime.claimWriteTokens.mint(ctx.service, streamPath, consumerId)
|
|
839
866
|
}
|
|
840
867
|
|
|
841
|
-
function
|
|
868
|
+
function encodeWakeCallbackBody(
|
|
842
869
|
service: string,
|
|
843
870
|
consumerId: string,
|
|
844
|
-
body:
|
|
871
|
+
body: WakeCallbackBody | undefined,
|
|
845
872
|
routingAdapter: DurableStreamsRoutingAdapter
|
|
846
873
|
): Uint8Array {
|
|
847
|
-
const payload =
|
|
874
|
+
const payload = encodeWakeCallbackPayload(consumerId, body, (stream) =>
|
|
848
875
|
routingAdapter.toBackendStreamPath(service, stream)
|
|
849
876
|
)
|
|
850
877
|
return new TextEncoder().encode(JSON.stringify(payload))
|
|
851
878
|
}
|
|
852
879
|
|
|
853
|
-
function
|
|
880
|
+
function encodeWakeCallbackPayload(
|
|
854
881
|
consumerId: string,
|
|
855
|
-
body:
|
|
882
|
+
body: WakeCallbackBody | undefined,
|
|
856
883
|
mapStream: (stream: string) => string
|
|
857
884
|
): Record<string, unknown> {
|
|
858
885
|
if (!body) return {}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP routes for ensuring observation backing streams.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Type, type Static } from '@sinclair/typebox'
|
|
6
|
+
import { Router, json } from 'itty-router'
|
|
7
|
+
import { routeBody, withSchema } from './schema.js'
|
|
8
|
+
import type { JsonRouteRequest } from './schema.js'
|
|
9
|
+
import type { RouterType } from 'itty-router'
|
|
10
|
+
import type { TenantContext } from './context.js'
|
|
11
|
+
|
|
12
|
+
const stringRecordSchema = Type.Record(Type.String(), Type.String())
|
|
13
|
+
|
|
14
|
+
const ensureEntitiesMembershipStreamBodySchema = Type.Object({
|
|
15
|
+
tags: Type.Optional(stringRecordSchema),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const ensureCronStreamBodySchema = Type.Object({
|
|
19
|
+
expression: Type.String(),
|
|
20
|
+
timezone: Type.Optional(Type.String()),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
type EnsureEntitiesMembershipStreamBody = Static<
|
|
24
|
+
typeof ensureEntitiesMembershipStreamBodySchema
|
|
25
|
+
>
|
|
26
|
+
type EnsureCronStreamBody = Static<typeof ensureCronStreamBodySchema>
|
|
27
|
+
|
|
28
|
+
export type ObservationsRoutes = RouterType<
|
|
29
|
+
JsonRouteRequest,
|
|
30
|
+
[TenantContext],
|
|
31
|
+
Response | undefined
|
|
32
|
+
>
|
|
33
|
+
|
|
34
|
+
export const observationsRouter: ObservationsRoutes = Router<
|
|
35
|
+
JsonRouteRequest,
|
|
36
|
+
[TenantContext],
|
|
37
|
+
Response | undefined
|
|
38
|
+
>({
|
|
39
|
+
base: `/_electric/observations`,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
observationsRouter.post(
|
|
43
|
+
`/entities/ensure-stream`,
|
|
44
|
+
withSchema(ensureEntitiesMembershipStreamBodySchema),
|
|
45
|
+
ensureEntitiesMembershipStream
|
|
46
|
+
)
|
|
47
|
+
observationsRouter.post(
|
|
48
|
+
`/cron/ensure-stream`,
|
|
49
|
+
withSchema(ensureCronStreamBodySchema),
|
|
50
|
+
ensureCronStream
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
async function ensureEntitiesMembershipStream(
|
|
54
|
+
request: JsonRouteRequest,
|
|
55
|
+
ctx: TenantContext
|
|
56
|
+
): Promise<Response> {
|
|
57
|
+
const parsed = routeBody<EnsureEntitiesMembershipStreamBody>(request)
|
|
58
|
+
const result = await ctx.entityManager.ensureEntitiesMembershipStream(
|
|
59
|
+
parsed.tags ?? {}
|
|
60
|
+
)
|
|
61
|
+
return json(result)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function ensureCronStream(
|
|
65
|
+
request: JsonRouteRequest,
|
|
66
|
+
ctx: TenantContext
|
|
67
|
+
): Promise<Response> {
|
|
68
|
+
const parsed = routeBody<EnsureCronStreamBody>(request)
|
|
69
|
+
const streamPath = await ctx.entityManager.getOrCreateCronStream(
|
|
70
|
+
parsed.expression,
|
|
71
|
+
parsed.timezone
|
|
72
|
+
)
|
|
73
|
+
return json({ streamUrl: streamPath })
|
|
74
|
+
}
|
|
@@ -628,7 +628,7 @@ async function notificationFromClaim(
|
|
|
628
628
|
streams,
|
|
629
629
|
callback: appendPathToUrl(
|
|
630
630
|
ctx.publicUrl,
|
|
631
|
-
`/_electric/
|
|
631
|
+
`/_electric/wake-callbacks/${encodeURIComponent(input.claim.wake_id)}`
|
|
632
632
|
),
|
|
633
633
|
claimToken: input.claim.token,
|
|
634
634
|
triggerEvent: `message_received`,
|
package/src/wake-registry.ts
CHANGED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTTP routes under /_electric/cron.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Type, type Static } from '@sinclair/typebox'
|
|
6
|
-
import { Router, json } from 'itty-router'
|
|
7
|
-
import { routeBody, withSchema } from './schema.js'
|
|
8
|
-
import type { JsonRouteRequest } from './schema.js'
|
|
9
|
-
import type { RouterType } from 'itty-router'
|
|
10
|
-
import type { TenantContext } from './context.js'
|
|
11
|
-
|
|
12
|
-
const cronRegisterBodySchema = Type.Object({
|
|
13
|
-
expression: Type.String(),
|
|
14
|
-
timezone: Type.Optional(Type.String()),
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
type CronRegisterBody = Static<typeof cronRegisterBodySchema>
|
|
18
|
-
|
|
19
|
-
export type CronRoutes = RouterType<
|
|
20
|
-
JsonRouteRequest,
|
|
21
|
-
[TenantContext],
|
|
22
|
-
Response | undefined
|
|
23
|
-
>
|
|
24
|
-
|
|
25
|
-
export const cronRouter: CronRoutes = Router<
|
|
26
|
-
JsonRouteRequest,
|
|
27
|
-
[TenantContext],
|
|
28
|
-
Response | undefined
|
|
29
|
-
>({
|
|
30
|
-
base: `/_electric/cron`,
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
cronRouter.post(`/register`, withSchema(cronRegisterBodySchema), registerCron)
|
|
34
|
-
|
|
35
|
-
async function registerCron(
|
|
36
|
-
request: JsonRouteRequest,
|
|
37
|
-
ctx: TenantContext
|
|
38
|
-
): Promise<Response> {
|
|
39
|
-
const parsed = routeBody<CronRegisterBody>(request)
|
|
40
|
-
const streamPath = await ctx.entityManager.getOrCreateCronStream(
|
|
41
|
-
parsed.expression,
|
|
42
|
-
parsed.timezone
|
|
43
|
-
)
|
|
44
|
-
return json({ streamUrl: streamPath })
|
|
45
|
-
}
|