@contember/engine-actions 2.1.0-beta.1 → 2.1.0-beta.2
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/development/index.cjs +10 -4
- package/dist/development/index.cjs.map +1 -1
- package/dist/development/index.js +10 -4
- package/dist/development/index.js.map +1 -1
- package/dist/development/src/ActionsExecutionContainerHookFactory.cjs +3 -2
- package/dist/development/src/ActionsExecutionContainerHookFactory.cjs.map +1 -1
- package/dist/development/src/ActionsExecutionContainerHookFactory.js +3 -2
- package/dist/development/src/ActionsExecutionContainerHookFactory.js.map +1 -1
- package/dist/development/src/dispatch/LazyDispatchWorker.cjs +67 -0
- package/dist/development/src/dispatch/LazyDispatchWorker.cjs.map +1 -0
- package/dist/development/src/dispatch/LazyDispatchWorker.js +67 -0
- package/dist/development/src/dispatch/LazyDispatchWorker.js.map +1 -0
- package/dist/development/src/triggers/TriggerPayloadPersister.cjs +32 -20
- package/dist/development/src/triggers/TriggerPayloadPersister.cjs.map +1 -1
- package/dist/development/src/triggers/TriggerPayloadPersister.js +32 -20
- package/dist/development/src/triggers/TriggerPayloadPersister.js.map +1 -1
- package/dist/production/index.cjs +10 -4
- package/dist/production/index.cjs.map +1 -1
- package/dist/production/index.js +10 -4
- package/dist/production/index.js.map +1 -1
- package/dist/production/src/ActionsExecutionContainerHookFactory.cjs +3 -2
- package/dist/production/src/ActionsExecutionContainerHookFactory.cjs.map +1 -1
- package/dist/production/src/ActionsExecutionContainerHookFactory.js +3 -2
- package/dist/production/src/ActionsExecutionContainerHookFactory.js.map +1 -1
- package/dist/production/src/dispatch/LazyDispatchWorker.cjs +67 -0
- package/dist/production/src/dispatch/LazyDispatchWorker.cjs.map +1 -0
- package/dist/production/src/dispatch/LazyDispatchWorker.js +67 -0
- package/dist/production/src/dispatch/LazyDispatchWorker.js.map +1 -0
- package/dist/production/src/triggers/TriggerPayloadPersister.cjs +32 -20
- package/dist/production/src/triggers/TriggerPayloadPersister.cjs.map +1 -1
- package/dist/production/src/triggers/TriggerPayloadPersister.js +32 -20
- package/dist/production/src/triggers/TriggerPayloadPersister.js.map +1 -1
- package/dist/tests/cases/unit/lazyDispatchWorker.test.d.ts +2 -0
- package/dist/tests/cases/unit/lazyDispatchWorker.test.d.ts.map +1 -0
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/dist/types/ActionsExecutionContainerHookFactory.d.ts.map +1 -1
- package/dist/types/dispatch/LazyDispatchWorker.d.ts +11 -0
- package/dist/types/dispatch/LazyDispatchWorker.d.ts.map +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/triggers/TriggerPayloadPersister.d.ts +3 -2
- package/dist/types/triggers/TriggerPayloadPersister.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/ActionsExecutionContainerHookFactory.ts +5 -1
- package/src/dispatch/LazyDispatchWorker.ts +72 -0
- package/src/index.ts +11 -5
- package/src/triggers/TriggerPayloadPersister.ts +20 -7
- package/tests/cases/unit/lazyDispatchWorker.test.ts +226 -0
- package/vitest.config.ts +0 -3
package/package.json
CHANGED
|
@@ -14,7 +14,10 @@ export class ActionsExecutionContainerHookFactory {
|
|
|
14
14
|
return builder => {
|
|
15
15
|
return builder.setupService(
|
|
16
16
|
'mapperFactory',
|
|
17
|
-
(
|
|
17
|
+
(
|
|
18
|
+
mapperFactory,
|
|
19
|
+
{ whereBuilder, schema, pathFactory, systemSchema, providers, stage, project, schemaMeta, joinBuilder, userInfo, triggeredActionsCollector },
|
|
20
|
+
) => {
|
|
18
21
|
mapperFactory.hooks.push(mapper => {
|
|
19
22
|
const triggerPayloadPersister = new TriggerPayloadPersister(
|
|
20
23
|
mapper,
|
|
@@ -25,6 +28,7 @@ export class ActionsExecutionContainerHookFactory {
|
|
|
25
28
|
schemaMeta.id,
|
|
26
29
|
mapper.identityId,
|
|
27
30
|
userInfo,
|
|
31
|
+
triggeredActionsCollector,
|
|
28
32
|
)
|
|
29
33
|
const triggerPayloadBuilder = new TriggerPayloadBuilder(mapper)
|
|
30
34
|
const payloadManager = new TriggerPayloadManager(triggerPayloadBuilder, triggerPayloadPersister)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Runnable, RunnableArgs, Running, Supervisor, SupervisorOptions } from '@contember/engine-common'
|
|
2
|
+
import { ProjectGroupContainerResolver } from '@contember/engine-http'
|
|
3
|
+
import { DispatchWorkerSupervisorFactory } from './DispatchWorkerSupervisor'
|
|
4
|
+
|
|
5
|
+
export class LazyDispatchWorker implements Runnable {
|
|
6
|
+
constructor(
|
|
7
|
+
private readonly projectGroupContainerResolver: ProjectGroupContainerResolver,
|
|
8
|
+
private readonly supervisorFactory: DispatchWorkerSupervisorFactory,
|
|
9
|
+
private readonly supervisorOptions: SupervisorOptions = { startupMax: 10 },
|
|
10
|
+
) {
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public async run({ logger, onError }: RunnableArgs): Promise<Running> {
|
|
14
|
+
const running = new Map<string | undefined, Promise<Running | undefined>>()
|
|
15
|
+
let aborted = false
|
|
16
|
+
|
|
17
|
+
const unlisten = this.projectGroupContainerResolver.on('create', ({ container, slug }) => {
|
|
18
|
+
const groupLabel = slug ?? '<default>'
|
|
19
|
+
const childLogger = logger.child({ projectGroup: groupLabel })
|
|
20
|
+
childLogger.debug(`Starting dispatch worker for project group ${groupLabel}`)
|
|
21
|
+
|
|
22
|
+
const supervised = new Supervisor(
|
|
23
|
+
this.supervisorFactory.create(container),
|
|
24
|
+
this.supervisorOptions,
|
|
25
|
+
)
|
|
26
|
+
const runningPromise: Promise<Running | undefined> = supervised.run({
|
|
27
|
+
logger: childLogger,
|
|
28
|
+
onError: e => {
|
|
29
|
+
if (aborted) {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
childLogger.error(e, { message: `Dispatch worker for project group ${groupLabel} crashed` })
|
|
33
|
+
onError(e)
|
|
34
|
+
},
|
|
35
|
+
}).catch(e => {
|
|
36
|
+
if (!aborted) {
|
|
37
|
+
childLogger.error(e, { message: `Failed to start dispatch worker for project group ${groupLabel}` })
|
|
38
|
+
onError(e)
|
|
39
|
+
}
|
|
40
|
+
return undefined
|
|
41
|
+
})
|
|
42
|
+
running.set(slug, runningPromise)
|
|
43
|
+
|
|
44
|
+
return () => {
|
|
45
|
+
running.delete(slug)
|
|
46
|
+
;(async () => {
|
|
47
|
+
try {
|
|
48
|
+
const r = await runningPromise
|
|
49
|
+
await r?.end()
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
})()
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
end: async () => {
|
|
58
|
+
aborted = true
|
|
59
|
+
unlisten()
|
|
60
|
+
const all = [...running.values()]
|
|
61
|
+
running.clear()
|
|
62
|
+
await Promise.allSettled(all.map(async p => {
|
|
63
|
+
try {
|
|
64
|
+
const r = await p
|
|
65
|
+
await r?.end()
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}))
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { WebhookTargetHandler } from './dispatch/WebhookTargetHandler'
|
|
|
21
21
|
import { ActionsContextResolver } from './graphql/http/ActionsContextResolver'
|
|
22
22
|
import { ActionsWebsocketControllerFactory } from './graphql/http/ActionsWebsocketControllerFactory'
|
|
23
23
|
import { DispatchWorkerSupervisorFactory } from './dispatch/DispatchWorkerSupervisor'
|
|
24
|
+
import { LazyDispatchWorker } from './dispatch/LazyDispatchWorker'
|
|
24
25
|
import { ProjectDispatcherFactory } from './dispatch/ProjectDispatcher'
|
|
25
26
|
import { VariablesQueryResolver } from './graphql/resolvers/query/VariablesQueryResolver'
|
|
26
27
|
import { VariablesManager } from './model/VariablesManager'
|
|
@@ -108,11 +109,16 @@ export default class ActionsPlugin implements Plugin {
|
|
|
108
109
|
it.addWebsocketRoute('actions', '/actions/_worker', actionsWebsocketMiddlewareFactory.create())
|
|
109
110
|
},
|
|
110
111
|
)
|
|
111
|
-
.setupService(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
.setupService(
|
|
113
|
+
'applicationWorkers',
|
|
114
|
+
(it, { actions_dispatchWorkerSupervisorFactory, projectGroupContainer, projectGroupContainerResolver, serverConfig }) => {
|
|
115
|
+
if (!(serverConfig as any).projectGroup) {
|
|
116
|
+
it.registerWorker(this.name, actions_dispatchWorkerSupervisorFactory.create(projectGroupContainer))
|
|
117
|
+
} else {
|
|
118
|
+
it.registerWorker(this.name, new LazyDispatchWorker(projectGroupContainerResolver, actions_dispatchWorkerSupervisorFactory))
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
) as unknown as MasterContainerBuilder
|
|
116
122
|
}
|
|
117
123
|
return hook
|
|
118
124
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Client, InsertBuilder } from '@contember/database'
|
|
2
|
-
import { Mapper } from '@contember/engine-content-api'
|
|
2
|
+
import { Mapper, TriggeredActionEvent, TriggeredActionsCollector } from '@contember/engine-content-api'
|
|
3
3
|
import { Actions, ActionsPayload } from '@contember/schema'
|
|
4
4
|
import { EventRow } from '../model/types'
|
|
5
5
|
import { notify } from '../utils/notifyChannel'
|
|
@@ -20,6 +20,7 @@ export class TriggerPayloadPersister {
|
|
|
20
20
|
private readonly schemaId: number | undefined,
|
|
21
21
|
private readonly identityId: string,
|
|
22
22
|
private readonly userInfo: { ipAddress: string | null; userAgent: string | null },
|
|
23
|
+
private readonly triggeredActionsCollector: TriggeredActionsCollector | undefined,
|
|
23
24
|
) {
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -28,13 +29,17 @@ export class TriggerPayloadPersister {
|
|
|
28
29
|
if (!this.schemaId) {
|
|
29
30
|
throw new Error('Schema id is not set')
|
|
30
31
|
}
|
|
32
|
+
const collector = this.triggeredActionsCollector
|
|
31
33
|
for (let i = 0; i < payloads.length; i += chunkSize) {
|
|
32
34
|
const chunk = payloads.slice(i, i + chunkSize)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
.
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
const collected: TriggeredActionEvent[] | undefined = collector ? [] : undefined
|
|
36
|
+
const rows = chunk.map((it): EventRowToInsert => {
|
|
37
|
+
const id = this.providers.uuid()
|
|
38
|
+
const transactionId = this.mapper.transactionId
|
|
39
|
+
collected?.push({ id, trigger: trigger.name, target: trigger.target, transactionId })
|
|
40
|
+
return {
|
|
41
|
+
id,
|
|
42
|
+
transaction_id: transactionId,
|
|
38
43
|
created_at: 'now',
|
|
39
44
|
visible_at: 'now',
|
|
40
45
|
num_retries: 0,
|
|
@@ -50,8 +55,16 @@ export class TriggerPayloadPersister {
|
|
|
50
55
|
identity_id: this.identityId,
|
|
51
56
|
ip_address: this.userInfo.ipAddress,
|
|
52
57
|
user_agent: this.userInfo.userAgent,
|
|
53
|
-
}
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
await InsertBuilder.create()
|
|
61
|
+
.into('actions_event')
|
|
62
|
+
.values(rows)
|
|
54
63
|
.execute(this.client)
|
|
64
|
+
|
|
65
|
+
if (collected && collected.length > 0) {
|
|
66
|
+
collector!.add(collected)
|
|
67
|
+
}
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
await notify(this.client, this.projectSlug)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { Runnable, RunnableArgs, Running } from '@contember/engine-common'
|
|
3
|
+
import { createLogger, TestLoggerHandler } from '@contember/logger'
|
|
4
|
+
import { LazyDispatchWorker } from '../../../src/dispatch/LazyDispatchWorker'
|
|
5
|
+
import { DispatchWorkerSupervisorFactory } from '../../../src/dispatch/DispatchWorkerSupervisor'
|
|
6
|
+
import { ProjectGroupContainerResolver } from '@contember/engine-http'
|
|
7
|
+
import { ProjectGroupContainer } from '@contember/engine-http'
|
|
8
|
+
|
|
9
|
+
const silentLogger = createLogger(new TestLoggerHandler())
|
|
10
|
+
|
|
11
|
+
const tick = () => new Promise<void>(r => setImmediate(r))
|
|
12
|
+
|
|
13
|
+
const waitFor = async (cond: () => boolean, attempts = 50) => {
|
|
14
|
+
for (let i = 0; i < attempts; i++) {
|
|
15
|
+
if (cond()) return
|
|
16
|
+
await tick()
|
|
17
|
+
}
|
|
18
|
+
throw new Error('waitFor: condition not met')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type FakeRunnable = Runnable & {
|
|
22
|
+
id: string
|
|
23
|
+
startCount: number
|
|
24
|
+
endCount: number
|
|
25
|
+
failOnRun?: () => Error | undefined
|
|
26
|
+
emitError?: (e: any) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const createFakeSupervisor = (id: string, opts: { failOnRun?: () => Error | undefined } = {}): FakeRunnable => {
|
|
30
|
+
const fake: FakeRunnable = {
|
|
31
|
+
id,
|
|
32
|
+
startCount: 0,
|
|
33
|
+
endCount: 0,
|
|
34
|
+
failOnRun: opts.failOnRun,
|
|
35
|
+
async run(args: RunnableArgs): Promise<Running> {
|
|
36
|
+
fake.startCount++
|
|
37
|
+
const failure = fake.failOnRun?.()
|
|
38
|
+
if (failure) {
|
|
39
|
+
throw failure
|
|
40
|
+
}
|
|
41
|
+
fake.emitError = args.onError
|
|
42
|
+
return {
|
|
43
|
+
end: async () => {
|
|
44
|
+
fake.endCount++
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
return fake
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type FakeResolver = Pick<ProjectGroupContainerResolver, 'on'> & {
|
|
53
|
+
fire: (slug: string | undefined) => (() => void) | undefined
|
|
54
|
+
listenerCount: () => number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const createFakeResolver = (): FakeResolver => {
|
|
58
|
+
const listeners: Array<(args: { container: ProjectGroupContainer; slug: string | undefined }) => void | (() => void)> = []
|
|
59
|
+
return {
|
|
60
|
+
on(_event, cb) {
|
|
61
|
+
listeners.push(cb as any)
|
|
62
|
+
return () => {
|
|
63
|
+
const idx = listeners.indexOf(cb as any)
|
|
64
|
+
if (idx >= 0) listeners.splice(idx, 1)
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
fire(slug) {
|
|
68
|
+
const fakeContainer = { slug } as unknown as ProjectGroupContainer
|
|
69
|
+
const cleanups = listeners.map(it => it({ container: fakeContainer, slug }))
|
|
70
|
+
return () => cleanups.forEach(it => it?.())
|
|
71
|
+
},
|
|
72
|
+
listenerCount: () => listeners.length,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const createFakeFactory = (registry: FakeRunnable[]): DispatchWorkerSupervisorFactory => {
|
|
77
|
+
let counter = 0
|
|
78
|
+
return {
|
|
79
|
+
create(_container: ProjectGroupContainer): any {
|
|
80
|
+
const fake = createFakeSupervisor(`fake-${counter++}`)
|
|
81
|
+
registry.push(fake)
|
|
82
|
+
return fake
|
|
83
|
+
},
|
|
84
|
+
} as any
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const recordOnError = () => {
|
|
88
|
+
const errors: any[] = []
|
|
89
|
+
return { errors, onError: (e: any) => errors.push(e) }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe('LazyDispatchWorker', () => {
|
|
93
|
+
test('does not spawn supervisor before any project group is created', async () => {
|
|
94
|
+
const resolver = createFakeResolver()
|
|
95
|
+
const supervisors: FakeRunnable[] = []
|
|
96
|
+
const worker = new LazyDispatchWorker(resolver as any, createFakeFactory(supervisors))
|
|
97
|
+
const { onError } = recordOnError()
|
|
98
|
+
|
|
99
|
+
const running = await worker.run({ logger: silentLogger, onError })
|
|
100
|
+
|
|
101
|
+
expect(supervisors).toHaveLength(0)
|
|
102
|
+
expect(resolver.listenerCount()).toBe(1)
|
|
103
|
+
|
|
104
|
+
await running.end()
|
|
105
|
+
expect(resolver.listenerCount()).toBe(0)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('spawns supervisor when project group is created', async () => {
|
|
109
|
+
const resolver = createFakeResolver()
|
|
110
|
+
const supervisors: FakeRunnable[] = []
|
|
111
|
+
const worker = new LazyDispatchWorker(resolver as any, createFakeFactory(supervisors))
|
|
112
|
+
const { onError, errors } = recordOnError()
|
|
113
|
+
|
|
114
|
+
const running = await worker.run({ logger: silentLogger, onError })
|
|
115
|
+
|
|
116
|
+
resolver.fire('tenantA')
|
|
117
|
+
await waitFor(() => supervisors.length === 1 && supervisors[0].startCount === 1)
|
|
118
|
+
|
|
119
|
+
expect(supervisors[0].startCount).toBe(1)
|
|
120
|
+
expect(supervisors[0].endCount).toBe(0)
|
|
121
|
+
expect(errors).toHaveLength(0)
|
|
122
|
+
|
|
123
|
+
await running.end()
|
|
124
|
+
expect(supervisors[0].endCount).toBe(1)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('spawns one supervisor per project group, isolated', async () => {
|
|
128
|
+
const resolver = createFakeResolver()
|
|
129
|
+
const supervisors: FakeRunnable[] = []
|
|
130
|
+
const worker = new LazyDispatchWorker(resolver as any, createFakeFactory(supervisors))
|
|
131
|
+
const { onError } = recordOnError()
|
|
132
|
+
|
|
133
|
+
const running = await worker.run({ logger: silentLogger, onError })
|
|
134
|
+
|
|
135
|
+
resolver.fire('tenantA')
|
|
136
|
+
resolver.fire('tenantB')
|
|
137
|
+
await waitFor(() => supervisors.length === 2 && supervisors.every(it => it.startCount === 1))
|
|
138
|
+
|
|
139
|
+
await running.end()
|
|
140
|
+
expect(supervisors.map(it => it.endCount)).toEqual([1, 1])
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('container invalidation ends only that supervisor', async () => {
|
|
144
|
+
const resolver = createFakeResolver()
|
|
145
|
+
const supervisors: FakeRunnable[] = []
|
|
146
|
+
const worker = new LazyDispatchWorker(resolver as any, createFakeFactory(supervisors))
|
|
147
|
+
const { onError } = recordOnError()
|
|
148
|
+
|
|
149
|
+
const running = await worker.run({ logger: silentLogger, onError })
|
|
150
|
+
|
|
151
|
+
const cleanupA = resolver.fire('tenantA')!
|
|
152
|
+
resolver.fire('tenantB')
|
|
153
|
+
await waitFor(() => supervisors.length === 2 && supervisors.every(it => it.startCount === 1))
|
|
154
|
+
|
|
155
|
+
cleanupA()
|
|
156
|
+
await waitFor(() => supervisors[0].endCount === 1)
|
|
157
|
+
|
|
158
|
+
expect(supervisors[0].endCount).toBe(1)
|
|
159
|
+
expect(supervisors[1].endCount).toBe(0)
|
|
160
|
+
|
|
161
|
+
await running.end()
|
|
162
|
+
expect(supervisors[1].endCount).toBe(1)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('end() unsubscribes listener, no further supervisors are spawned', async () => {
|
|
166
|
+
const resolver = createFakeResolver()
|
|
167
|
+
const supervisors: FakeRunnable[] = []
|
|
168
|
+
const worker = new LazyDispatchWorker(resolver as any, createFakeFactory(supervisors))
|
|
169
|
+
const { onError } = recordOnError()
|
|
170
|
+
|
|
171
|
+
const running = await worker.run({ logger: silentLogger, onError })
|
|
172
|
+
expect(resolver.listenerCount()).toBe(1)
|
|
173
|
+
|
|
174
|
+
await running.end()
|
|
175
|
+
expect(resolver.listenerCount()).toBe(0)
|
|
176
|
+
|
|
177
|
+
resolver.fire('tenantLate')
|
|
178
|
+
await tick()
|
|
179
|
+
expect(supervisors).toHaveLength(0)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('startup failure of a tenant supervisor propagates to onError once retries are exhausted', async () => {
|
|
183
|
+
const resolver = createFakeResolver()
|
|
184
|
+
const registry: FakeRunnable[] = []
|
|
185
|
+
const factory: DispatchWorkerSupervisorFactory = {
|
|
186
|
+
create(_container: ProjectGroupContainer): any {
|
|
187
|
+
const fake = createFakeSupervisor(`fake-${registry.length}`, {
|
|
188
|
+
failOnRun: () => new Error('boom'),
|
|
189
|
+
})
|
|
190
|
+
registry.push(fake)
|
|
191
|
+
return fake
|
|
192
|
+
},
|
|
193
|
+
} as any
|
|
194
|
+
const worker = new LazyDispatchWorker(resolver as any, factory, { startupMax: 0, minBackoff: 1 })
|
|
195
|
+
const { onError, errors } = recordOnError()
|
|
196
|
+
|
|
197
|
+
const running = await worker.run({ logger: silentLogger, onError })
|
|
198
|
+
|
|
199
|
+
resolver.fire('tenantBroken')
|
|
200
|
+
await waitFor(() => errors.length > 0, 200)
|
|
201
|
+
|
|
202
|
+
expect(errors[0]).toBeInstanceOf(Error)
|
|
203
|
+
expect((errors[0] as Error).message).toContain('boom')
|
|
204
|
+
|
|
205
|
+
await running.end()
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('errors after end() are swallowed', async () => {
|
|
209
|
+
const resolver = createFakeResolver()
|
|
210
|
+
const supervisors: FakeRunnable[] = []
|
|
211
|
+
const worker = new LazyDispatchWorker(resolver as any, createFakeFactory(supervisors))
|
|
212
|
+
const { onError, errors } = recordOnError()
|
|
213
|
+
|
|
214
|
+
const running = await worker.run({ logger: silentLogger, onError })
|
|
215
|
+
|
|
216
|
+
resolver.fire('tenantA')
|
|
217
|
+
await waitFor(() => supervisors.length === 1 && supervisors[0].emitError !== undefined)
|
|
218
|
+
|
|
219
|
+
await running.end()
|
|
220
|
+
expect(supervisors[0].endCount).toBe(1)
|
|
221
|
+
|
|
222
|
+
supervisors[0].emitError!(new Error('after end'))
|
|
223
|
+
await tick()
|
|
224
|
+
expect(errors).toHaveLength(0)
|
|
225
|
+
})
|
|
226
|
+
})
|
package/vitest.config.ts
DELETED