@objectstack/plugin-approvals 5.0.0 → 5.2.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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +54 -0
- package/dist/index.d.mts +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +82 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +82 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -5
- package/src/approval-service.test.ts +109 -0
- package/src/approval-service.ts +88 -3
- package/src/approvals-plugin.ts +14 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/plugin-approvals@5.
|
|
2
|
+
> @objectstack/plugin-approvals@5.2.0 build /home/runner/work/framework/framework/packages/plugins/plugin-approvals
|
|
3
3
|
> tsup --config ../../../tsup.config.ts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
|
-
[32mESM[39m [1mdist/index.mjs [22m[
|
|
14
|
-
[32mESM[39m [1mdist/index.mjs.map [22m[
|
|
15
|
-
[32mESM[39m ⚡️ Build success in
|
|
16
|
-
[32mCJS[39m [1mdist/index.js [22m[
|
|
17
|
-
[32mCJS[39m [1mdist/index.js.map [22m[
|
|
18
|
-
[32mCJS[39m ⚡️ Build success in
|
|
13
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m42.73 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m91.14 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 140ms
|
|
16
|
+
[32mCJS[39m [1mdist/index.js [22m[32m43.79 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m91.06 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 146ms
|
|
19
19
|
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in
|
|
21
|
-
[32mDTS[39m [1mdist/index.d.mts [22m[
|
|
22
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 19856ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m9.33 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m9.33 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# @objectstack/plugin-approvals
|
|
2
2
|
|
|
3
|
+
## 5.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- bab2b20: feat(approvals): execution-pinned approval processes (ADR-0009)
|
|
8
|
+
|
|
9
|
+
When an approval request is submitted, the engine now records a `process_hash`
|
|
10
|
+
on `sys_approval_request` — the sha256 of the approval process body resolved
|
|
11
|
+
through `MetadataRepository`. While the request is in flight, `approve` /
|
|
12
|
+
`reject` / `recall` resolve the pinned process body via
|
|
13
|
+
`MetadataRepository.getByHash`. Upgrading the approval process definition
|
|
14
|
+
mid-flight therefore no longer affects requests that already started against
|
|
15
|
+
the previous version.
|
|
16
|
+
|
|
17
|
+
Behavior:
|
|
18
|
+
|
|
19
|
+
- `sys_approval_request` gains a `process_hash` column (text, nullable,
|
|
20
|
+
read-only). Existing rows keep working — the engine falls back to the
|
|
21
|
+
current `sys_approval_process` projection when the column is empty.
|
|
22
|
+
- `ApprovalServiceOptions` accepts an optional `metadataRepo`. When omitted
|
|
23
|
+
(e.g. defining processes purely through the runtime API or in unit tests),
|
|
24
|
+
pinning is silently disabled and the service behaves as before.
|
|
25
|
+
- `ApprovalsServicePlugin` looks up the metadata service from the kernel
|
|
26
|
+
and wires its repository automatically.
|
|
27
|
+
- The metadata-core local `MetadataTypeSchema` enum was realigned with the
|
|
28
|
+
canonical `@objectstack/spec/kernel` enum (drift fix: `approval`, `field`,
|
|
29
|
+
`function`, `service`, …).
|
|
30
|
+
|
|
31
|
+
This is the first user-visible consumer of the `executionPinned` capability
|
|
32
|
+
introduced in ADR-0009.
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- Updated dependencies [bab2b20]
|
|
37
|
+
- Updated dependencies [fa011d8]
|
|
38
|
+
- Updated dependencies [f0f7c27]
|
|
39
|
+
- Updated dependencies [b806f58]
|
|
40
|
+
- @objectstack/platform-objects@5.2.0
|
|
41
|
+
- @objectstack/spec@5.2.0
|
|
42
|
+
- @objectstack/metadata-core@5.2.0
|
|
43
|
+
- @objectstack/core@5.2.0
|
|
44
|
+
- @objectstack/formula@5.2.0
|
|
45
|
+
|
|
46
|
+
## 5.1.0
|
|
47
|
+
|
|
48
|
+
### Patch Changes
|
|
49
|
+
|
|
50
|
+
- Updated dependencies [75f4ee6]
|
|
51
|
+
- Updated dependencies [823d559]
|
|
52
|
+
- @objectstack/spec@5.1.0
|
|
53
|
+
- @objectstack/platform-objects@5.1.0
|
|
54
|
+
- @objectstack/core@5.1.0
|
|
55
|
+
- @objectstack/formula@5.1.0
|
|
56
|
+
|
|
3
57
|
## 5.0.0
|
|
4
58
|
|
|
5
59
|
### Patch Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { SysApprovalAction, SysApprovalProcess, SysApprovalRequest } from '@objectstack/platform-objects/audit';
|
|
2
2
|
import { IApprovalService, DefineApprovalProcessInput, SharingExecutionContext, ApprovalProcessRow, SubmitApprovalInput, ApprovalRequestRow, ApprovalStatus, ApprovalDecisionInput, ApprovalDecisionResult, ApprovalActionRow } from '@objectstack/spec/contracts';
|
|
3
3
|
export { ApprovalActionRow, ApprovalDecisionInput, ApprovalDecisionResult, ApprovalProcessRow, ApprovalRequestRow, ApprovalStatus, DefineApprovalProcessInput, IApprovalService, SubmitApprovalInput } from '@objectstack/spec/contracts';
|
|
4
|
+
import { MetadataRepository } from '@objectstack/metadata-core';
|
|
4
5
|
import { Plugin, PluginContext } from '@objectstack/core';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -70,6 +71,19 @@ interface ApprovalServiceOptions {
|
|
|
70
71
|
* The plugin uses this to re-bind lifecycle hooks for auto-trigger / lock.
|
|
71
72
|
*/
|
|
72
73
|
onRegistryChange?: () => void | Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Optional metadata repository for execution-pinned process resolution
|
|
76
|
+
* (ADR-0009). When provided:
|
|
77
|
+
*
|
|
78
|
+
* - `submit()` records the process body's sha256 on the request row.
|
|
79
|
+
* - `approve` / `reject` / `recall` resolve the pinned body via
|
|
80
|
+
* `MetadataRepository.getByHash` so process upgrades don't affect
|
|
81
|
+
* in-flight requests.
|
|
82
|
+
*
|
|
83
|
+
* When omitted, the service reads the current process from the
|
|
84
|
+
* `sys_approval_process` projection (pre-ADR-0009 behaviour).
|
|
85
|
+
*/
|
|
86
|
+
metadataRepo?: MetadataRepository;
|
|
73
87
|
}
|
|
74
88
|
declare class ApprovalService implements IApprovalService {
|
|
75
89
|
private readonly engine;
|
|
@@ -78,6 +92,7 @@ declare class ApprovalService implements IApprovalService {
|
|
|
78
92
|
private readonly fetchImpl?;
|
|
79
93
|
private readonly webhookTimeoutMs?;
|
|
80
94
|
private readonly onRegistryChange?;
|
|
95
|
+
private readonly metadataRepo?;
|
|
81
96
|
constructor(opts: ApprovalServiceOptions);
|
|
82
97
|
/** Allow the plugin to attach a hook re-binding callback after construction. */
|
|
83
98
|
setRegistryChangeHandler(handler: () => void | Promise<void>): void;
|
|
@@ -105,6 +120,24 @@ declare class ApprovalService implements IApprovalService {
|
|
|
105
120
|
private expandRoleUsers;
|
|
106
121
|
private lookupManager;
|
|
107
122
|
private notifyRegistryChanged;
|
|
123
|
+
/**
|
|
124
|
+
* Look up the HEAD checksum of an approval process from the metadata repo
|
|
125
|
+
* (ADR-0009). Returns null when no repo is wired, no metadata exists for
|
|
126
|
+
* the name, or the lookup fails — callers MUST treat null as "do not pin"
|
|
127
|
+
* and fall back to the projection table.
|
|
128
|
+
*/
|
|
129
|
+
private resolveProcessHash;
|
|
130
|
+
/**
|
|
131
|
+
* Resolve the approval process for an in-flight request, honouring
|
|
132
|
+
* ADR-0009 execution pinning when a `process_hash` is recorded.
|
|
133
|
+
*
|
|
134
|
+
* Resolution order:
|
|
135
|
+
* 1. If `req.process_hash` AND `metadataRepo` are set, try
|
|
136
|
+
* `getByHash` — return a row whose `definition` is the pinned body.
|
|
137
|
+
* 2. Otherwise (or on lookup failure) fall back to the current
|
|
138
|
+
* projection via `getProcess(req.process_name)`.
|
|
139
|
+
*/
|
|
140
|
+
private loadProcessForRequest;
|
|
108
141
|
/** Mirror request status onto `process.approvalStatusField` if configured. */
|
|
109
142
|
private syncStatusField;
|
|
110
143
|
/** Convenience wrapper that funnels every action invocation through the executor. */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { SysApprovalAction, SysApprovalProcess, SysApprovalRequest } from '@objectstack/platform-objects/audit';
|
|
2
2
|
import { IApprovalService, DefineApprovalProcessInput, SharingExecutionContext, ApprovalProcessRow, SubmitApprovalInput, ApprovalRequestRow, ApprovalStatus, ApprovalDecisionInput, ApprovalDecisionResult, ApprovalActionRow } from '@objectstack/spec/contracts';
|
|
3
3
|
export { ApprovalActionRow, ApprovalDecisionInput, ApprovalDecisionResult, ApprovalProcessRow, ApprovalRequestRow, ApprovalStatus, DefineApprovalProcessInput, IApprovalService, SubmitApprovalInput } from '@objectstack/spec/contracts';
|
|
4
|
+
import { MetadataRepository } from '@objectstack/metadata-core';
|
|
4
5
|
import { Plugin, PluginContext } from '@objectstack/core';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -70,6 +71,19 @@ interface ApprovalServiceOptions {
|
|
|
70
71
|
* The plugin uses this to re-bind lifecycle hooks for auto-trigger / lock.
|
|
71
72
|
*/
|
|
72
73
|
onRegistryChange?: () => void | Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Optional metadata repository for execution-pinned process resolution
|
|
76
|
+
* (ADR-0009). When provided:
|
|
77
|
+
*
|
|
78
|
+
* - `submit()` records the process body's sha256 on the request row.
|
|
79
|
+
* - `approve` / `reject` / `recall` resolve the pinned body via
|
|
80
|
+
* `MetadataRepository.getByHash` so process upgrades don't affect
|
|
81
|
+
* in-flight requests.
|
|
82
|
+
*
|
|
83
|
+
* When omitted, the service reads the current process from the
|
|
84
|
+
* `sys_approval_process` projection (pre-ADR-0009 behaviour).
|
|
85
|
+
*/
|
|
86
|
+
metadataRepo?: MetadataRepository;
|
|
73
87
|
}
|
|
74
88
|
declare class ApprovalService implements IApprovalService {
|
|
75
89
|
private readonly engine;
|
|
@@ -78,6 +92,7 @@ declare class ApprovalService implements IApprovalService {
|
|
|
78
92
|
private readonly fetchImpl?;
|
|
79
93
|
private readonly webhookTimeoutMs?;
|
|
80
94
|
private readonly onRegistryChange?;
|
|
95
|
+
private readonly metadataRepo?;
|
|
81
96
|
constructor(opts: ApprovalServiceOptions);
|
|
82
97
|
/** Allow the plugin to attach a hook re-binding callback after construction. */
|
|
83
98
|
setRegistryChangeHandler(handler: () => void | Promise<void>): void;
|
|
@@ -105,6 +120,24 @@ declare class ApprovalService implements IApprovalService {
|
|
|
105
120
|
private expandRoleUsers;
|
|
106
121
|
private lookupManager;
|
|
107
122
|
private notifyRegistryChanged;
|
|
123
|
+
/**
|
|
124
|
+
* Look up the HEAD checksum of an approval process from the metadata repo
|
|
125
|
+
* (ADR-0009). Returns null when no repo is wired, no metadata exists for
|
|
126
|
+
* the name, or the lookup fails — callers MUST treat null as "do not pin"
|
|
127
|
+
* and fall back to the projection table.
|
|
128
|
+
*/
|
|
129
|
+
private resolveProcessHash;
|
|
130
|
+
/**
|
|
131
|
+
* Resolve the approval process for an in-flight request, honouring
|
|
132
|
+
* ADR-0009 execution pinning when a `process_hash` is recorded.
|
|
133
|
+
*
|
|
134
|
+
* Resolution order:
|
|
135
|
+
* 1. If `req.process_hash` AND `metadataRepo` are set, try
|
|
136
|
+
* `getByHash` — return a row whose `definition` is the pinned body.
|
|
137
|
+
* 2. Otherwise (or on lookup failure) fall back to the current
|
|
138
|
+
* projection via `getProcess(req.process_name)`.
|
|
139
|
+
*/
|
|
140
|
+
private loadProcessForRequest;
|
|
108
141
|
/** Mirror request status onto `process.approvalStatusField` if configured. */
|
|
109
142
|
private syncStatusField;
|
|
110
143
|
/** Convenience wrapper that funnels every action invocation through the executor. */
|
package/dist/index.js
CHANGED
|
@@ -265,6 +265,7 @@ function rowFromRequest(row) {
|
|
|
265
265
|
id: String(row.id),
|
|
266
266
|
organization_id: row.organization_id ?? void 0,
|
|
267
267
|
process_name: String(row.process_name ?? ""),
|
|
268
|
+
process_hash: row.process_hash ?? void 0,
|
|
268
269
|
object_name: String(row.object_name ?? ""),
|
|
269
270
|
record_id: String(row.record_id ?? ""),
|
|
270
271
|
submitter_id: row.submitter_id ?? void 0,
|
|
@@ -299,6 +300,7 @@ var ApprovalService = class {
|
|
|
299
300
|
this.fetchImpl = opts.fetch;
|
|
300
301
|
this.webhookTimeoutMs = opts.webhookTimeoutMs;
|
|
301
302
|
this.onRegistryChange = opts.onRegistryChange;
|
|
303
|
+
this.metadataRepo = opts.metadataRepo;
|
|
302
304
|
}
|
|
303
305
|
/** Allow the plugin to attach a hook re-binding callback after construction. */
|
|
304
306
|
setRegistryChangeHandler(handler) {
|
|
@@ -467,6 +469,70 @@ var ApprovalService = class {
|
|
|
467
469
|
this.logger?.warn?.("[approvals] onRegistryChange handler failed", { error: err?.message });
|
|
468
470
|
}
|
|
469
471
|
}
|
|
472
|
+
/**
|
|
473
|
+
* Look up the HEAD checksum of an approval process from the metadata repo
|
|
474
|
+
* (ADR-0009). Returns null when no repo is wired, no metadata exists for
|
|
475
|
+
* the name, or the lookup fails — callers MUST treat null as "do not pin"
|
|
476
|
+
* and fall back to the projection table.
|
|
477
|
+
*/
|
|
478
|
+
async resolveProcessHash(processName, organizationId) {
|
|
479
|
+
if (!this.metadataRepo) return null;
|
|
480
|
+
if (!processName) return null;
|
|
481
|
+
const orgRef = { org: organizationId || "system", type: "approval", name: processName };
|
|
482
|
+
try {
|
|
483
|
+
const head = await this.metadataRepo.get(orgRef);
|
|
484
|
+
return head?.hash ?? null;
|
|
485
|
+
} catch (err) {
|
|
486
|
+
this.logger?.debug?.("[approvals] metadataRepo.get failed", { name: processName, error: err?.message });
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Resolve the approval process for an in-flight request, honouring
|
|
492
|
+
* ADR-0009 execution pinning when a `process_hash` is recorded.
|
|
493
|
+
*
|
|
494
|
+
* Resolution order:
|
|
495
|
+
* 1. If `req.process_hash` AND `metadataRepo` are set, try
|
|
496
|
+
* `getByHash` — return a row whose `definition` is the pinned body.
|
|
497
|
+
* 2. Otherwise (or on lookup failure) fall back to the current
|
|
498
|
+
* projection via `getProcess(req.process_name)`.
|
|
499
|
+
*/
|
|
500
|
+
async loadProcessForRequest(req, context) {
|
|
501
|
+
const hash = req.process_hash;
|
|
502
|
+
if (hash && this.metadataRepo) {
|
|
503
|
+
const orgId = req.organization_id ?? null;
|
|
504
|
+
const orgRef = { org: orgId || "system", type: "approval", name: req.process_name };
|
|
505
|
+
try {
|
|
506
|
+
const pinned = await this.metadataRepo.getByHash(orgRef, hash);
|
|
507
|
+
if (pinned?.body) {
|
|
508
|
+
const current = await this.getProcess(req.process_name, context);
|
|
509
|
+
const body = pinned.body;
|
|
510
|
+
return {
|
|
511
|
+
id: current?.id ?? `pinned_${hash.slice(7, 19)}`,
|
|
512
|
+
name: req.process_name,
|
|
513
|
+
label: body.label ?? current?.label ?? req.process_name,
|
|
514
|
+
object_name: req.object_name,
|
|
515
|
+
description: body.description ?? current?.description,
|
|
516
|
+
active: current?.active ?? true,
|
|
517
|
+
definition: body,
|
|
518
|
+
created_at: current?.created_at,
|
|
519
|
+
updated_at: current?.updated_at
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
this.logger?.warn?.("[approvals] pinned process body not found; falling back to current", {
|
|
523
|
+
request: req.id,
|
|
524
|
+
process: req.process_name,
|
|
525
|
+
hash
|
|
526
|
+
});
|
|
527
|
+
} catch (err) {
|
|
528
|
+
this.logger?.warn?.("[approvals] getByHash failed; falling back to current", {
|
|
529
|
+
request: req.id,
|
|
530
|
+
error: err?.message
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return this.getProcess(req.process_name, context);
|
|
535
|
+
}
|
|
470
536
|
/** Mirror request status onto `process.approvalStatusField` if configured. */
|
|
471
537
|
async syncStatusField(process, request) {
|
|
472
538
|
const field = process.definition?.approvalStatusField;
|
|
@@ -607,9 +673,11 @@ var ApprovalService = class {
|
|
|
607
673
|
const approvers = await this.expandApprovers(step0, input.payload, ctxOrg);
|
|
608
674
|
const now = this.clock.now().toISOString();
|
|
609
675
|
const id = uid("areq");
|
|
676
|
+
const processHash = await this.resolveProcessHash(process.name, ctxOrg);
|
|
610
677
|
const row = {
|
|
611
678
|
id,
|
|
612
679
|
process_name: process.name,
|
|
680
|
+
process_hash: processHash,
|
|
613
681
|
object_name: input.object,
|
|
614
682
|
record_id: input.recordId,
|
|
615
683
|
submitter_id: input.submitterId ?? context.userId ?? null,
|
|
@@ -693,7 +761,7 @@ var ApprovalService = class {
|
|
|
693
761
|
if (!context.isSystem && !(req.pending_approvers ?? []).includes(input.actorId)) {
|
|
694
762
|
throw new Error(`FORBIDDEN: actor '${input.actorId}' is not a pending approver`);
|
|
695
763
|
}
|
|
696
|
-
const process = await this.
|
|
764
|
+
const process = await this.loadProcessForRequest(req, context);
|
|
697
765
|
if (!process) throw new Error(`PROCESS_NOT_FOUND: ${req.process_name}`);
|
|
698
766
|
const steps = process.definition?.steps ?? [];
|
|
699
767
|
const stepIndex = req.current_step_index ?? 0;
|
|
@@ -765,7 +833,7 @@ var ApprovalService = class {
|
|
|
765
833
|
if (!context.isSystem && !(req.pending_approvers ?? []).includes(input.actorId)) {
|
|
766
834
|
throw new Error(`FORBIDDEN: actor '${input.actorId}' is not a pending approver`);
|
|
767
835
|
}
|
|
768
|
-
const process = await this.
|
|
836
|
+
const process = await this.loadProcessForRequest(req, context);
|
|
769
837
|
if (!process) throw new Error(`PROCESS_NOT_FOUND: ${req.process_name}`);
|
|
770
838
|
const steps = process.definition?.steps ?? [];
|
|
771
839
|
const stepIndex = req.current_step_index ?? 0;
|
|
@@ -837,7 +905,7 @@ var ApprovalService = class {
|
|
|
837
905
|
updated_at: now
|
|
838
906
|
}, { context: SYSTEM_CTX2 });
|
|
839
907
|
const fresh = await this.getRequest(req.id, context);
|
|
840
|
-
const process = await this.
|
|
908
|
+
const process = await this.loadProcessForRequest(req, context);
|
|
841
909
|
if (process) {
|
|
842
910
|
await this.syncStatusField(process, fresh);
|
|
843
911
|
await this.runActions(process.definition?.onRecall, "recall", process, fresh, void 0, input.actorId, input.comment);
|
|
@@ -1047,10 +1115,20 @@ var ApprovalsServicePlugin = class {
|
|
|
1047
1115
|
}
|
|
1048
1116
|
this.engine = engine;
|
|
1049
1117
|
this.logger = ctx.logger;
|
|
1118
|
+
let metadataRepo;
|
|
1119
|
+
try {
|
|
1120
|
+
const meta = ctx.getService("metadata");
|
|
1121
|
+
metadataRepo = meta?.getRepository?.();
|
|
1122
|
+
} catch {
|
|
1123
|
+
}
|
|
1050
1124
|
this.service = new ApprovalService({
|
|
1051
1125
|
engine,
|
|
1052
|
-
logger: ctx.logger
|
|
1126
|
+
logger: ctx.logger,
|
|
1127
|
+
metadataRepo
|
|
1053
1128
|
});
|
|
1129
|
+
if (metadataRepo) {
|
|
1130
|
+
ctx.logger.info("ApprovalsServicePlugin: execution pinning enabled (ADR-0009)");
|
|
1131
|
+
}
|
|
1054
1132
|
if (!this.options.disableAutoHooks) {
|
|
1055
1133
|
this.service.setRegistryChangeHandler(() => this.rebindHooks());
|
|
1056
1134
|
const hookOn = ctx.hook ?? ctx.on;
|