@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
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP routing for durable stream appends that affect Electric Agents state.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Router } from 'itty-router'
|
|
6
|
+
import { apiError, readRequestBody } from '../electric-agents-http.js'
|
|
7
|
+
import {
|
|
8
|
+
ErrCodeForkInProgress,
|
|
9
|
+
ErrCodeNotRunning,
|
|
10
|
+
ErrCodeUnauthorized,
|
|
11
|
+
} from '../electric-agents-types.js'
|
|
12
|
+
import { serverLog } from '../utils/log.js'
|
|
13
|
+
import type { EntityManager } from '../entity-manager.js'
|
|
14
|
+
import type { IRequest, RouterType } from 'itty-router'
|
|
15
|
+
|
|
16
|
+
type StreamAppendEvent =
|
|
17
|
+
| Record<string, unknown>
|
|
18
|
+
| Array<Record<string, unknown>>
|
|
19
|
+
|
|
20
|
+
export interface ElectricAgentsStreamAppendRouteRequest extends IRequest {
|
|
21
|
+
method: string
|
|
22
|
+
url: string
|
|
23
|
+
headers: Headers
|
|
24
|
+
readBody(): Promise<Uint8Array>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ElectricAgentsStreamAppendRuntime {
|
|
28
|
+
manager: EntityManager
|
|
29
|
+
evaluateWakePayload(
|
|
30
|
+
sourceUrl: string,
|
|
31
|
+
event: StreamAppendEvent
|
|
32
|
+
): Promise<void>
|
|
33
|
+
checkRunFinished(sourceUrl: string, event: StreamAppendEvent): void
|
|
34
|
+
syncManifestWakes(
|
|
35
|
+
subscriberUrl: string,
|
|
36
|
+
event: StreamAppendEvent
|
|
37
|
+
): Promise<void>
|
|
38
|
+
syncManifestEntitySources(
|
|
39
|
+
ownerEntityUrl: string,
|
|
40
|
+
event: StreamAppendEvent
|
|
41
|
+
): Promise<void>
|
|
42
|
+
syncManifestSchedules(
|
|
43
|
+
ownerEntityUrl: string,
|
|
44
|
+
event: StreamAppendEvent
|
|
45
|
+
): Promise<void>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type DurableStreamsAppendForwarder = (
|
|
49
|
+
request: ElectricAgentsStreamAppendRouteRequest,
|
|
50
|
+
body: Uint8Array
|
|
51
|
+
) => Promise<Response>
|
|
52
|
+
|
|
53
|
+
type StreamAppendRouteArgs = [
|
|
54
|
+
ElectricAgentsStreamAppendRuntime,
|
|
55
|
+
DurableStreamsAppendForwarder,
|
|
56
|
+
]
|
|
57
|
+
type StreamAppendRouteResult = Response | undefined
|
|
58
|
+
|
|
59
|
+
export type ElectricAgentsStreamAppendRoutes = RouterType<
|
|
60
|
+
ElectricAgentsStreamAppendRouteRequest,
|
|
61
|
+
StreamAppendRouteArgs,
|
|
62
|
+
StreamAppendRouteResult
|
|
63
|
+
>
|
|
64
|
+
|
|
65
|
+
export const electricAgentsStreamAppendRouter: ElectricAgentsStreamAppendRoutes =
|
|
66
|
+
Router<
|
|
67
|
+
ElectricAgentsStreamAppendRouteRequest,
|
|
68
|
+
StreamAppendRouteArgs,
|
|
69
|
+
StreamAppendRouteResult
|
|
70
|
+
>()
|
|
71
|
+
|
|
72
|
+
electricAgentsStreamAppendRouter.post(`*`, handleStreamAppend)
|
|
73
|
+
|
|
74
|
+
export function createStreamAppendRouteRequest(
|
|
75
|
+
request: Request
|
|
76
|
+
): ElectricAgentsStreamAppendRouteRequest {
|
|
77
|
+
return {
|
|
78
|
+
method: request.method,
|
|
79
|
+
url: request.url,
|
|
80
|
+
headers: request.headers,
|
|
81
|
+
readBody: () => readRequestBody(request),
|
|
82
|
+
} as ElectricAgentsStreamAppendRouteRequest
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function handleStreamAppend(
|
|
86
|
+
request: ElectricAgentsStreamAppendRouteRequest,
|
|
87
|
+
runtime: ElectricAgentsStreamAppendRuntime,
|
|
88
|
+
forward: DurableStreamsAppendForwarder
|
|
89
|
+
): Promise<StreamAppendRouteResult> {
|
|
90
|
+
const path = new URL(request.url).pathname
|
|
91
|
+
const { manager } = runtime
|
|
92
|
+
const entity = await manager.registry.getEntityByStream(path)
|
|
93
|
+
const isSharedState = path.startsWith(`/_electric/shared-state/`)
|
|
94
|
+
if (!entity && !isSharedState) {
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const body = await request.readBody()
|
|
99
|
+
const event = decodeStreamAppendEvent(body)
|
|
100
|
+
|
|
101
|
+
if (entity) {
|
|
102
|
+
const token = writeTokenFromHeaders(request.headers)
|
|
103
|
+
if (!manager.isValidWriteToken(entity, token)) {
|
|
104
|
+
return apiError(401, ErrCodeUnauthorized, `Invalid write token`)
|
|
105
|
+
}
|
|
106
|
+
if (manager.isForkWriteLockedEntity(entity.url)) {
|
|
107
|
+
return apiError(
|
|
108
|
+
409,
|
|
109
|
+
ErrCodeForkInProgress,
|
|
110
|
+
`Entity subtree is being forked`
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
if (entity.status === `stopped`) {
|
|
114
|
+
return apiError(409, ErrCodeNotRunning, `Entity is stopped`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (event) {
|
|
118
|
+
const events = Array.isArray(event) ? event : [event]
|
|
119
|
+
for (const eventItem of events) {
|
|
120
|
+
const validationError = await manager.validateWriteEvent(
|
|
121
|
+
entity,
|
|
122
|
+
eventItem
|
|
123
|
+
)
|
|
124
|
+
if (validationError) {
|
|
125
|
+
return apiError(
|
|
126
|
+
validationError.status,
|
|
127
|
+
validationError.code,
|
|
128
|
+
validationError.message
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} else if (manager.isForkWriteLockedStream(path)) {
|
|
134
|
+
return apiError(
|
|
135
|
+
409,
|
|
136
|
+
ErrCodeForkInProgress,
|
|
137
|
+
`Entity subtree is being forked`
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const upstream = await forward(request, body)
|
|
142
|
+
if (!upstream.ok || !event) {
|
|
143
|
+
return upstream
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (entity) {
|
|
147
|
+
void runtime
|
|
148
|
+
.evaluateWakePayload(entity.url, event)
|
|
149
|
+
.catch((err) =>
|
|
150
|
+
serverLog.warn(`[agent-server] wake evaluation failed:`, err)
|
|
151
|
+
)
|
|
152
|
+
runtime.checkRunFinished(entity.url, event)
|
|
153
|
+
void runtime
|
|
154
|
+
.syncManifestWakes(entity.url, event)
|
|
155
|
+
.catch((err) =>
|
|
156
|
+
serverLog.warn(`[agent-server] manifest wake sync failed:`, err)
|
|
157
|
+
)
|
|
158
|
+
void runtime
|
|
159
|
+
.syncManifestEntitySources(entity.url, event)
|
|
160
|
+
.catch((err) =>
|
|
161
|
+
serverLog.warn(`[agent-server] manifest source sync failed:`, err)
|
|
162
|
+
)
|
|
163
|
+
void runtime
|
|
164
|
+
.syncManifestSchedules(entity.url, event)
|
|
165
|
+
.catch((err) =>
|
|
166
|
+
serverLog.warn(`[agent-server] manifest schedule sync failed:`, err)
|
|
167
|
+
)
|
|
168
|
+
} else {
|
|
169
|
+
void runtime
|
|
170
|
+
.evaluateWakePayload(path, event)
|
|
171
|
+
.catch((err) =>
|
|
172
|
+
serverLog.warn(`[agent-server] wake evaluation failed:`, err)
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return upstream
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function decodeStreamAppendEvent(body: Uint8Array): StreamAppendEvent | null {
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(new TextDecoder().decode(body)) as StreamAppendEvent
|
|
182
|
+
} catch {
|
|
183
|
+
return null
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function writeTokenFromHeaders(headers: Headers): string {
|
|
188
|
+
const electricClaimToken = headers.get(`electric-claim-token`)?.trim()
|
|
189
|
+
if (electricClaimToken) return electricClaimToken
|
|
190
|
+
return (
|
|
191
|
+
headers
|
|
192
|
+
.get(`authorization`)
|
|
193
|
+
?.replace(/^Bearer\s+/i, ``)
|
|
194
|
+
.trim() ?? ``
|
|
195
|
+
)
|
|
196
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function withoutLeadingSlash(path: string): string {
|
|
2
|
+
return path.replace(/^\/+/, ``)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function withLeadingSlash(path: string): string {
|
|
6
|
+
return path.startsWith(`/`) ? path : `/${path}`
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function prefixTenantStreamPath(path: string, tenantId: string): string {
|
|
10
|
+
const normalized = withoutLeadingSlash(path)
|
|
11
|
+
if (!normalized || normalized === tenantId) return tenantId
|
|
12
|
+
if (normalized.startsWith(`${tenantId}/`)) return normalized
|
|
13
|
+
return `${tenantId}/${normalized}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function stripTenantStreamPrefix(
|
|
17
|
+
path: string,
|
|
18
|
+
tenantId: string
|
|
19
|
+
): string {
|
|
20
|
+
const normalized = withoutLeadingSlash(path)
|
|
21
|
+
if (normalized === tenantId) return ``
|
|
22
|
+
if (normalized.startsWith(`${tenantId}/`)) {
|
|
23
|
+
return normalized.slice(tenantId.length + 1)
|
|
24
|
+
}
|
|
25
|
+
return normalized
|
|
26
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface RuntimeRegistration {
|
|
2
|
+
name: string
|
|
3
|
+
publicUrl?: string
|
|
4
|
+
types: string[]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface RuntimeRegistry {
|
|
8
|
+
register(r: RuntimeRegistration): void
|
|
9
|
+
list(): Array<Required<RuntimeRegistration>>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createRuntimeRegistry(): RuntimeRegistry {
|
|
13
|
+
const map = new Map<string, RuntimeRegistration>()
|
|
14
|
+
return {
|
|
15
|
+
register(r) {
|
|
16
|
+
if (!r.publicUrl) {
|
|
17
|
+
console.warn(
|
|
18
|
+
`[agents-server] runtime "${r.name}" registered without publicUrl; omitted from /api/runtimes`
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
// Producers POST entity types one at a time, so merge each
|
|
22
|
+
// single-type registration into the runtime's accumulated list
|
|
23
|
+
// (deduped, first-seen order). Latest publicUrl wins.
|
|
24
|
+
const existing = map.get(r.name)
|
|
25
|
+
if (!existing) {
|
|
26
|
+
map.set(r.name, { ...r, types: [...r.types] })
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
const seen = new Set(existing.types)
|
|
30
|
+
const mergedTypes = [...existing.types]
|
|
31
|
+
for (const t of r.types) {
|
|
32
|
+
if (!seen.has(t)) {
|
|
33
|
+
seen.add(t)
|
|
34
|
+
mergedTypes.push(t)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
map.set(r.name, {
|
|
38
|
+
name: r.name,
|
|
39
|
+
publicUrl: r.publicUrl ?? existing.publicUrl,
|
|
40
|
+
types: mergedTypes,
|
|
41
|
+
})
|
|
42
|
+
},
|
|
43
|
+
list() {
|
|
44
|
+
return [...map.values()].filter((r) => !!r.publicUrl) as Array<
|
|
45
|
+
Required<RuntimeRegistration>
|
|
46
|
+
>
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
}
|