@open-mercato/cli 0.6.8-develop.7015.1.af90a2ddc7 → 0.6.8-develop.7019.1.f4c01c4b5c
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/.turbo/turbo-build.log +5 -1
- package/dist/agentic/guides/module-facts.json +3966 -285
- package/dist/agentic/guides/module-facts.v2.json +3966 -285
- package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
- package/dist/agentic/guides/modules/api_docs/index.md +1 -1
- package/dist/agentic/guides/modules/api_keys/di-registrations-rich.md +11 -0
- package/dist/agentic/guides/modules/api_keys/di-service-tokens.md +11 -0
- package/dist/agentic/guides/modules/api_keys/exact-override-targets.md +1 -0
- package/dist/agentic/guides/modules/api_keys/index.md +4 -2
- package/dist/agentic/guides/modules/attachments/di-registrations-rich.md +6 -5
- package/dist/agentic/guides/modules/attachments/di-service-tokens.md +5 -4
- package/dist/agentic/guides/modules/attachments/exact-override-targets.md +6 -5
- package/dist/agentic/guides/modules/attachments/index.md +2 -2
- package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
- package/dist/agentic/guides/modules/auth/di-registrations-rich.md +3 -2
- package/dist/agentic/guides/modules/auth/di-service-tokens.md +3 -2
- package/dist/agentic/guides/modules/auth/exact-override-targets.md +3 -2
- package/dist/agentic/guides/modules/auth/index.md +1 -1
- package/dist/agentic/guides/modules/business_rules/index.md +1 -1
- package/dist/agentic/guides/modules/catalog/incoming-installed-contributions.md +3 -0
- package/dist/agentic/guides/modules/catalog/index.md +2 -2
- package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
- package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
- package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
- package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
- package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
- package/dist/agentic/guides/modules/checkout/index.md +1 -1
- package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
- package/dist/agentic/guides/modules/configs/index.md +1 -1
- package/dist/agentic/guides/modules/content/index.md +1 -1
- package/dist/agentic/guides/modules/currencies/index.md +1 -1
- package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
- package/dist/agentic/guides/modules/customers/active-extension-bindings.md +3 -0
- package/dist/agentic/guides/modules/customers/incoming-installed-contributions.md +7 -0
- package/dist/agentic/guides/modules/customers/index.md +3 -3
- package/dist/agentic/guides/modules/dashboards/index.md +1 -1
- package/dist/agentic/guides/modules/data_sync/exact-override-targets.md +2 -2
- package/dist/agentic/guides/modules/data_sync/index.md +1 -1
- package/dist/agentic/guides/modules/data_sync/workers.md +2 -2
- package/dist/agentic/guides/modules/design_system/index.md +1 -1
- package/dist/agentic/guides/modules/devices/index.md +1 -1
- package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
- package/dist/agentic/guides/modules/directory/di-registrations-rich.md +12 -0
- package/dist/agentic/guides/modules/directory/di-service-tokens.md +12 -0
- package/dist/agentic/guides/modules/directory/exact-override-targets.md +2 -0
- package/dist/agentic/guides/modules/directory/index.md +5 -3
- package/dist/agentic/guides/modules/documents/acl-features.md +17 -0
- package/dist/agentic/guides/modules/documents/backend-pages.md +13 -0
- package/dist/agentic/guides/modules/documents/contribution-resolutions.md +27 -0
- package/dist/agentic/guides/modules/documents/di-registrations-rich.md +21 -0
- package/dist/agentic/guides/modules/documents/domain-commands.md +37 -0
- package/dist/agentic/guides/modules/documents/encryption.md +11 -0
- package/dist/agentic/guides/modules/documents/entities.md +21 -0
- package/dist/agentic/guides/modules/documents/events.md +25 -0
- package/dist/agentic/guides/modules/documents/exact-override-targets.md +39 -0
- package/dist/agentic/guides/modules/documents/index.md +25 -0
- package/dist/agentic/guides/modules/documents/notifications.md +13 -0
- package/dist/agentic/guides/modules/documents/owned-contract-module-metadata.md +11 -0
- package/dist/agentic/guides/modules/documents/setup.md +11 -0
- package/dist/agentic/guides/modules/documents/umes-contributions.md +27 -0
- package/dist/agentic/guides/modules/documents/umes-hosts.md +36 -0
- package/dist/agentic/guides/modules/entities/index.md +1 -1
- package/dist/agentic/guides/modules/eudr/index.md +1 -1
- package/dist/agentic/guides/modules/events/index.md +1 -1
- package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
- package/dist/agentic/guides/modules/generators/index.md +1 -1
- package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
- package/dist/agentic/guides/modules/integrations/index.md +1 -1
- package/dist/agentic/guides/modules/messages/index.md +1 -1
- package/dist/agentic/guides/modules/notifications/index.md +1 -1
- package/dist/agentic/guides/modules/onboarding/index.md +1 -1
- package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
- package/dist/agentic/guides/modules/perspectives/index.md +1 -1
- package/dist/agentic/guides/modules/planner/index.md +1 -1
- package/dist/agentic/guides/modules/portal/index.md +1 -1
- package/dist/agentic/guides/modules/progress/index.md +1 -1
- package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
- package/dist/agentic/guides/modules/query_index/index.md +1 -1
- package/dist/agentic/guides/modules/record_locks/index.md +1 -1
- package/dist/agentic/guides/modules/resources/index.md +1 -1
- package/dist/agentic/guides/modules/sales/incoming-installed-contributions.md +2 -0
- package/dist/agentic/guides/modules/sales/index.md +1 -1
- package/dist/agentic/guides/modules/scheduler/index.md +1 -1
- package/dist/agentic/guides/modules/search/index.md +1 -1
- package/dist/agentic/guides/modules/security/index.md +1 -1
- package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
- package/dist/agentic/guides/modules/sso/index.md +1 -1
- package/dist/agentic/guides/modules/staff/index.md +1 -1
- package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
- package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
- package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
- package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
- package/dist/agentic/guides/modules/translations/index.md +1 -1
- package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
- package/dist/agentic/guides/modules/webhooks/index.md +1 -1
- package/dist/agentic/guides/modules/wms/index.md +1 -1
- package/dist/agentic/guides/modules/workflows/index.md +1 -1
- package/dist/agentic/guides/reference-module-facts.json +1 -1
- package/dist/agentic/guides/upstream/manifest.json +1 -1
- package/dist/agentic/shared/AGENTS.md.template +3 -3
- package/dist/agentic/shared/ai/harness/README.md +2 -2
- package/dist/agentic/shared/ai/harness/RELEASE.md +4 -4
- package/dist/agentic/shared/ai/harness/cases.json +83 -0
- package/dist/agentic/shared/ai/harness/cases.schema.json +4 -4
- package/dist/agentic/shared/ai/harness/validators.json +1 -1
- package/dist/lib/generators/module-registry.js +77 -11
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/mercato.js +14 -0
- package/dist/mercato.js.map +2 -2
- package/package.json +6 -6
- package/src/__tests__/resolve-queue-abandon-hook.test.ts +39 -0
- package/src/lib/generators/__tests__/generated-registry-imports.test.ts +128 -0
- package/src/lib/generators/__tests__/metadata-extraction.test.ts +44 -0
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +9 -5
- package/src/lib/generators/module-registry.ts +118 -5
- package/src/mercato.ts +27 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cli",
|
|
3
|
-
"version": "0.6.8-develop.
|
|
3
|
+
"version": "0.6.8-develop.7019.1.f4c01c4b5c",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
"@mikro-orm/decorators": "^7.1.8",
|
|
61
61
|
"@mikro-orm/migrations": "^7.1.8",
|
|
62
62
|
"@mikro-orm/postgresql": "^7.1.8",
|
|
63
|
-
"@open-mercato/queue": "0.6.8-develop.
|
|
64
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
65
|
-
"@open-mercato/telemetry": "0.6.8-develop.
|
|
63
|
+
"@open-mercato/queue": "0.6.8-develop.7019.1.f4c01c4b5c",
|
|
64
|
+
"@open-mercato/shared": "0.6.8-develop.7019.1.f4c01c4b5c",
|
|
65
|
+
"@open-mercato/telemetry": "0.6.8-develop.7019.1.f4c01c4b5c",
|
|
66
66
|
"cross-spawn": "^7.0.6",
|
|
67
67
|
"pg": "8.22.0",
|
|
68
68
|
"semver": "^7.8.5",
|
|
@@ -73,10 +73,10 @@
|
|
|
73
73
|
"typescript-js": "npm:typescript@6.0.3"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
76
|
+
"@open-mercato/shared": "0.6.8-develop.7019.1.f4c01c4b5c"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
79
|
+
"@open-mercato/shared": "0.6.8-develop.7019.1.f4c01c4b5c",
|
|
80
80
|
"@types/jest": "^30.0.0",
|
|
81
81
|
"jest": "^30.4.2",
|
|
82
82
|
"ts-jest": "^29.4.12"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { resolveQueueAbandonHook } from '../mercato'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Covers the CLI's per-queue pick, which is the last link between worker metadata and the queue the
|
|
5
|
+
* worker runs on. The warning exists because two handlers on one queue each expecting to report is a
|
|
6
|
+
* wiring mistake, and a silent first-wins would hide it.
|
|
7
|
+
*/
|
|
8
|
+
describe('resolveQueueAbandonHook', () => {
|
|
9
|
+
const hookA = jest.fn(async () => {})
|
|
10
|
+
const hookB = jest.fn(async () => {})
|
|
11
|
+
|
|
12
|
+
it('returns undefined when no worker declares one', () => {
|
|
13
|
+
const warn = jest.fn()
|
|
14
|
+
expect(resolveQueueAbandonHook('q', [{ id: 'a' }, { id: 'b' }], warn)).toBeUndefined()
|
|
15
|
+
expect(warn).not.toHaveBeenCalled()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('returns the single declared callback without warning', () => {
|
|
19
|
+
const warn = jest.fn()
|
|
20
|
+
expect(resolveQueueAbandonHook('q', [{ id: 'a' }, { id: 'b', onJobAbandoned: hookA }], warn)).toBe(hookA)
|
|
21
|
+
expect(warn).not.toHaveBeenCalled()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('keeps the first and warns, naming the queue and every declaring worker', () => {
|
|
25
|
+
const warn = jest.fn()
|
|
26
|
+
const picked = resolveQueueAbandonHook(
|
|
27
|
+
'data-sync-import',
|
|
28
|
+
[{ id: 'first', onJobAbandoned: hookA }, { id: 'second', onJobAbandoned: hookB }],
|
|
29
|
+
warn,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
expect(picked).toBe(hookA)
|
|
33
|
+
expect(warn).toHaveBeenCalledTimes(1)
|
|
34
|
+
const message = warn.mock.calls[0][0] as string
|
|
35
|
+
expect(message).toContain('data-sync-import')
|
|
36
|
+
expect(message).toContain('first')
|
|
37
|
+
expect(message).toContain('second')
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import type { PackageResolver, ModuleEntry } from '../../resolver'
|
|
5
|
+
import { generateModuleRegistry, generateModuleRegistryApp, generateModuleRegistryCli } from '../module-registry'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The generator's output, not its helpers.
|
|
9
|
+
*
|
|
10
|
+
* A worker declaring `metadata.onJobAbandoned` makes the registry emit
|
|
11
|
+
* `createLazyModuleWorkerAbandonHook(...)`, and the import for it used to be opted into per render
|
|
12
|
+
* site. Forgetting it at one of the six sites produced a file referencing an undeclared name, which
|
|
13
|
+
* type-checks nowhere and surfaces only at `build:app` — every unit test stayed green because no
|
|
14
|
+
* fixture declared such a worker.
|
|
15
|
+
*
|
|
16
|
+
* These assertions are driven off the files the generators actually wrote, so a renderer added later
|
|
17
|
+
* is covered without anyone remembering to extend this test.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
let tmpDir: string
|
|
21
|
+
|
|
22
|
+
function touchFile(filePath: string, content = ''): void {
|
|
23
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
24
|
+
fs.writeFileSync(filePath, content)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createMockResolver(dir: string, enabled: ModuleEntry[]): PackageResolver {
|
|
28
|
+
const outputDir = path.join(dir, 'output', 'generated')
|
|
29
|
+
fs.mkdirSync(outputDir, { recursive: true })
|
|
30
|
+
return {
|
|
31
|
+
isMonorepo: () => true,
|
|
32
|
+
getRootDir: () => dir,
|
|
33
|
+
getAppDir: () => path.join(dir, 'app'),
|
|
34
|
+
getOutputDir: () => outputDir,
|
|
35
|
+
getModulesConfigPath: () => path.join(dir, 'app', 'src', 'modules.ts'),
|
|
36
|
+
discoverPackages: () => [],
|
|
37
|
+
loadEnabledModules: () => enabled,
|
|
38
|
+
getModulePaths: (entry: ModuleEntry) => ({
|
|
39
|
+
appBase: path.join(dir, 'app', 'src', 'modules', entry.id),
|
|
40
|
+
pkgBase: path.join(dir, 'packages', 'core', 'src', 'modules', entry.id),
|
|
41
|
+
}),
|
|
42
|
+
getModuleImportBase: (entry: ModuleEntry) => ({
|
|
43
|
+
appBase: `@/modules/${entry.id}`,
|
|
44
|
+
pkgBase: `@open-mercato/core/modules/${entry.id}`,
|
|
45
|
+
}),
|
|
46
|
+
getPackageOutputDir: () => outputDir,
|
|
47
|
+
getPackageRoot: () => path.join(dir, 'packages', 'core'),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function scaffoldWorkerModule(dir: string, modId: string, workerSource: string): void {
|
|
52
|
+
touchFile(path.join(dir, 'packages', 'core', 'src', 'modules', modId, 'workers', 'sync.ts'), workerSource)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function generatedFiles(dir: string): Array<{ name: string; content: string }> {
|
|
56
|
+
const outputDir = path.join(dir, 'output', 'generated')
|
|
57
|
+
if (!fs.existsSync(outputDir)) return []
|
|
58
|
+
return fs.readdirSync(outputDir)
|
|
59
|
+
.filter((name) => name.endsWith('.ts'))
|
|
60
|
+
.map((name) => ({ name, content: fs.readFileSync(path.join(outputDir, name), 'utf8') }))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const ABANDON_HOOK_FACTORY = 'createLazyModuleWorkerAbandonHook'
|
|
64
|
+
|
|
65
|
+
const WORKER_WITH_HOOK = `
|
|
66
|
+
import { failAbandonedRun } from '../lib/abandoned-run'
|
|
67
|
+
export const metadata = {
|
|
68
|
+
queue: 'demo-queue',
|
|
69
|
+
id: 'demo:worker',
|
|
70
|
+
concurrency: 2,
|
|
71
|
+
onJobAbandoned: failAbandonedRun,
|
|
72
|
+
}
|
|
73
|
+
export default async function handle() {}
|
|
74
|
+
`
|
|
75
|
+
|
|
76
|
+
const WORKER_WITHOUT_HOOK = `
|
|
77
|
+
export const metadata = {
|
|
78
|
+
queue: 'demo-queue',
|
|
79
|
+
id: 'demo:worker',
|
|
80
|
+
concurrency: 2,
|
|
81
|
+
}
|
|
82
|
+
export default async function handle() {}
|
|
83
|
+
`
|
|
84
|
+
|
|
85
|
+
async function generateAll(resolver: PackageResolver): Promise<void> {
|
|
86
|
+
await generateModuleRegistry({ resolver, quiet: true })
|
|
87
|
+
await generateModuleRegistryApp({ resolver, quiet: true })
|
|
88
|
+
await generateModuleRegistryCli({ resolver, quiet: true })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'generated-registry-imports-'))
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('generated registries — emitted identifiers are imported', () => {
|
|
100
|
+
it('imports the abandon-hook helper in every file that calls it', async () => {
|
|
101
|
+
scaffoldWorkerModule(tmpDir, 'demo', WORKER_WITH_HOOK)
|
|
102
|
+
const resolver = createMockResolver(tmpDir, [{ id: 'demo', from: 'pkg' } as ModuleEntry])
|
|
103
|
+
|
|
104
|
+
await generateAll(resolver)
|
|
105
|
+
|
|
106
|
+
const files = generatedFiles(tmpDir)
|
|
107
|
+
const callers = files.filter(({ content }) => content.includes(`${ABANDON_HOOK_FACTORY}(`))
|
|
108
|
+
// The fixture must actually exercise the path, or this test passes vacuously — which is exactly
|
|
109
|
+
// how the missing import survived every existing suite.
|
|
110
|
+
expect(callers.length).toBeGreaterThan(0)
|
|
111
|
+
|
|
112
|
+
for (const { name, content } of callers) {
|
|
113
|
+
expect(`${name}: ${content.includes(`import { `) && new RegExp(`import \\{[^}]*\\b${ABANDON_HOOK_FACTORY}\\b[^}]*\\} from`, 's').test(content)}`)
|
|
114
|
+
.toBe(`${name}: true`)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('leaves output untouched for a module set whose workers declare no hook', async () => {
|
|
119
|
+
scaffoldWorkerModule(tmpDir, 'demo', WORKER_WITHOUT_HOOK)
|
|
120
|
+
const resolver = createMockResolver(tmpDir, [{ id: 'demo', from: 'pkg' } as ModuleEntry])
|
|
121
|
+
|
|
122
|
+
await generateAll(resolver)
|
|
123
|
+
|
|
124
|
+
for (const { name, content } of generatedFiles(tmpDir)) {
|
|
125
|
+
expect(`${name}: ${content.includes(ABANDON_HOOK_FACTORY)}`).toBe(`${name}: false`)
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
})
|
|
@@ -4,6 +4,7 @@ import os from 'node:os'
|
|
|
4
4
|
import {
|
|
5
5
|
extractNamedObjectLiteralExport,
|
|
6
6
|
hasNamedExport,
|
|
7
|
+
namedObjectLiteralDeclaresProperty,
|
|
7
8
|
resolveNamedObjectExport,
|
|
8
9
|
} from '../module-registry'
|
|
9
10
|
|
|
@@ -218,3 +219,46 @@ describe('hasNamedExport', () => {
|
|
|
218
219
|
expect(hasNamedExport(file, 'metadata')).toBe(false)
|
|
219
220
|
})
|
|
220
221
|
})
|
|
222
|
+
|
|
223
|
+
describe('namedObjectLiteralDeclaresProperty', () => {
|
|
224
|
+
// A worker declaring `onJobAbandoned: someImportedFn` is the case that broke: the value resolvers
|
|
225
|
+
// cannot evaluate an imported identifier, so "is it declared" has to be answered from the syntax.
|
|
226
|
+
// Getting this wrong is silent — the generator simply never emits the hook.
|
|
227
|
+
it('sees a property whose value is an unresolvable identifier', () => {
|
|
228
|
+
const file = writeSource('worker.ts', `
|
|
229
|
+
import { failAbandonedRun } from '../lib/abandoned-run'
|
|
230
|
+
export const metadata = {
|
|
231
|
+
queue: 'data-sync-import',
|
|
232
|
+
concurrency: 5,
|
|
233
|
+
onJobAbandoned: failAbandonedRun,
|
|
234
|
+
}
|
|
235
|
+
`)
|
|
236
|
+
expect(namedObjectLiteralDeclaresProperty(file, 'metadata', 'onJobAbandoned')).toBe(true)
|
|
237
|
+
expect(extractNamedObjectLiteralExport(file, 'metadata')?.onJobAbandoned).toBeUndefined()
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('sees a property declared as an inline function', () => {
|
|
241
|
+
const file = writeSource('worker.ts', `
|
|
242
|
+
export const metadata = {
|
|
243
|
+
queue: 'q',
|
|
244
|
+
onJobAbandoned: async () => {},
|
|
245
|
+
}
|
|
246
|
+
`)
|
|
247
|
+
expect(namedObjectLiteralDeclaresProperty(file, 'metadata', 'onJobAbandoned')).toBe(true)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('reports absence for a worker that declares no hook', () => {
|
|
251
|
+
const file = writeSource('worker.ts', `
|
|
252
|
+
export const metadata = { queue: 'q', concurrency: 1 }
|
|
253
|
+
`)
|
|
254
|
+
expect(namedObjectLiteralDeclaresProperty(file, 'metadata', 'onJobAbandoned')).toBe(false)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('reports absence for a metadata export that is not an object literal', () => {
|
|
258
|
+
const file = writeSource('worker.ts', `
|
|
259
|
+
const base = buildMetadata()
|
|
260
|
+
export const metadata = base
|
|
261
|
+
`)
|
|
262
|
+
expect(namedObjectLiteralDeclaresProperty(file, 'metadata', 'onJobAbandoned')).toBe(false)
|
|
263
|
+
})
|
|
264
|
+
})
|
|
@@ -149,16 +149,20 @@ describe('module-facts BC resolve guard (T2)', () => {
|
|
|
149
149
|
// wms, staff, integrations and checkout were therefore invisible to every fact
|
|
150
150
|
// consumer — `integrations` published no contributions at all. Reading them costs
|
|
151
151
|
// ~28KB, which is the fix working, not drift.
|
|
152
|
-
// The additive EUDR
|
|
153
|
-
// entities, and extension surfaces without changing the extraction
|
|
152
|
+
// The additive EUDR and Documents modules contribute their real routes, ACL,
|
|
153
|
+
// events, entities, and extension surfaces without changing the extraction
|
|
154
|
+
// shape.
|
|
154
155
|
//
|
|
155
156
|
// JSON cap raised a fifth time by the devices/push-notifications stack: the
|
|
156
157
|
// `devices` and `push_notifications` modules plus the `channel-fcm`,
|
|
157
158
|
// `channel-apns` and `channel-expo` provider packages add their own facts,
|
|
158
159
|
// provenance entries and override targets to every render. The
|
|
159
|
-
// `warranty_claims` module (see above)
|
|
160
|
-
// absorbs
|
|
161
|
-
|
|
160
|
+
// `warranty_claims` module (see above) and the additive Documents module
|
|
161
|
+
// land alongside it, so the cap absorbs all three additions. Both sides of
|
|
162
|
+
// that merge had raised the cap independently (3_950_000 for Documents,
|
|
163
|
+
// 4_000_000 for the rest); neither number covers the union, which measures
|
|
164
|
+
// ~4.09MB. Keep bounded headroom over the measured size only.
|
|
165
|
+
expect(Buffer.byteLength(completeJson)).toBeLessThan(4_150_000)
|
|
162
166
|
expect(Buffer.byteLength(completeJson) - Buffer.byteLength(legacyJson)).toBeLessThan(1_800_000)
|
|
163
167
|
// Markdown cap raised with the source-link contract: entities, events, ACL
|
|
164
168
|
// features, DI tokens, search entities, notifications, UMES hosts and UMES
|
|
@@ -246,6 +246,15 @@ type SerializableWorkerMetadata = {
|
|
|
246
246
|
concurrency?: number
|
|
247
247
|
lockDuration?: number
|
|
248
248
|
maxStalledCount?: number
|
|
249
|
+
/**
|
|
250
|
+
* Whether the worker's metadata declares an `onJobAbandoned` callback.
|
|
251
|
+
*
|
|
252
|
+
* A function cannot be serialized into the registry the way the scalar options are, so the flag is
|
|
253
|
+
* resolved here at build time and the callback itself is emitted as a lazy import beside the
|
|
254
|
+
* handler. Recorded only when the source module could actually be loaded — the object-literal
|
|
255
|
+
* fallback below cannot see whether a referenced identifier is a function.
|
|
256
|
+
*/
|
|
257
|
+
hasJobAbandonedHook?: boolean
|
|
249
258
|
}
|
|
250
259
|
|
|
251
260
|
type PageMetadataManifestLoadResult = {
|
|
@@ -577,6 +586,55 @@ function extractObjectPropertiesFromAst(sourceFile: string, exportName: string):
|
|
|
577
586
|
return Object.keys(result).length > 0 ? result : null
|
|
578
587
|
}
|
|
579
588
|
|
|
589
|
+
/**
|
|
590
|
+
* Whether an exported object literal declares a property, regardless of what it evaluates to.
|
|
591
|
+
*
|
|
592
|
+
* Needed for callbacks: the value resolvers above can read literals and local constants, but a
|
|
593
|
+
* property whose value is an imported function stays unresolvable, so "did the author declare it"
|
|
594
|
+
* cannot be answered by inspecting the extracted value. Answering it from the syntax instead is what
|
|
595
|
+
* lets the generator emit a lazy accessor for a hook it can see but cannot serialize.
|
|
596
|
+
*/
|
|
597
|
+
export function namedObjectLiteralDeclaresProperty(sourceFile: string, exportName: string, propertyName: string): boolean {
|
|
598
|
+
let source = ''
|
|
599
|
+
try {
|
|
600
|
+
source = fs.readFileSync(sourceFile, 'utf8')
|
|
601
|
+
} catch {
|
|
602
|
+
return false
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const parsed = ts.createSourceFile(sourceFile, source, ts.ScriptTarget.Latest, true, inferScriptKind(sourceFile))
|
|
606
|
+
const exportedNames = new Set<string>()
|
|
607
|
+
for (const statement of parsed.statements) {
|
|
608
|
+
if (
|
|
609
|
+
ts.isExportDeclaration(statement)
|
|
610
|
+
&& statement.exportClause
|
|
611
|
+
&& ts.isNamedExports(statement.exportClause)
|
|
612
|
+
&& !statement.moduleSpecifier
|
|
613
|
+
) {
|
|
614
|
+
for (const element of statement.exportClause.elements) exportedNames.add(element.name.text)
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
for (const statement of parsed.statements) {
|
|
619
|
+
if (!ts.isVariableStatement(statement)) continue
|
|
620
|
+
const exportsDirectly = (ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined)
|
|
621
|
+
?.some((modifier: ts.Modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)
|
|
622
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
623
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== exportName) continue
|
|
624
|
+
if (!exportsDirectly && !exportedNames.has(exportName)) continue
|
|
625
|
+
const objectLiteral = unwrapObjectLiteralExpression(declaration.initializer)
|
|
626
|
+
if (!objectLiteral) return false
|
|
627
|
+
return objectLiteral.properties.some((property) => {
|
|
628
|
+
const name = property.name
|
|
629
|
+
if (!name) return false
|
|
630
|
+
const key = ts.isIdentifier(name) ? name.text : ts.isStringLiteral(name) ? name.text : null
|
|
631
|
+
return key === propertyName
|
|
632
|
+
})
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return false
|
|
636
|
+
}
|
|
637
|
+
|
|
580
638
|
export function extractNamedObjectLiteralExport(sourceFile: string, exportName: string): Record<string, unknown> | null {
|
|
581
639
|
const literal = extractNamedObjectLiteralSource(sourceFile, exportName)
|
|
582
640
|
if (literal) {
|
|
@@ -1777,6 +1835,7 @@ function normalizeWorkerMetadata(raw: unknown): SerializableWorkerMetadata | nul
|
|
|
1777
1835
|
if (typeof source.concurrency === 'number') normalized.concurrency = source.concurrency
|
|
1778
1836
|
if (typeof source.lockDuration === 'number') normalized.lockDuration = source.lockDuration
|
|
1779
1837
|
if (typeof source.maxStalledCount === 'number') normalized.maxStalledCount = source.maxStalledCount
|
|
1838
|
+
if (typeof source.onJobAbandoned === 'function') normalized.hasJobAbandonedHook = true
|
|
1780
1839
|
return Object.keys(normalized).length > 0 ? normalized : null
|
|
1781
1840
|
}
|
|
1782
1841
|
|
|
@@ -1788,8 +1847,16 @@ async function loadSubscriberMetadata(sourceFile: string): Promise<SerializableS
|
|
|
1788
1847
|
|
|
1789
1848
|
async function loadWorkerMetadata(sourceFile: string): Promise<SerializableWorkerMetadata | null> {
|
|
1790
1849
|
const sourceModule = await loadModuleExportsFromSource<Record<string, unknown>>(sourceFile)
|
|
1791
|
-
|
|
1850
|
+
const metadata = normalizeWorkerMetadata(sourceModule?.metadata)
|
|
1792
1851
|
?? normalizeWorkerMetadata(extractNamedObjectLiteralExport(sourceFile, 'metadata'))
|
|
1852
|
+
if (!metadata) return null
|
|
1853
|
+
// Resolved from the syntax, not the extracted value: a worker module that cannot be imported at
|
|
1854
|
+
// build time falls back to the literal extractor, which cannot tell an imported function from an
|
|
1855
|
+
// unresolvable identifier. Missing this is silent — the hook is simply never installed.
|
|
1856
|
+
if (!metadata.hasJobAbandonedHook && namedObjectLiteralDeclaresProperty(sourceFile, 'metadata', 'onJobAbandoned')) {
|
|
1857
|
+
metadata.hasJobAbandonedHook = true
|
|
1858
|
+
}
|
|
1859
|
+
return metadata
|
|
1793
1860
|
}
|
|
1794
1861
|
|
|
1795
1862
|
async function discoverSubscribers(
|
|
@@ -2326,12 +2393,25 @@ function processSubscribers(discovered: DiscoveredSubscriber[]): string[] {
|
|
|
2326
2393
|
return subscribers
|
|
2327
2394
|
}
|
|
2328
2395
|
|
|
2396
|
+
/**
|
|
2397
|
+
* Registry helper emitted for a worker that declares `metadata.onJobAbandoned`.
|
|
2398
|
+
*
|
|
2399
|
+
* Named once because two things must stay in step: the call the worker entry renders, and the import
|
|
2400
|
+
* the file needs for it. A generated file that references this without importing it type-checks
|
|
2401
|
+
* nowhere, and only fails at `build:app` — see `generated-registry-imports.test.ts`.
|
|
2402
|
+
*/
|
|
2403
|
+
const ABANDON_HOOK_FACTORY = 'createLazyModuleWorkerAbandonHook'
|
|
2404
|
+
|
|
2405
|
+
function workersDeclareAbandonHook(discovered: DiscoveredWorker[]): boolean {
|
|
2406
|
+
return discovered.some(({ metadata }) => metadata.hasJobAbandonedHook === true)
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2329
2409
|
function processWorkers(discovered: DiscoveredWorker[]): string[] {
|
|
2330
2410
|
const workers: string[] = []
|
|
2331
2411
|
for (const { id, importPath, metadata } of discovered) {
|
|
2332
2412
|
const workerId = metadata.id ?? id
|
|
2333
2413
|
workers.push(
|
|
2334
|
-
`{ id: ${toLiteral(workerId)}, queue: ${toLiteral(metadata.queue)}, concurrency: ${toLiteral(metadata.concurrency ?? 1)}${metadata.lockDuration === undefined ? '' : `, lockDuration: ${toLiteral(metadata.lockDuration)}`}${metadata.maxStalledCount === undefined ? '' : `, maxStalledCount: ${toLiteral(metadata.maxStalledCount)}`}, handler: createLazyModuleWorker(() => ${buildDynamicImportExpression(importPath)}, ${toLiteral(workerId)}) }`
|
|
2414
|
+
`{ id: ${toLiteral(workerId)}, queue: ${toLiteral(metadata.queue)}, concurrency: ${toLiteral(metadata.concurrency ?? 1)}${metadata.lockDuration === undefined ? '' : `, lockDuration: ${toLiteral(metadata.lockDuration)}`}${metadata.maxStalledCount === undefined ? '' : `, maxStalledCount: ${toLiteral(metadata.maxStalledCount)}`}${metadata.hasJobAbandonedHook ? `, onJobAbandoned: ${ABANDON_HOOK_FACTORY}(() => ${buildDynamicImportExpression(importPath)}, ${toLiteral(workerId)})` : ''}, handler: createLazyModuleWorker(() => ${buildDynamicImportExpression(importPath)}, ${toLiteral(workerId)}) }`
|
|
2335
2415
|
)
|
|
2336
2416
|
}
|
|
2337
2417
|
return workers
|
|
@@ -2567,6 +2647,7 @@ function renderAstModuleRegistryFile(options: {
|
|
|
2567
2647
|
imports: string[]
|
|
2568
2648
|
moduleEntries: WriterFunction[]
|
|
2569
2649
|
includeCreateElementImport?: boolean
|
|
2650
|
+
includeAbandonHookImport?: boolean
|
|
2570
2651
|
}): string {
|
|
2571
2652
|
const sourceFile = createGeneratedSourceFile(options.fileName)
|
|
2572
2653
|
addAutoGeneratedComment(sourceFile, options.generator)
|
|
@@ -2582,6 +2663,7 @@ function renderAstModuleRegistryFile(options: {
|
|
|
2582
2663
|
namedImports: [
|
|
2583
2664
|
{ name: 'createLazyModuleSubscriber' },
|
|
2584
2665
|
{ name: 'createLazyModuleWorker' },
|
|
2666
|
+
...(options.includeAbandonHookImport ? [{ name: ABANDON_HOOK_FACTORY }] : []),
|
|
2585
2667
|
{ name: 'Module', isTypeOnly: true },
|
|
2586
2668
|
],
|
|
2587
2669
|
})
|
|
@@ -2694,10 +2776,18 @@ function renderAstLegacyModuleRegistryOutput(options: {
|
|
|
2694
2776
|
imports: GeneratedImportStatement[]
|
|
2695
2777
|
moduleEntries: string[]
|
|
2696
2778
|
includeCreateElementImport?: boolean
|
|
2779
|
+
includeAbandonHookImport?: boolean
|
|
2697
2780
|
}): string {
|
|
2781
|
+
// Derived from what is actually being written, not from a flag the caller has to remember: an
|
|
2782
|
+
// import that must be opted into at every render site produces a file referencing an undeclared
|
|
2783
|
+
// name the first time someone forgets, and the file only fails at `build:app`.
|
|
2784
|
+
const needsAbandonHookImport = options.includeAbandonHookImport
|
|
2785
|
+
|| options.moduleEntries.some((entry) => entry.includes(ABANDON_HOOK_FACTORY))
|
|
2698
2786
|
const importSection = [
|
|
2699
2787
|
...(options.includeCreateElementImport ? ["import { createElement } from 'react'"] : []),
|
|
2700
|
-
|
|
2788
|
+
needsAbandonHookImport
|
|
2789
|
+
? "import { createLazyModuleSubscriber, createLazyModuleWorker, createLazyModuleWorkerAbandonHook, type Module } from '@open-mercato/shared/modules/registry'"
|
|
2790
|
+
: "import { createLazyModuleSubscriber, createLazyModuleWorker, type Module } from '@open-mercato/shared/modules/registry'",
|
|
2701
2791
|
...options.imports.map((entry) => serializeGeneratedImport(entry)),
|
|
2702
2792
|
].join('\n')
|
|
2703
2793
|
|
|
@@ -3011,6 +3101,15 @@ function processWorkersAst(discovered: DiscoveredWorker[]): WriterFunction[] {
|
|
|
3011
3101
|
{ name: 'concurrency', value: metadata.concurrency ?? 1 },
|
|
3012
3102
|
...(metadata.lockDuration === undefined ? [] : [{ name: 'lockDuration', value: metadata.lockDuration }]),
|
|
3013
3103
|
...(metadata.maxStalledCount === undefined ? [] : [{ name: 'maxStalledCount', value: metadata.maxStalledCount }]),
|
|
3104
|
+
...(metadata.hasJobAbandonedHook
|
|
3105
|
+
? [{
|
|
3106
|
+
name: 'onJobAbandoned',
|
|
3107
|
+
value: callExpression(identifier(ABANDON_HOOK_FACTORY), [
|
|
3108
|
+
arrowFunction({ body: importExpression(importPath) }),
|
|
3109
|
+
workerId,
|
|
3110
|
+
]),
|
|
3111
|
+
}]
|
|
3112
|
+
: []),
|
|
3014
3113
|
{
|
|
3015
3114
|
name: 'handler',
|
|
3016
3115
|
value: callExpression(identifier('createLazyModuleWorker'), [
|
|
@@ -3384,6 +3483,7 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
|
|
|
3384
3483
|
const requiresByModule = new Map<string, string[]>()
|
|
3385
3484
|
const commandLoaderEntries: CommandLoaderGenerationEntry[] = []
|
|
3386
3485
|
let hasRouteComponents = false
|
|
3486
|
+
let hasAbandonHookWorkers = false
|
|
3387
3487
|
|
|
3388
3488
|
// UMES conflict detection: collect file paths during module processing
|
|
3389
3489
|
const umesConflictSources: Array<{
|
|
@@ -3635,7 +3735,9 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
|
|
|
3635
3735
|
subscribers.push(...processSubscribers(await discovered.getSubscribers()))
|
|
3636
3736
|
|
|
3637
3737
|
// 18. Workers
|
|
3638
|
-
|
|
3738
|
+
const discoveredWorkersForModule = await discovered.getWorkers()
|
|
3739
|
+
if (workersDeclareAbandonHook(discoveredWorkersForModule)) hasAbandonHookWorkers = true
|
|
3740
|
+
workers.push(...processWorkers(discoveredWorkersForModule))
|
|
3639
3741
|
|
|
3640
3742
|
// Build combined customFieldSets expression
|
|
3641
3743
|
{
|
|
@@ -3801,12 +3903,14 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
|
|
|
3801
3903
|
imports,
|
|
3802
3904
|
moduleEntries: moduleDecls,
|
|
3803
3905
|
includeCreateElementImport: hasRouteComponents,
|
|
3906
|
+
includeAbandonHookImport: hasAbandonHookWorkers,
|
|
3804
3907
|
})
|
|
3805
3908
|
const runtimeOutput = renderAstLegacyModuleRegistryOutput({
|
|
3806
3909
|
fileName: 'modules.runtime.generated.ts',
|
|
3807
3910
|
imports: runtimeImports,
|
|
3808
3911
|
moduleEntries: runtimeModuleDecls,
|
|
3809
3912
|
includeCreateElementImport: true,
|
|
3913
|
+
includeAbandonHookImport: hasAbandonHookWorkers,
|
|
3810
3914
|
})
|
|
3811
3915
|
const frontendRoutesOutput = renderAstLegacyManifestOutput({
|
|
3812
3916
|
fileName: 'frontend-routes.generated.ts',
|
|
@@ -4010,6 +4114,7 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
|
|
|
4010
4114
|
const requiresByModule = new Map<string, string[]>()
|
|
4011
4115
|
const commandLoaderEntries: CommandLoaderGenerationEntry[] = []
|
|
4012
4116
|
let hasRouteComponents = false
|
|
4117
|
+
let hasAbandonHookWorkers = false
|
|
4013
4118
|
const seenBackendRoutePatterns = new Map<string, string>()
|
|
4014
4119
|
|
|
4015
4120
|
for (const discovered of discovery.modules) {
|
|
@@ -4171,7 +4276,9 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
|
|
|
4171
4276
|
|
|
4172
4277
|
subscribers.push(...processSubscribersAst(await discovered.getSubscribers()))
|
|
4173
4278
|
|
|
4174
|
-
|
|
4279
|
+
const discoveredWorkersForModule = await discovered.getWorkers()
|
|
4280
|
+
if (workersDeclareAbandonHook(discoveredWorkersForModule)) hasAbandonHookWorkers = true
|
|
4281
|
+
workers.push(...processWorkersAst(discoveredWorkersForModule))
|
|
4175
4282
|
|
|
4176
4283
|
{
|
|
4177
4284
|
dashboardWidgetsValue = buildModuleDashboardWidgetsValue(discovered.dashboardWidgetEntries)
|
|
@@ -4295,18 +4402,21 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
|
|
|
4295
4402
|
imports,
|
|
4296
4403
|
moduleEntries: moduleDecls,
|
|
4297
4404
|
includeCreateElementImport: hasRouteComponents,
|
|
4405
|
+
includeAbandonHookImport: hasAbandonHookWorkers,
|
|
4298
4406
|
})
|
|
4299
4407
|
const bootstrapOutput = renderAstModuleRegistryFile({
|
|
4300
4408
|
fileName: 'modules.bootstrap.generated.ts',
|
|
4301
4409
|
generator: 'registry (bootstrap version)',
|
|
4302
4410
|
imports: bootstrapImports,
|
|
4303
4411
|
moduleEntries: bootstrapModuleDecls,
|
|
4412
|
+
includeAbandonHookImport: hasAbandonHookWorkers,
|
|
4304
4413
|
})
|
|
4305
4414
|
const i18nOutput = renderAstModuleRegistryFile({
|
|
4306
4415
|
fileName: 'modules.i18n.generated.ts',
|
|
4307
4416
|
generator: 'registry (i18n version)',
|
|
4308
4417
|
imports: i18nImports,
|
|
4309
4418
|
moduleEntries: i18nModuleDecls,
|
|
4419
|
+
includeAbandonHookImport: hasAbandonHookWorkers,
|
|
4310
4420
|
})
|
|
4311
4421
|
const legacyOutput = renderAstLegacyAliasFile({
|
|
4312
4422
|
fileName: 'bootstrap-modules.generated.ts',
|
|
@@ -4397,6 +4507,7 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
|
|
|
4397
4507
|
* Excludes: frontend routes, backend routes, API handlers, injection widgets
|
|
4398
4508
|
*/
|
|
4399
4509
|
async function generateModuleRegistryCliFromDiscovery(options: ModuleRegistryRenderOptions): Promise<GeneratorResult> {
|
|
4510
|
+
let hasAbandonHookWorkers = false
|
|
4400
4511
|
const { resolver, quiet = false } = options
|
|
4401
4512
|
const { discovery } = options
|
|
4402
4513
|
const result = createGeneratorResult()
|
|
@@ -4548,6 +4659,7 @@ async function generateModuleRegistryCliFromDiscovery(options: ModuleRegistryRen
|
|
|
4548
4659
|
// Workers
|
|
4549
4660
|
const discoveredWorkers = await discovered.getWorkers()
|
|
4550
4661
|
const workerGenerationEntries = collectWorkerGenerationEntries(discoveredWorkers, modId)
|
|
4662
|
+
if (workersDeclareAbandonHook(discoveredWorkers)) hasAbandonHookWorkers = true
|
|
4551
4663
|
workers.push(...processWorkersAst(discoveredWorkers))
|
|
4552
4664
|
devSupervisorWorkers.push(...workerGenerationEntries.map(({ importPath: _importPath, ...worker }) => worker))
|
|
4553
4665
|
|
|
@@ -4671,6 +4783,7 @@ async function generateModuleRegistryCliFromDiscovery(options: ModuleRegistryRen
|
|
|
4671
4783
|
generator: 'registry (CLI version)',
|
|
4672
4784
|
imports,
|
|
4673
4785
|
moduleEntries: moduleDecls,
|
|
4786
|
+
includeAbandonHookImport: hasAbandonHookWorkers,
|
|
4674
4787
|
})
|
|
4675
4788
|
const legacyOutput = renderAstLegacyAliasFile({
|
|
4676
4789
|
fileName: 'cli-modules.generated.ts',
|
package/src/mercato.ts
CHANGED
|
@@ -78,6 +78,29 @@ function getRegisteredCliWorkers(modules: Module[] = getCliModules()): ModuleWor
|
|
|
78
78
|
return allWorkers
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Picks the abandoned-job callback for a queue from its workers.
|
|
83
|
+
*
|
|
84
|
+
* The numeric queue options merge across a queue's workers with `Math.max`; two callbacks cannot
|
|
85
|
+
* merge, so the first declared one wins. Silence there would make a genuine wiring mistake — two
|
|
86
|
+
* handlers on one queue each expecting to report its abandoned jobs — look like it works while one of
|
|
87
|
+
* them never runs.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveQueueAbandonHook(
|
|
90
|
+
queueName: string,
|
|
91
|
+
queueWorkers: Array<Pick<ModuleWorker, 'id' | 'onJobAbandoned'>>,
|
|
92
|
+
warn: (message: string) => void = console.warn,
|
|
93
|
+
): ModuleWorker['onJobAbandoned'] {
|
|
94
|
+
const declaring = queueWorkers.filter((worker) => worker.onJobAbandoned)
|
|
95
|
+
if (declaring.length > 1) {
|
|
96
|
+
warn(
|
|
97
|
+
`[worker] Queue "${queueName}" has ${declaring.length} workers declaring onJobAbandoned `
|
|
98
|
+
+ `(${declaring.map((worker) => worker.id).join(', ')}); using the first and ignoring the rest.`,
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
return declaring[0]?.onJobAbandoned
|
|
102
|
+
}
|
|
103
|
+
|
|
81
104
|
function shouldEmbedLocalSchedulerInSharedWorker(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
82
105
|
return parseBooleanToken(env.OM_DEV_EMBED_SCHEDULER_IN_SHARED_WORKER) === true
|
|
83
106
|
}
|
|
@@ -1678,6 +1701,7 @@ export async function run(argv = process.argv) {
|
|
|
1678
1701
|
Math.max(...queueWorkers.map((w) => w.concurrency), 1)
|
|
1679
1702
|
const maxStalledCount = Math.max(...queueWorkers.map((w) => w.maxStalledCount ?? 1), 1)
|
|
1680
1703
|
const lockDuration = Math.max(...queueWorkers.map((w) => w.lockDuration ?? 0), 0) || undefined
|
|
1704
|
+
const onJobAbandoned = resolveQueueAbandonHook(queue, queueWorkers)
|
|
1681
1705
|
|
|
1682
1706
|
console.log(`[worker] Starting "${queue}" with ${queueWorkers.length} handler(s), concurrency: ${concurrency}`)
|
|
1683
1707
|
|
|
@@ -1688,6 +1712,7 @@ export async function run(argv = process.argv) {
|
|
|
1688
1712
|
concurrency,
|
|
1689
1713
|
lockDuration,
|
|
1690
1714
|
maxStalledCount,
|
|
1715
|
+
onJobAbandoned,
|
|
1691
1716
|
background: true,
|
|
1692
1717
|
handler: createPerJobWorkerHandler(queueWorkers, createRequestContainer),
|
|
1693
1718
|
})
|
|
@@ -1733,6 +1758,7 @@ export async function run(argv = process.argv) {
|
|
|
1733
1758
|
const concurrency = budgetPlan.entries[0]?.effective ?? requested
|
|
1734
1759
|
const maxStalledCount = Math.max(...queueWorkers.map((w) => w.maxStalledCount ?? 1), 1)
|
|
1735
1760
|
const lockDuration = Math.max(...queueWorkers.map((w) => w.lockDuration ?? 0), 0) || undefined
|
|
1761
|
+
const onJobAbandoned = resolveQueueAbandonHook(queueName!, queueWorkers)
|
|
1736
1762
|
|
|
1737
1763
|
console.log(`[worker] Found ${queueWorkers.length} worker(s) for queue "${queueName}"`)
|
|
1738
1764
|
|
|
@@ -1743,6 +1769,7 @@ export async function run(argv = process.argv) {
|
|
|
1743
1769
|
concurrency,
|
|
1744
1770
|
lockDuration,
|
|
1745
1771
|
maxStalledCount,
|
|
1772
|
+
onJobAbandoned,
|
|
1746
1773
|
handler: createPerJobWorkerHandler(queueWorkers, createRequestContainer),
|
|
1747
1774
|
})
|
|
1748
1775
|
} else {
|