@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,946 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ShapeStream,
|
|
3
|
+
isChangeMessage,
|
|
4
|
+
isControlMessage,
|
|
5
|
+
} from '@electric-sql/client'
|
|
6
|
+
import { and, eq } from 'drizzle-orm'
|
|
7
|
+
import { wakeRegistrations } from './db/schema.js'
|
|
8
|
+
import { serverLog } from './utils/log.js'
|
|
9
|
+
import { electricUrlWithPath } from './utils/electric-url.js'
|
|
10
|
+
import { DEFAULT_TENANT_ID } from './tenant.js'
|
|
11
|
+
import type { DrizzleDB } from './db/index.js'
|
|
12
|
+
import type { Message, Row, Value } from '@electric-sql/client'
|
|
13
|
+
|
|
14
|
+
export interface WakeRegistration {
|
|
15
|
+
tenantId?: string
|
|
16
|
+
subscriberUrl: string
|
|
17
|
+
sourceUrl: string
|
|
18
|
+
condition:
|
|
19
|
+
| `runFinished`
|
|
20
|
+
| {
|
|
21
|
+
on: `change`
|
|
22
|
+
collections?: Array<string>
|
|
23
|
+
ops?: Array<`insert` | `update` | `delete`>
|
|
24
|
+
}
|
|
25
|
+
debounceMs?: number
|
|
26
|
+
timeoutMs?: number
|
|
27
|
+
oneShot: boolean
|
|
28
|
+
includeResponse?: boolean
|
|
29
|
+
manifestKey?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface WakeEvalResult {
|
|
33
|
+
tenantId: string
|
|
34
|
+
subscriberUrl: string
|
|
35
|
+
registrationDbId: number
|
|
36
|
+
sourceEventKey: string
|
|
37
|
+
wakeMessage: {
|
|
38
|
+
source: string
|
|
39
|
+
timeout: boolean
|
|
40
|
+
changes: Array<{
|
|
41
|
+
collection: string
|
|
42
|
+
kind: `insert` | `update` | `delete`
|
|
43
|
+
key: string
|
|
44
|
+
}>
|
|
45
|
+
}
|
|
46
|
+
runFinishedStatus?: `completed` | `failed`
|
|
47
|
+
includeResponse?: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type WakeTimeoutCallback = (result: WakeEvalResult) => void
|
|
51
|
+
export type WakeDebounceCallback = (result: WakeEvalResult) => void
|
|
52
|
+
|
|
53
|
+
interface CachedWakeRegistration extends WakeRegistration {
|
|
54
|
+
tenantId: string
|
|
55
|
+
dbId: number
|
|
56
|
+
createdAt?: Date
|
|
57
|
+
timeoutConsumed?: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface WakeRegistrationShapeRow extends Row<Date> {
|
|
61
|
+
id: number
|
|
62
|
+
tenant_id: string
|
|
63
|
+
subscriber_url: string
|
|
64
|
+
source_url: string
|
|
65
|
+
condition: WakeRegistration[`condition`] & Value<Date>
|
|
66
|
+
debounce_ms: number
|
|
67
|
+
timeout_ms: number
|
|
68
|
+
one_shot: boolean
|
|
69
|
+
timeout_consumed: boolean
|
|
70
|
+
include_response: boolean
|
|
71
|
+
manifest_key: string | null
|
|
72
|
+
created_at: Date
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function wakeSourceEventId(event: Record<string, unknown>): string {
|
|
76
|
+
const headers =
|
|
77
|
+
typeof event.headers === `object` && event.headers !== null
|
|
78
|
+
? (event.headers as Record<string, unknown>)
|
|
79
|
+
: undefined
|
|
80
|
+
const offset = headers?.offset
|
|
81
|
+
if (typeof offset === `string` && offset.length > 0) {
|
|
82
|
+
return offset
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const operation = headers?.operation
|
|
86
|
+
const key = event.key
|
|
87
|
+
if (typeof operation === `string` && typeof key === `string`) {
|
|
88
|
+
return `${operation}:${key}`
|
|
89
|
+
}
|
|
90
|
+
if (typeof key === `string`) {
|
|
91
|
+
return key
|
|
92
|
+
}
|
|
93
|
+
return crypto.randomUUID()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sqlStringLiteral(value: string): string {
|
|
97
|
+
return `'${value.replace(/'/g, `''`)}'`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class WakeRegistry {
|
|
101
|
+
private db: DrizzleDB
|
|
102
|
+
private registrationCache = new Map<string, Array<CachedWakeRegistration>>()
|
|
103
|
+
private debounceTimers = new Map<string, NodeJS.Timeout>()
|
|
104
|
+
private debounceBuffers = new Map<
|
|
105
|
+
string,
|
|
106
|
+
Array<WakeEvalResult[`wakeMessage`][`changes`][number]>
|
|
107
|
+
>()
|
|
108
|
+
private debounceRunStatus = new Map<string, `completed` | `failed`>()
|
|
109
|
+
private timeoutTimers = new Map<string, NodeJS.Timeout>()
|
|
110
|
+
private timeoutDelivered = new Set<number>()
|
|
111
|
+
private timeoutCallbacks = new Map<string, WakeTimeoutCallback>()
|
|
112
|
+
private debounceCallbacks = new Map<string, WakeDebounceCallback>()
|
|
113
|
+
private syncElectricUrl: string | null = null
|
|
114
|
+
private syncElectricSecret: string | undefined
|
|
115
|
+
private syncAbortController: AbortController | null = null
|
|
116
|
+
private syncUnsubscribe: (() => void) | null = null
|
|
117
|
+
private syncReadyPromise: Promise<void> | null = null
|
|
118
|
+
private syncRecoveryPromise: Promise<void> | null = null
|
|
119
|
+
|
|
120
|
+
constructor(
|
|
121
|
+
db: DrizzleDB,
|
|
122
|
+
readonly tenantId: string | null = DEFAULT_TENANT_ID
|
|
123
|
+
) {
|
|
124
|
+
this.db = db
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
setTimeoutCallback(cb: WakeTimeoutCallback, tenantId?: string): void {
|
|
128
|
+
const resolvedTenantId = this.resolveTenantId(tenantId)
|
|
129
|
+
this.timeoutCallbacks.set(resolvedTenantId, cb)
|
|
130
|
+
this.syncTenantTimeoutTimers(resolvedTenantId)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
setDebounceCallback(cb: WakeDebounceCallback, tenantId?: string): void {
|
|
134
|
+
this.debounceCallbacks.set(this.resolveTenantId(tenantId), cb)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private resolveTenantId(tenantId?: string): string {
|
|
138
|
+
if (tenantId) return tenantId
|
|
139
|
+
if (this.tenantId) return this.tenantId
|
|
140
|
+
throw new Error(`WakeRegistry tenantId is required in shared mode`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private cacheKey(tenantId: string, sourceUrl: string): string {
|
|
144
|
+
return `${tenantId}:${sourceUrl}`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private registrationKey(reg: CachedWakeRegistration): string {
|
|
148
|
+
return [
|
|
149
|
+
reg.tenantId,
|
|
150
|
+
reg.subscriberUrl,
|
|
151
|
+
reg.sourceUrl,
|
|
152
|
+
reg.manifestKey ?? ``,
|
|
153
|
+
reg.oneShot ? `1` : `0`,
|
|
154
|
+
reg.debounceMs ?? ``,
|
|
155
|
+
reg.timeoutMs ?? ``,
|
|
156
|
+
JSON.stringify(reg.condition),
|
|
157
|
+
reg.includeResponse === false ? `0` : `1`,
|
|
158
|
+
].join(`:`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private deliverTimeout(result: WakeEvalResult): boolean {
|
|
162
|
+
const callback = this.timeoutCallbacks.get(result.tenantId)
|
|
163
|
+
if (!callback) return false
|
|
164
|
+
callback(result)
|
|
165
|
+
return true
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private deliverDebounce(result: WakeEvalResult): void {
|
|
169
|
+
this.debounceCallbacks.get(result.tenantId)?.(result)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async startSync(electricUrl: string, electricSecret?: string): Promise<void> {
|
|
173
|
+
if (this.syncReadyPromise) {
|
|
174
|
+
await this.syncReadyPromise
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.syncElectricUrl = electricUrl
|
|
179
|
+
this.syncElectricSecret = electricSecret
|
|
180
|
+
|
|
181
|
+
const abortController = new AbortController()
|
|
182
|
+
const stream = new ShapeStream<WakeRegistrationShapeRow>({
|
|
183
|
+
url: electricUrlWithPath(electricUrl, `/v1/shape`).toString(),
|
|
184
|
+
params: {
|
|
185
|
+
table: `wake_registrations`,
|
|
186
|
+
...(this.tenantId
|
|
187
|
+
? { where: `tenant_id = ${sqlStringLiteral(this.tenantId)}` }
|
|
188
|
+
: {}),
|
|
189
|
+
...(electricSecret ? { secret: electricSecret } : {}),
|
|
190
|
+
columns: [
|
|
191
|
+
`id`,
|
|
192
|
+
`tenant_id`,
|
|
193
|
+
`subscriber_url`,
|
|
194
|
+
`source_url`,
|
|
195
|
+
`condition`,
|
|
196
|
+
`debounce_ms`,
|
|
197
|
+
`timeout_ms`,
|
|
198
|
+
`one_shot`,
|
|
199
|
+
`timeout_consumed`,
|
|
200
|
+
`include_response`,
|
|
201
|
+
`manifest_key`,
|
|
202
|
+
`created_at`,
|
|
203
|
+
],
|
|
204
|
+
replica: `full`,
|
|
205
|
+
},
|
|
206
|
+
parser: {
|
|
207
|
+
timestamptz: (value: string) => new Date(value),
|
|
208
|
+
},
|
|
209
|
+
signal: abortController.signal,
|
|
210
|
+
onError: (error) => {
|
|
211
|
+
if (abortController.signal.aborted) {
|
|
212
|
+
return {}
|
|
213
|
+
}
|
|
214
|
+
if (this.syncReadyPromise) {
|
|
215
|
+
void this.recoverSync(error, `shape stream error`)
|
|
216
|
+
}
|
|
217
|
+
return {}
|
|
218
|
+
},
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
this.syncAbortController = abortController
|
|
222
|
+
this.syncReadyPromise = new Promise<void>((resolve, reject) => {
|
|
223
|
+
let settled = false
|
|
224
|
+
|
|
225
|
+
this.syncUnsubscribe = stream.subscribe(
|
|
226
|
+
async (messages) => {
|
|
227
|
+
try {
|
|
228
|
+
for (const message of messages) {
|
|
229
|
+
await this.applyShapeMessage(message)
|
|
230
|
+
if (
|
|
231
|
+
!settled &&
|
|
232
|
+
`control` in message.headers &&
|
|
233
|
+
message.headers.control === `up-to-date`
|
|
234
|
+
) {
|
|
235
|
+
settled = true
|
|
236
|
+
resolve()
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (!settled) {
|
|
241
|
+
settled = true
|
|
242
|
+
reject(error)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
serverLog.error(
|
|
246
|
+
`[wake-registry] failed to apply shape change:`,
|
|
247
|
+
error
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
(error) => {
|
|
252
|
+
if (!settled) {
|
|
253
|
+
settled = true
|
|
254
|
+
reject(error)
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
void this.recoverSync(error, `subscription error`)
|
|
258
|
+
}
|
|
259
|
+
)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
await this.syncReadyPromise
|
|
264
|
+
} catch (error) {
|
|
265
|
+
await this.stopSync()
|
|
266
|
+
throw error
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async stopSync(): Promise<void> {
|
|
271
|
+
this.syncUnsubscribe?.()
|
|
272
|
+
this.syncUnsubscribe = null
|
|
273
|
+
this.syncAbortController?.abort()
|
|
274
|
+
this.syncAbortController = null
|
|
275
|
+
this.syncReadyPromise = null
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private async recoverSync(
|
|
279
|
+
error: unknown,
|
|
280
|
+
source: `shape stream error` | `subscription error`
|
|
281
|
+
): Promise<void> {
|
|
282
|
+
if (this.syncRecoveryPromise) {
|
|
283
|
+
return this.syncRecoveryPromise
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const electricUrl = this.syncElectricUrl
|
|
287
|
+
if (!electricUrl) {
|
|
288
|
+
serverLog.error(
|
|
289
|
+
`[wake-registry] Electric sync failed (${source}):`,
|
|
290
|
+
error
|
|
291
|
+
)
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.syncRecoveryPromise = (async () => {
|
|
296
|
+
serverLog.error(
|
|
297
|
+
`[wake-registry] Electric sync failed (${source}):`,
|
|
298
|
+
error
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
await this.stopSync()
|
|
302
|
+
await this.loadRegistrations()
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
await this.startSync(electricUrl, this.syncElectricSecret)
|
|
306
|
+
serverLog.info(`[wake-registry] Electric sync recovered`)
|
|
307
|
+
} catch (recoveryError) {
|
|
308
|
+
serverLog.error(
|
|
309
|
+
`[wake-registry] Electric sync recovery failed:`,
|
|
310
|
+
recoveryError
|
|
311
|
+
)
|
|
312
|
+
} finally {
|
|
313
|
+
this.syncRecoveryPromise = null
|
|
314
|
+
}
|
|
315
|
+
})()
|
|
316
|
+
|
|
317
|
+
return this.syncRecoveryPromise
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async register(reg: WakeRegistration): Promise<void> {
|
|
321
|
+
const tenantId = this.resolveTenantId(reg.tenantId)
|
|
322
|
+
const result = await this.db
|
|
323
|
+
.insert(wakeRegistrations)
|
|
324
|
+
.values({
|
|
325
|
+
tenantId,
|
|
326
|
+
subscriberUrl: reg.subscriberUrl,
|
|
327
|
+
sourceUrl: reg.sourceUrl,
|
|
328
|
+
condition: reg.condition,
|
|
329
|
+
debounceMs: reg.debounceMs ?? 0,
|
|
330
|
+
timeoutMs: reg.timeoutMs ?? 0,
|
|
331
|
+
oneShot: reg.oneShot,
|
|
332
|
+
includeResponse: reg.includeResponse !== false,
|
|
333
|
+
manifestKey: reg.manifestKey ?? null,
|
|
334
|
+
})
|
|
335
|
+
.onConflictDoNothing()
|
|
336
|
+
.returning({ id: wakeRegistrations.id })
|
|
337
|
+
|
|
338
|
+
if (result.length === 0) {
|
|
339
|
+
// Another path (e.g. manifest-sync) may have created the row first.
|
|
340
|
+
// Refresh the cache so this process still sees the effective registration.
|
|
341
|
+
await this.loadRegistrations()
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const dbId = result[0]!.id
|
|
346
|
+
this.upsertCachedRegistration({
|
|
347
|
+
...reg,
|
|
348
|
+
tenantId,
|
|
349
|
+
dbId,
|
|
350
|
+
createdAt: new Date(),
|
|
351
|
+
timeoutConsumed: false,
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private startTimeoutTimer(reg: CachedWakeRegistration, dbId: number): void {
|
|
356
|
+
if (reg.timeoutMs == null || reg.timeoutMs <= 0) return
|
|
357
|
+
this.startTimeoutTimerWithDuration(reg, dbId, reg.timeoutMs)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private async markTimeoutConsumed(
|
|
361
|
+
dbId: number,
|
|
362
|
+
tenantId: string
|
|
363
|
+
): Promise<void> {
|
|
364
|
+
await this.db
|
|
365
|
+
.update(wakeRegistrations)
|
|
366
|
+
.set({ timeoutConsumed: true })
|
|
367
|
+
.where(
|
|
368
|
+
and(
|
|
369
|
+
eq(wakeRegistrations.tenantId, tenantId),
|
|
370
|
+
eq(wakeRegistrations.id, dbId)
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async unregisterByManifestKey(
|
|
376
|
+
subscriberUrl: string,
|
|
377
|
+
manifestKey: string,
|
|
378
|
+
tenantId?: string
|
|
379
|
+
): Promise<void> {
|
|
380
|
+
const resolvedTenantId = this.resolveTenantId(tenantId)
|
|
381
|
+
await this.db
|
|
382
|
+
.delete(wakeRegistrations)
|
|
383
|
+
.where(
|
|
384
|
+
and(
|
|
385
|
+
eq(wakeRegistrations.tenantId, resolvedTenantId),
|
|
386
|
+
eq(wakeRegistrations.subscriberUrl, subscriberUrl),
|
|
387
|
+
eq(wakeRegistrations.manifestKey, manifestKey)
|
|
388
|
+
)
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
const toRemove = Array.from(this.registrationCache.values()).flatMap(
|
|
392
|
+
(regs) =>
|
|
393
|
+
regs
|
|
394
|
+
.filter(
|
|
395
|
+
(r) =>
|
|
396
|
+
r.tenantId === resolvedTenantId &&
|
|
397
|
+
r.subscriberUrl === subscriberUrl &&
|
|
398
|
+
r.manifestKey === manifestKey
|
|
399
|
+
)
|
|
400
|
+
.map((r) => r.dbId)
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
for (const dbId of toRemove) {
|
|
404
|
+
this.removeCachedRegistrationByDbId(dbId)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async unregisterBySubscriber(
|
|
409
|
+
subscriberUrl: string,
|
|
410
|
+
tenantId?: string
|
|
411
|
+
): Promise<void> {
|
|
412
|
+
const resolvedTenantId = this.resolveTenantId(tenantId)
|
|
413
|
+
await this.db
|
|
414
|
+
.delete(wakeRegistrations)
|
|
415
|
+
.where(
|
|
416
|
+
and(
|
|
417
|
+
eq(wakeRegistrations.tenantId, resolvedTenantId),
|
|
418
|
+
eq(wakeRegistrations.subscriberUrl, subscriberUrl)
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
const toRemove = Array.from(this.registrationCache.values()).flatMap(
|
|
423
|
+
(regs) =>
|
|
424
|
+
regs
|
|
425
|
+
.filter(
|
|
426
|
+
(r) =>
|
|
427
|
+
r.tenantId === resolvedTenantId &&
|
|
428
|
+
r.subscriberUrl === subscriberUrl
|
|
429
|
+
)
|
|
430
|
+
.map((r) => r.dbId)
|
|
431
|
+
)
|
|
432
|
+
for (const dbId of toRemove) {
|
|
433
|
+
this.removeCachedRegistrationByDbId(dbId)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async unregisterBySource(
|
|
438
|
+
sourceUrl: string,
|
|
439
|
+
tenantId?: string
|
|
440
|
+
): Promise<void> {
|
|
441
|
+
const resolvedTenantId = this.resolveTenantId(tenantId)
|
|
442
|
+
await this.db
|
|
443
|
+
.delete(wakeRegistrations)
|
|
444
|
+
.where(
|
|
445
|
+
and(
|
|
446
|
+
eq(wakeRegistrations.tenantId, resolvedTenantId),
|
|
447
|
+
eq(wakeRegistrations.sourceUrl, sourceUrl)
|
|
448
|
+
)
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
const key = this.cacheKey(resolvedTenantId, sourceUrl)
|
|
452
|
+
const regs = this.registrationCache.get(key)
|
|
453
|
+
if (regs) {
|
|
454
|
+
for (const reg of [...regs]) {
|
|
455
|
+
this.removeCachedRegistrationByDbId(reg.dbId)
|
|
456
|
+
}
|
|
457
|
+
this.registrationCache.delete(key)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async unregisterBySubscriberAndSource(
|
|
462
|
+
subscriberUrl: string,
|
|
463
|
+
sourceUrl: string,
|
|
464
|
+
tenantId?: string
|
|
465
|
+
): Promise<void> {
|
|
466
|
+
const resolvedTenantId = this.resolveTenantId(tenantId)
|
|
467
|
+
await this.db
|
|
468
|
+
.delete(wakeRegistrations)
|
|
469
|
+
.where(
|
|
470
|
+
and(
|
|
471
|
+
eq(wakeRegistrations.tenantId, resolvedTenantId),
|
|
472
|
+
eq(wakeRegistrations.subscriberUrl, subscriberUrl),
|
|
473
|
+
eq(wakeRegistrations.sourceUrl, sourceUrl)
|
|
474
|
+
)
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
const regs = this.registrationCache.get(
|
|
478
|
+
this.cacheKey(resolvedTenantId, sourceUrl)
|
|
479
|
+
)
|
|
480
|
+
if (regs) {
|
|
481
|
+
const toRemove = regs
|
|
482
|
+
.filter(
|
|
483
|
+
(r) =>
|
|
484
|
+
r.tenantId === resolvedTenantId && r.subscriberUrl === subscriberUrl
|
|
485
|
+
)
|
|
486
|
+
.map((r) => r.dbId)
|
|
487
|
+
for (const dbId of toRemove) {
|
|
488
|
+
this.removeCachedRegistrationByDbId(dbId)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async loadRegistrations(): Promise<void> {
|
|
494
|
+
const rows =
|
|
495
|
+
this.tenantId === null
|
|
496
|
+
? await this.db.select().from(wakeRegistrations)
|
|
497
|
+
: await this.db
|
|
498
|
+
.select()
|
|
499
|
+
.from(wakeRegistrations)
|
|
500
|
+
.where(eq(wakeRegistrations.tenantId, this.tenantId))
|
|
501
|
+
|
|
502
|
+
this.resetCachedRegistrations()
|
|
503
|
+
|
|
504
|
+
for (const row of rows) {
|
|
505
|
+
const reg: CachedWakeRegistration = {
|
|
506
|
+
tenantId: row.tenantId,
|
|
507
|
+
subscriberUrl: row.subscriberUrl,
|
|
508
|
+
sourceUrl: row.sourceUrl,
|
|
509
|
+
condition: row.condition as WakeRegistration[`condition`],
|
|
510
|
+
debounceMs: row.debounceMs || undefined,
|
|
511
|
+
timeoutMs: row.timeoutMs || undefined,
|
|
512
|
+
oneShot: row.oneShot,
|
|
513
|
+
includeResponse: row.includeResponse === false ? false : undefined,
|
|
514
|
+
manifestKey: row.manifestKey ?? undefined,
|
|
515
|
+
dbId: row.id,
|
|
516
|
+
createdAt: row.createdAt,
|
|
517
|
+
timeoutConsumed: row.timeoutConsumed,
|
|
518
|
+
}
|
|
519
|
+
this.upsertCachedRegistration(reg)
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private startTimeoutTimerWithDuration(
|
|
524
|
+
reg: CachedWakeRegistration,
|
|
525
|
+
dbId: number,
|
|
526
|
+
durationMs: number
|
|
527
|
+
): void {
|
|
528
|
+
const timerKey = this.registrationKey(reg)
|
|
529
|
+
const timer = setTimeout(() => {
|
|
530
|
+
this.timeoutTimers.delete(timerKey)
|
|
531
|
+
this.deliverTimeoutForRegistration(reg, dbId)
|
|
532
|
+
}, durationMs)
|
|
533
|
+
this.timeoutTimers.set(timerKey, timer)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private clearDebounceState(timerKey: string): void {
|
|
537
|
+
const debounceTimer = this.debounceTimers.get(timerKey)
|
|
538
|
+
if (debounceTimer) {
|
|
539
|
+
clearTimeout(debounceTimer)
|
|
540
|
+
this.debounceTimers.delete(timerKey)
|
|
541
|
+
this.debounceBuffers.delete(timerKey)
|
|
542
|
+
this.debounceRunStatus.delete(timerKey)
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private clearTimeoutState(timerKey: string): void {
|
|
547
|
+
const timeoutTimer = this.timeoutTimers.get(timerKey)
|
|
548
|
+
if (timeoutTimer) {
|
|
549
|
+
clearTimeout(timeoutTimer)
|
|
550
|
+
this.timeoutTimers.delete(timerKey)
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private clearRegistrationState(reg: CachedWakeRegistration): void {
|
|
555
|
+
const timerKey = this.registrationKey(reg)
|
|
556
|
+
this.clearDebounceState(timerKey)
|
|
557
|
+
this.clearTimeoutState(timerKey)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private resetCachedRegistrations(): void {
|
|
561
|
+
for (const timer of this.debounceTimers.values()) {
|
|
562
|
+
clearTimeout(timer)
|
|
563
|
+
}
|
|
564
|
+
this.debounceTimers.clear()
|
|
565
|
+
this.debounceBuffers.clear()
|
|
566
|
+
this.debounceRunStatus.clear()
|
|
567
|
+
|
|
568
|
+
for (const timer of this.timeoutTimers.values()) {
|
|
569
|
+
clearTimeout(timer)
|
|
570
|
+
}
|
|
571
|
+
this.timeoutTimers.clear()
|
|
572
|
+
this.registrationCache.clear()
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private findCachedRegistration(
|
|
576
|
+
dbId: number
|
|
577
|
+
): { cacheKey: string; index: number; reg: CachedWakeRegistration } | null {
|
|
578
|
+
for (const [cacheKey, regs] of this.registrationCache) {
|
|
579
|
+
const index = regs.findIndex((reg) => reg.dbId === dbId)
|
|
580
|
+
if (index >= 0) {
|
|
581
|
+
return {
|
|
582
|
+
cacheKey,
|
|
583
|
+
index,
|
|
584
|
+
reg: regs[index]!,
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return null
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private upsertCachedRegistration(reg: CachedWakeRegistration): void {
|
|
593
|
+
const existing = this.findCachedRegistration(reg.dbId)
|
|
594
|
+
const nextKey = this.registrationKey(reg)
|
|
595
|
+
|
|
596
|
+
if (existing) {
|
|
597
|
+
const previousKey = this.registrationKey(existing.reg)
|
|
598
|
+
const regs = this.registrationCache.get(existing.cacheKey)
|
|
599
|
+
if (regs) {
|
|
600
|
+
regs.splice(existing.index, 1)
|
|
601
|
+
if (regs.length === 0) {
|
|
602
|
+
this.registrationCache.delete(existing.cacheKey)
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (previousKey !== nextKey) {
|
|
606
|
+
this.clearRegistrationState(existing.reg)
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const cacheKey = this.cacheKey(reg.tenantId, reg.sourceUrl)
|
|
611
|
+
const cached = this.registrationCache.get(cacheKey) ?? []
|
|
612
|
+
cached.push(reg)
|
|
613
|
+
this.registrationCache.set(cacheKey, cached)
|
|
614
|
+
this.syncTimeoutTimer(reg)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private removeCachedRegistrationByDbId(dbId: number): void {
|
|
618
|
+
const existing = this.findCachedRegistration(dbId)
|
|
619
|
+
if (!existing) return
|
|
620
|
+
|
|
621
|
+
this.clearRegistrationState(existing.reg)
|
|
622
|
+
this.timeoutDelivered.delete(dbId)
|
|
623
|
+
|
|
624
|
+
const regs = this.registrationCache.get(existing.cacheKey)
|
|
625
|
+
if (!regs) return
|
|
626
|
+
regs.splice(existing.index, 1)
|
|
627
|
+
if (regs.length === 0) {
|
|
628
|
+
this.registrationCache.delete(existing.cacheKey)
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private syncTimeoutTimer(reg: CachedWakeRegistration): void {
|
|
633
|
+
const timerKey = this.registrationKey(reg)
|
|
634
|
+
|
|
635
|
+
if (reg.timeoutConsumed || reg.timeoutMs == null || reg.timeoutMs <= 0) {
|
|
636
|
+
this.clearTimeoutState(timerKey)
|
|
637
|
+
return
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (this.timeoutTimers.has(timerKey)) {
|
|
641
|
+
return
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (!reg.createdAt) {
|
|
645
|
+
this.startTimeoutTimer(reg, reg.dbId)
|
|
646
|
+
return
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const remaining = reg.createdAt.getTime() + reg.timeoutMs - Date.now()
|
|
650
|
+
if (remaining > 0) {
|
|
651
|
+
this.startTimeoutTimerWithDuration(reg, reg.dbId, remaining)
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (this.timeoutDelivered.has(reg.dbId)) {
|
|
656
|
+
return
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
this.deliverTimeoutForRegistration(reg, reg.dbId)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
private deliverTimeoutForRegistration(
|
|
663
|
+
reg: CachedWakeRegistration,
|
|
664
|
+
dbId: number
|
|
665
|
+
): void {
|
|
666
|
+
if (this.deliverTimeout(this.timeoutWakeResult(reg, dbId))) {
|
|
667
|
+
this.timeoutDelivered.add(dbId)
|
|
668
|
+
void this.markTimeoutConsumed(dbId, reg.tenantId)
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
private syncTenantTimeoutTimers(tenantId: string): void {
|
|
673
|
+
for (const regs of this.registrationCache.values()) {
|
|
674
|
+
for (const reg of regs) {
|
|
675
|
+
if (reg.tenantId === tenantId) {
|
|
676
|
+
this.syncTimeoutTimer(reg)
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
private timeoutWakeResult(
|
|
683
|
+
reg: CachedWakeRegistration,
|
|
684
|
+
dbId: number
|
|
685
|
+
): WakeEvalResult {
|
|
686
|
+
return {
|
|
687
|
+
tenantId: reg.tenantId,
|
|
688
|
+
subscriberUrl: reg.subscriberUrl,
|
|
689
|
+
registrationDbId: dbId,
|
|
690
|
+
sourceEventKey: `timeout`,
|
|
691
|
+
wakeMessage: {
|
|
692
|
+
source: reg.sourceUrl,
|
|
693
|
+
timeout: true,
|
|
694
|
+
changes: [],
|
|
695
|
+
},
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
private normalizeShapeRow(
|
|
700
|
+
row: WakeRegistrationShapeRow
|
|
701
|
+
): CachedWakeRegistration {
|
|
702
|
+
return {
|
|
703
|
+
tenantId:
|
|
704
|
+
(row as { tenant_id?: string }).tenant_id ?? this.resolveTenantId(),
|
|
705
|
+
subscriberUrl: row.subscriber_url,
|
|
706
|
+
sourceUrl: row.source_url,
|
|
707
|
+
condition: row.condition,
|
|
708
|
+
debounceMs: row.debounce_ms || undefined,
|
|
709
|
+
timeoutMs: row.timeout_ms || undefined,
|
|
710
|
+
oneShot: row.one_shot,
|
|
711
|
+
includeResponse: row.include_response === false ? false : undefined,
|
|
712
|
+
manifestKey: row.manifest_key ?? undefined,
|
|
713
|
+
dbId: row.id,
|
|
714
|
+
createdAt: row.created_at,
|
|
715
|
+
timeoutConsumed: row.timeout_consumed,
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
private async applyShapeMessage(
|
|
720
|
+
message: Message<WakeRegistrationShapeRow>
|
|
721
|
+
): Promise<void> {
|
|
722
|
+
if (isControlMessage(message)) {
|
|
723
|
+
if (message.headers.control === `must-refetch`) {
|
|
724
|
+
this.resetCachedRegistrations()
|
|
725
|
+
}
|
|
726
|
+
return
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (!isChangeMessage(message)) {
|
|
730
|
+
return
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (message.headers.operation === `delete`) {
|
|
734
|
+
this.removeCachedRegistrationByDbId(Number(message.key))
|
|
735
|
+
return
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
this.upsertCachedRegistration(this.normalizeShapeRow(message.value))
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
evaluate(
|
|
742
|
+
sourceUrl: string,
|
|
743
|
+
event: Record<string, unknown>,
|
|
744
|
+
tenantId?: string
|
|
745
|
+
): Array<WakeEvalResult> {
|
|
746
|
+
const resolvedTenantId = this.resolveTenantId(tenantId)
|
|
747
|
+
const cacheKey = this.cacheKey(resolvedTenantId, sourceUrl)
|
|
748
|
+
const regs = this.registrationCache.get(cacheKey)
|
|
749
|
+
if (!regs || regs.length === 0) return []
|
|
750
|
+
|
|
751
|
+
const results: Array<WakeEvalResult> = []
|
|
752
|
+
const toRemove: Array<number> = []
|
|
753
|
+
|
|
754
|
+
for (let i = 0; i < regs.length; i++) {
|
|
755
|
+
const reg = regs[i]!
|
|
756
|
+
const match = this.matchCondition(reg, event)
|
|
757
|
+
if (!match) continue
|
|
758
|
+
|
|
759
|
+
const timerKey = this.registrationKey(reg)
|
|
760
|
+
const timeoutTimer = this.timeoutTimers.get(timerKey)
|
|
761
|
+
if (timeoutTimer) {
|
|
762
|
+
clearTimeout(timeoutTimer)
|
|
763
|
+
this.timeoutTimers.delete(timerKey)
|
|
764
|
+
void this.markTimeoutConsumed(reg.dbId, reg.tenantId)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (reg.debounceMs != null && reg.debounceMs > 0) {
|
|
768
|
+
const buffer = this.debounceBuffers.get(timerKey) ?? []
|
|
769
|
+
buffer.push(match.change)
|
|
770
|
+
this.debounceBuffers.set(timerKey, buffer)
|
|
771
|
+
|
|
772
|
+
// Preserve the latest runFinished status for debounced delivery
|
|
773
|
+
if (match.runFinishedStatus) {
|
|
774
|
+
this.debounceRunStatus.set(timerKey, match.runFinishedStatus)
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const existing = this.debounceTimers.get(timerKey)
|
|
778
|
+
if (existing) clearTimeout(existing)
|
|
779
|
+
|
|
780
|
+
const timer = setTimeout(() => {
|
|
781
|
+
this.debounceTimers.delete(timerKey)
|
|
782
|
+
const flushed = this.debounceBuffers.get(timerKey)
|
|
783
|
+
if (flushed && flushed.length > 0) {
|
|
784
|
+
this.debounceBuffers.delete(timerKey)
|
|
785
|
+
const runStatus = this.debounceRunStatus.get(timerKey)
|
|
786
|
+
this.debounceRunStatus.delete(timerKey)
|
|
787
|
+
this.deliverDebounce({
|
|
788
|
+
tenantId: reg.tenantId,
|
|
789
|
+
subscriberUrl: reg.subscriberUrl,
|
|
790
|
+
registrationDbId: reg.dbId,
|
|
791
|
+
sourceEventKey: flushed[flushed.length - 1]!.key,
|
|
792
|
+
wakeMessage: {
|
|
793
|
+
source: sourceUrl,
|
|
794
|
+
timeout: false,
|
|
795
|
+
changes: flushed,
|
|
796
|
+
},
|
|
797
|
+
runFinishedStatus: runStatus,
|
|
798
|
+
includeResponse: reg.includeResponse,
|
|
799
|
+
})
|
|
800
|
+
}
|
|
801
|
+
}, reg.debounceMs)
|
|
802
|
+
this.debounceTimers.set(timerKey, timer)
|
|
803
|
+
} else {
|
|
804
|
+
results.push({
|
|
805
|
+
tenantId: reg.tenantId,
|
|
806
|
+
subscriberUrl: reg.subscriberUrl,
|
|
807
|
+
registrationDbId: reg.dbId,
|
|
808
|
+
sourceEventKey: wakeSourceEventId(event),
|
|
809
|
+
wakeMessage: {
|
|
810
|
+
source: sourceUrl,
|
|
811
|
+
timeout: false,
|
|
812
|
+
changes: [match.change],
|
|
813
|
+
},
|
|
814
|
+
runFinishedStatus: match.runFinishedStatus,
|
|
815
|
+
includeResponse: reg.includeResponse,
|
|
816
|
+
})
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (reg.oneShot) {
|
|
820
|
+
toRemove.push(i)
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
for (let j = toRemove.length - 1; j >= 0; j--) {
|
|
825
|
+
const removed = regs.splice(toRemove[j]!, 1)
|
|
826
|
+
if (removed[0]) {
|
|
827
|
+
this.clearRegistrationState(removed[0])
|
|
828
|
+
this.timeoutDelivered.delete(removed[0].dbId)
|
|
829
|
+
void this.db
|
|
830
|
+
.delete(wakeRegistrations)
|
|
831
|
+
.where(
|
|
832
|
+
and(
|
|
833
|
+
eq(wakeRegistrations.tenantId, removed[0].tenantId),
|
|
834
|
+
eq(wakeRegistrations.id, removed[0].dbId)
|
|
835
|
+
)
|
|
836
|
+
)
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (regs.length === 0) {
|
|
840
|
+
this.registrationCache.delete(cacheKey)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return results
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/** Flush any pending debounce buffers for a subscriber and return them. */
|
|
847
|
+
flushDebounce(
|
|
848
|
+
subscriberUrl: string,
|
|
849
|
+
sourceUrl: string,
|
|
850
|
+
tenantId?: string
|
|
851
|
+
): WakeEvalResult | null {
|
|
852
|
+
const resolvedTenantId = this.resolveTenantId(tenantId)
|
|
853
|
+
const timerKeyPrefix = `${resolvedTenantId}:${subscriberUrl}:${sourceUrl}:`
|
|
854
|
+
const changes: Array<WakeEvalResult[`wakeMessage`][`changes`][number]> = []
|
|
855
|
+
|
|
856
|
+
for (const [timerKey, buffer] of this.debounceBuffers.entries()) {
|
|
857
|
+
if (!timerKey.startsWith(timerKeyPrefix)) continue
|
|
858
|
+
changes.push(...buffer)
|
|
859
|
+
this.debounceBuffers.delete(timerKey)
|
|
860
|
+
|
|
861
|
+
const timer = this.debounceTimers.get(timerKey)
|
|
862
|
+
if (timer) {
|
|
863
|
+
clearTimeout(timer)
|
|
864
|
+
this.debounceTimers.delete(timerKey)
|
|
865
|
+
}
|
|
866
|
+
this.debounceRunStatus.delete(timerKey)
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (changes.length === 0) return null
|
|
870
|
+
|
|
871
|
+
return {
|
|
872
|
+
tenantId: resolvedTenantId,
|
|
873
|
+
subscriberUrl,
|
|
874
|
+
registrationDbId: -1,
|
|
875
|
+
sourceEventKey: changes[changes.length - 1]!.key,
|
|
876
|
+
wakeMessage: {
|
|
877
|
+
source: sourceUrl,
|
|
878
|
+
timeout: false,
|
|
879
|
+
changes,
|
|
880
|
+
},
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
private matchCondition(
|
|
885
|
+
reg: WakeRegistration,
|
|
886
|
+
event: Record<string, unknown>
|
|
887
|
+
): {
|
|
888
|
+
change: {
|
|
889
|
+
collection: string
|
|
890
|
+
kind: `insert` | `update` | `delete`
|
|
891
|
+
key: string
|
|
892
|
+
}
|
|
893
|
+
runFinishedStatus?: `completed` | `failed`
|
|
894
|
+
} | null {
|
|
895
|
+
if (reg.condition === `runFinished`) {
|
|
896
|
+
if (event.type !== `run`) return null
|
|
897
|
+
const value = event.value as Record<string, unknown> | undefined
|
|
898
|
+
const headers = event.headers as Record<string, unknown> | undefined
|
|
899
|
+
const status = value?.status as string | undefined
|
|
900
|
+
const operation = headers?.operation as string | undefined
|
|
901
|
+
if (operation !== `update`) return null
|
|
902
|
+
if (status !== `completed` && status !== `failed`) return null
|
|
903
|
+
return {
|
|
904
|
+
change: {
|
|
905
|
+
collection: `runs`,
|
|
906
|
+
kind: `update`,
|
|
907
|
+
key: (event.key as string) || `run`,
|
|
908
|
+
},
|
|
909
|
+
runFinishedStatus: status,
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const condition = reg.condition
|
|
914
|
+
const eventType = event.type as string | undefined
|
|
915
|
+
const headers = event.headers as Record<string, unknown> | undefined
|
|
916
|
+
const operation = headers?.operation as string | undefined
|
|
917
|
+
if (!eventType) return null
|
|
918
|
+
|
|
919
|
+
if (condition.collections && condition.collections.length > 0) {
|
|
920
|
+
if (!condition.collections.includes(eventType)) return null
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const kind: `insert` | `update` | `delete` =
|
|
924
|
+
operation === `delete`
|
|
925
|
+
? `delete`
|
|
926
|
+
: operation === `update`
|
|
927
|
+
? `update`
|
|
928
|
+
: `insert`
|
|
929
|
+
|
|
930
|
+
if (
|
|
931
|
+
condition.ops &&
|
|
932
|
+
condition.ops.length > 0 &&
|
|
933
|
+
!condition.ops.includes(kind)
|
|
934
|
+
) {
|
|
935
|
+
return null
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return {
|
|
939
|
+
change: {
|
|
940
|
+
collection: eventType,
|
|
941
|
+
kind,
|
|
942
|
+
key: (event.key as string) || ``,
|
|
943
|
+
},
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|