@ic-reactor/candid 3.0.2-beta.0 → 3.0.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.
Files changed (83) hide show
  1. package/README.md +33 -1
  2. package/dist/adapter.js +2 -1
  3. package/dist/adapter.js.map +1 -1
  4. package/dist/display-reactor.d.ts +4 -13
  5. package/dist/display-reactor.d.ts.map +1 -1
  6. package/dist/display-reactor.js +22 -8
  7. package/dist/display-reactor.js.map +1 -1
  8. package/dist/index.d.ts +3 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +3 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/metadata-display-reactor.d.ts +108 -0
  13. package/dist/metadata-display-reactor.d.ts.map +1 -0
  14. package/dist/metadata-display-reactor.js +141 -0
  15. package/dist/metadata-display-reactor.js.map +1 -0
  16. package/dist/reactor.d.ts +1 -1
  17. package/dist/reactor.d.ts.map +1 -1
  18. package/dist/reactor.js +10 -6
  19. package/dist/reactor.js.map +1 -1
  20. package/dist/types.d.ts +38 -7
  21. package/dist/types.d.ts.map +1 -1
  22. package/dist/utils.d.ts +4 -4
  23. package/dist/utils.d.ts.map +1 -1
  24. package/dist/utils.js +33 -10
  25. package/dist/utils.js.map +1 -1
  26. package/dist/visitor/arguments/helpers.d.ts +55 -0
  27. package/dist/visitor/arguments/helpers.d.ts.map +1 -0
  28. package/dist/visitor/arguments/helpers.js +123 -0
  29. package/dist/visitor/arguments/helpers.js.map +1 -0
  30. package/dist/visitor/arguments/index.d.ts +101 -0
  31. package/dist/visitor/arguments/index.d.ts.map +1 -0
  32. package/dist/visitor/arguments/index.js +780 -0
  33. package/dist/visitor/arguments/index.js.map +1 -0
  34. package/dist/visitor/arguments/types.d.ts +270 -0
  35. package/dist/visitor/arguments/types.d.ts.map +1 -0
  36. package/dist/visitor/arguments/types.js +26 -0
  37. package/dist/visitor/arguments/types.js.map +1 -0
  38. package/dist/visitor/constants.d.ts +4 -0
  39. package/dist/visitor/constants.d.ts.map +1 -0
  40. package/dist/visitor/constants.js +73 -0
  41. package/dist/visitor/constants.js.map +1 -0
  42. package/dist/visitor/helpers.d.ts +30 -0
  43. package/dist/visitor/helpers.d.ts.map +1 -0
  44. package/dist/visitor/helpers.js +204 -0
  45. package/dist/visitor/helpers.js.map +1 -0
  46. package/dist/visitor/index.d.ts +5 -0
  47. package/dist/visitor/index.d.ts.map +1 -0
  48. package/dist/visitor/index.js +5 -0
  49. package/dist/visitor/index.js.map +1 -0
  50. package/dist/visitor/returns/index.d.ts +38 -0
  51. package/dist/visitor/returns/index.d.ts.map +1 -0
  52. package/dist/visitor/returns/index.js +460 -0
  53. package/dist/visitor/returns/index.js.map +1 -0
  54. package/dist/visitor/returns/types.d.ts +202 -0
  55. package/dist/visitor/returns/types.d.ts.map +1 -0
  56. package/dist/visitor/returns/types.js +2 -0
  57. package/dist/visitor/returns/types.js.map +1 -0
  58. package/dist/visitor/types.d.ts +19 -0
  59. package/dist/visitor/types.d.ts.map +1 -0
  60. package/dist/visitor/types.js +2 -0
  61. package/dist/visitor/types.js.map +1 -0
  62. package/package.json +16 -7
  63. package/src/adapter.ts +446 -0
  64. package/src/constants.ts +11 -0
  65. package/src/display-reactor.ts +337 -0
  66. package/src/index.ts +8 -0
  67. package/src/metadata-display-reactor.ts +230 -0
  68. package/src/reactor.ts +199 -0
  69. package/src/types.ts +127 -0
  70. package/src/utils.ts +60 -0
  71. package/src/visitor/arguments/helpers.ts +153 -0
  72. package/src/visitor/arguments/index.test.ts +1439 -0
  73. package/src/visitor/arguments/index.ts +981 -0
  74. package/src/visitor/arguments/schema.test.ts +324 -0
  75. package/src/visitor/arguments/types.ts +387 -0
  76. package/src/visitor/constants.ts +76 -0
  77. package/src/visitor/helpers.test.ts +274 -0
  78. package/src/visitor/helpers.ts +223 -0
  79. package/src/visitor/index.ts +4 -0
  80. package/src/visitor/returns/index.test.ts +2377 -0
  81. package/src/visitor/returns/index.ts +658 -0
  82. package/src/visitor/returns/types.ts +302 -0
  83. package/src/visitor/types.ts +75 -0
@@ -0,0 +1,337 @@
1
+ import type {
2
+ CandidDisplayReactorParameters,
3
+ DynamicMethodOptions,
4
+ } from "./types"
5
+ import { CandidAdapter } from "./adapter"
6
+
7
+ import {
8
+ BaseActor,
9
+ DisplayReactorParameters,
10
+ TransformKey,
11
+ DisplayReactor,
12
+ didToDisplayCodec,
13
+ didTypeFromArray,
14
+ } from "@ic-reactor/core"
15
+ import { IDL } from "@icp-sdk/core/candid"
16
+
17
+ // ============================================================================
18
+ // CandidDisplayReactor
19
+ // ============================================================================
20
+
21
+ /**
22
+ * CandidDisplayReactor combines the display transformation capabilities of
23
+ * DisplayReactor with dynamic Candid parsing from CandidReactor.
24
+ *
25
+ * This class provides:
26
+ * - **Display transformations**: Automatic type conversion between Candid and
27
+ * display-friendly types (bigint ↔ string, Principal ↔ string, etc.)
28
+ * - **Validation**: Optional argument validation with display types
29
+ * - **Dynamic Candid parsing**: Initialize from Candid source or fetch from network
30
+ * - **Dynamic method registration**: Register methods at runtime with Candid signatures
31
+ *
32
+ * @typeParam A - The actor service type
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * import { CandidDisplayReactor } from "@ic-reactor/candid"
37
+ *
38
+ * const reactor = new CandidDisplayReactor({
39
+ * clientManager,
40
+ * canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
41
+ * })
42
+ *
43
+ * // Initialize from network (fetches Candid from canister)
44
+ * await reactor.initialize()
45
+ *
46
+ * // Or provide Candid source directly
47
+ * const reactor2 = new CandidDisplayReactor({
48
+ * clientManager,
49
+ * canisterId: "...",
50
+ * candid: `service : { greet : (text) -> (text) query }`
51
+ * })
52
+ * await reactor2.initialize()
53
+ *
54
+ * // Call methods with display types (strings instead of bigint/Principal)
55
+ * const result = await reactor.callMethod({
56
+ * functionName: "transfer",
57
+ * args: [{ to: "aaaaa-aa", amount: "1000000" }] // strings!
58
+ * })
59
+ *
60
+ * // Add validation
61
+ * reactor.registerValidator("transfer", ([input]) => {
62
+ * if (!input.to) {
63
+ * return { success: false, issues: [{ path: ["to"], message: "Required" }] }
64
+ * }
65
+ * return { success: true }
66
+ * })
67
+ * ```
68
+ */
69
+ export class CandidDisplayReactor<
70
+ A = BaseActor,
71
+ T extends TransformKey = "display",
72
+ > extends DisplayReactor<A, T> {
73
+ public readonly transform = "display" as T
74
+ public adapter: CandidAdapter
75
+ private candidSource?: string
76
+
77
+ constructor(config: CandidDisplayReactorParameters<A>) {
78
+ const superConfig = { ...config }
79
+
80
+ // If funcClass is provided, build an idlFactory from it
81
+ if (config.funcClass && !superConfig.idlFactory) {
82
+ const { methodName, func } = config.funcClass
83
+ superConfig.idlFactory = ({ IDL: _IDL }) =>
84
+ _IDL.Service({ [methodName]: func })
85
+ }
86
+
87
+ if (!superConfig.idlFactory) {
88
+ superConfig.idlFactory = ({ IDL }) => IDL.Service({})
89
+ }
90
+
91
+ super(superConfig as DisplayReactorParameters<A>)
92
+
93
+ this.candidSource = config.candid
94
+
95
+ if (config.adapter) {
96
+ this.adapter = config.adapter
97
+ } else {
98
+ this.adapter = new CandidAdapter({
99
+ clientManager: this.clientManager,
100
+ })
101
+ }
102
+ }
103
+
104
+ // ══════════════════════════════════════════════════════════════════════════
105
+ // INITIALIZATION
106
+ // ══════════════════════════════════════════════════════════════════════════
107
+
108
+ /**
109
+ * Initializes the reactor by parsing the provided Candid string or fetching it from the network.
110
+ * This updates the internal service definition with the actual canister interface.
111
+ *
112
+ * After initialization, all DisplayReactor methods work with display type transformations.
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * const reactor = new CandidDisplayReactor({
117
+ * clientManager,
118
+ * canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
119
+ * })
120
+ *
121
+ * // Fetches Candid from the canister and initializes
122
+ * await reactor.initialize()
123
+ *
124
+ * // Now you can call methods with display types
125
+ * const balance = await reactor.callMethod({
126
+ * functionName: "icrc1_balance_of",
127
+ * args: [{ owner: "aaaaa-aa" }] // Principal as string!
128
+ * })
129
+ * ```
130
+ */
131
+ public async initialize(): Promise<void> {
132
+ let idlFactory: IDL.InterfaceFactory
133
+
134
+ if (this.candidSource) {
135
+ const definition = await this.adapter.parseCandidSource(this.candidSource)
136
+ idlFactory = definition.idlFactory
137
+ } else {
138
+ const definition = await this.adapter.getCandidDefinition(this.canisterId)
139
+ idlFactory = definition.idlFactory
140
+ }
141
+
142
+ this.service = idlFactory({ IDL })
143
+
144
+ // Re-initialize codecs after service is updated
145
+ this.reinitializeCodecs()
146
+ }
147
+
148
+ /**
149
+ * Re-initialize the display codecs after the service has been updated.
150
+ * This is called automatically after initialize() or registerMethod().
151
+ */
152
+ private reinitializeCodecs(): void {
153
+ const fields = this.getServiceInterface()?._fields
154
+ if (!fields) return
155
+
156
+ // Access the private codecs map from DisplayReactor
157
+ const codecs = (this as any).codecs as Map<
158
+ string,
159
+ { args: any; result: any }
160
+ >
161
+
162
+ for (const [methodName, funcType] of fields) {
163
+ // Skip if already exists
164
+ if (codecs.has(methodName)) continue
165
+
166
+ const argsIdlType = didTypeFromArray(funcType.argTypes)
167
+ const retIdlType = didTypeFromArray(funcType.retTypes)
168
+
169
+ codecs.set(methodName, {
170
+ args: didToDisplayCodec(argsIdlType),
171
+ result: didToDisplayCodec(retIdlType),
172
+ })
173
+ }
174
+ }
175
+
176
+ // ══════════════════════════════════════════════════════════════════════════
177
+ // DYNAMIC METHOD REGISTRATION
178
+ // ══════════════════════════════════════════════════════════════════════════
179
+
180
+ /**
181
+ * Register a dynamic method by its Candid signature.
182
+ * After registration, all DisplayReactor methods work with display type transformations.
183
+ *
184
+ * @example
185
+ * ```typescript
186
+ * // Register a method
187
+ * await reactor.registerMethod({
188
+ * functionName: "icrc1_balance_of",
189
+ * candid: "(record { owner : principal }) -> (nat) query"
190
+ * })
191
+ *
192
+ * // Now use with display types!
193
+ * const balance = await reactor.callMethod({
194
+ * functionName: "icrc1_balance_of",
195
+ * args: [{ owner: "aaaaa-aa" }] // Principal as string
196
+ * })
197
+ * // balance is string (not bigint) due to display transformation
198
+ * ```
199
+ */
200
+ public async registerMethod(options: DynamicMethodOptions): Promise<void> {
201
+ const { functionName, candid } = options
202
+
203
+ // Check if method already registered
204
+ const existing = this.service._fields.find(
205
+ ([name]) => name === functionName
206
+ )
207
+ if (existing) return
208
+
209
+ // Parse the Candid signature
210
+ const serviceSource = candid.includes("service :")
211
+ ? candid
212
+ : `service : { ${functionName} : ${candid}; }`
213
+
214
+ const { idlFactory } = await this.adapter.parseCandidSource(serviceSource)
215
+ const parsedService = idlFactory({ IDL })
216
+
217
+ const funcField = parsedService._fields.find(
218
+ ([name]) => name === functionName
219
+ )
220
+ if (!funcField) {
221
+ throw new Error(
222
+ `Method "${functionName}" not found in the provided Candid signature`
223
+ )
224
+ }
225
+
226
+ // Inject into our service
227
+ this.service._fields.push(funcField)
228
+
229
+ // Re-initialize codecs for the new method
230
+ this.reinitializeCodecs()
231
+ }
232
+
233
+ /**
234
+ * Register multiple methods at once.
235
+ *
236
+ * @example
237
+ * ```typescript
238
+ * await reactor.registerMethods([
239
+ * { functionName: "icrc1_balance_of", candid: "(record { owner : principal }) -> (nat) query" },
240
+ * { functionName: "icrc1_transfer", candid: "(record { to : principal; amount : nat }) -> (variant { Ok : nat; Err : text })" }
241
+ * ])
242
+ * ```
243
+ */
244
+ public async registerMethods(methods: DynamicMethodOptions[]): Promise<void> {
245
+ await Promise.all(methods.map((m) => this.registerMethod(m)))
246
+ }
247
+
248
+ /**
249
+ * Check if a method is registered (either from initialize or registerMethod).
250
+ */
251
+ public hasMethod(functionName: string): boolean {
252
+ return this.service._fields.some(([name]) => name === functionName)
253
+ }
254
+
255
+ /**
256
+ * Get all registered method names.
257
+ */
258
+ public getMethodNames(): string[] {
259
+ return this.service._fields.map(([name]) => name)
260
+ }
261
+
262
+ // ══════════════════════════════════════════════════════════════════════════
263
+ // DYNAMIC CALL SHORTCUTS
264
+ // ══════════════════════════════════════════════════════════════════════════
265
+
266
+ /**
267
+ * Perform a dynamic update call in one step with display type transformations.
268
+ * Registers the method if not already registered, then calls it.
269
+ *
270
+ * @example
271
+ * ```typescript
272
+ * const result = await reactor.callDynamic({
273
+ * functionName: "transfer",
274
+ * candid: "(record { to : principal; amount : nat }) -> (variant { Ok : nat; Err : text })",
275
+ * args: [{ to: "aaaaa-aa", amount: "100" }] // Display types!
276
+ * })
277
+ * ```
278
+ */
279
+ public async callDynamic<T = unknown>(
280
+ options: DynamicMethodOptions & { args?: unknown[] }
281
+ ): Promise<T> {
282
+ await this.registerMethod(options)
283
+ return this.callMethod({
284
+ functionName: options.functionName as any,
285
+ args: options.args as any,
286
+ }) as T
287
+ }
288
+
289
+ /**
290
+ * Perform a dynamic query call in one step with display type transformations.
291
+ * Registers the method if not already registered, then calls it.
292
+ *
293
+ * @example
294
+ * ```typescript
295
+ * const balance = await reactor.queryDynamic({
296
+ * functionName: "icrc1_balance_of",
297
+ * candid: "(record { owner : principal }) -> (nat) query",
298
+ * args: [{ owner: "aaaaa-aa" }] // Display types!
299
+ * })
300
+ * // balance is string (not BigInt)
301
+ * ```
302
+ */
303
+ public async queryDynamic<T = unknown>(
304
+ options: DynamicMethodOptions & { args?: unknown[] }
305
+ ): Promise<T> {
306
+ await this.registerMethod(options)
307
+ return this.callMethod({
308
+ functionName: options.functionName as any,
309
+ args: options.args as any,
310
+ }) as T
311
+ }
312
+
313
+ /**
314
+ * Fetch with dynamic Candid and TanStack Query caching.
315
+ * Registers the method if not already registered, then fetches with caching.
316
+ * Results are transformed to display types.
317
+ *
318
+ * @example
319
+ * ```typescript
320
+ * const balance = await reactor.fetchQueryDynamic({
321
+ * functionName: "icrc1_balance_of",
322
+ * candid: "(record { owner : principal }) -> (nat) query",
323
+ * args: [{ owner: "aaaaa-aa" }]
324
+ * })
325
+ * // Subsequent calls with same args return cached result
326
+ * ```
327
+ */
328
+ public async fetchQueryDynamic<T = unknown>(
329
+ options: DynamicMethodOptions & { args?: unknown[] }
330
+ ): Promise<T> {
331
+ await this.registerMethod(options)
332
+ return this.fetchQuery({
333
+ functionName: options.functionName as any,
334
+ args: options.args as any,
335
+ }) as T
336
+ }
337
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from "./adapter"
2
+ export * from "./types"
3
+ export * from "./visitor"
4
+ export * from "./constants"
5
+ export * from "./utils"
6
+ export * from "./reactor"
7
+ export * from "./display-reactor"
8
+ export * from "./metadata-display-reactor"
@@ -0,0 +1,230 @@
1
+ import type {
2
+ ActorMethodReturnType,
3
+ BaseActor,
4
+ FunctionName,
5
+ } from "@ic-reactor/core"
6
+ import { CandidDisplayReactor } from "./display-reactor"
7
+ import type {
8
+ CandidDisplayReactorParameters,
9
+ DynamicMethodOptions,
10
+ } from "./types"
11
+ import {
12
+ FieldVisitor,
13
+ ArgumentsMeta,
14
+ ArgumentsServiceMeta,
15
+ MetadataError,
16
+ } from "./visitor/arguments"
17
+ import {
18
+ MethodMeta,
19
+ MethodResult,
20
+ ResultFieldVisitor,
21
+ ServiceMeta,
22
+ } from "./visitor/returns"
23
+
24
+ // ============================================================================
25
+ // MetadataDisplayReactor
26
+ // ============================================================================
27
+
28
+ /**
29
+ * MetadataDisplayReactor combines visitor-based metadata generation
30
+ * for both input forms and result display.
31
+ *
32
+ * ## Architecture
33
+ *
34
+ * It extends the base Reactor and adds metadata generation capabilities.
35
+ * Unlike DisplayReactor, it does not use a separate codec for transformation.
36
+ * Instead, it uses the metadata visitor to resolve raw values into display-ready structures.
37
+ *
38
+ * ## Usage
39
+ *
40
+ * ```typescript
41
+ * const reactor = new MetadataDisplayReactor({
42
+ * canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
43
+ * clientManager,
44
+ * name: "ICPLedger",
45
+ * })
46
+ *
47
+ * await reactor.initialize()
48
+ *
49
+ * // Get form metadata
50
+ * const argMeta = reactor.getInputMeta("icrc1_transfer")
51
+ * console.log(argMeta.args) // Field descriptors
52
+ * console.log(argMeta.defaults) // Default values
53
+ *
54
+ * // Get result metadata
55
+ * const resultMeta = reactor.getOutputMeta("icrc1_transfer")
56
+ *
57
+ * // Call with display types
58
+ * const result = await reactor.callMethod({
59
+ * functionName: "icrc1_transfer",
60
+ * args: [{ to: { owner: "aaaaa-aa" }, amount: "1000000" }]
61
+ * })
62
+ * ```
63
+ */
64
+ declare module "@ic-reactor/core" {
65
+ interface TransformArgsRegistry<T> {
66
+ metadata: TransformArgsRegistry<T>["display"]
67
+ }
68
+ interface TransformReturnRegistry<T, A = BaseActor> {
69
+ metadata: MethodResult<A>
70
+ }
71
+ }
72
+
73
+ export class MetadataDisplayReactor<A = BaseActor> extends CandidDisplayReactor<
74
+ A,
75
+ "metadata"
76
+ > {
77
+ public override readonly transform = "metadata" as const
78
+
79
+ // Metadata storage
80
+ private argumentMeta: ArgumentsServiceMeta<A> | null = null
81
+ private resultMeta: ServiceMeta<A> | null = null
82
+
83
+ // Visitors (stateless, can be reused)
84
+ private static argVisitor = new FieldVisitor()
85
+ private static resultVisitor = new ResultFieldVisitor()
86
+
87
+ constructor(config: CandidDisplayReactorParameters<A>) {
88
+ super(config)
89
+
90
+ // If funcClass was provided, metadata is ready immediately
91
+ if (config.funcClass || config.idlFactory) {
92
+ this.generateMetadata()
93
+ }
94
+ }
95
+
96
+ // ══════════════════════════════════════════════════════════════════════════
97
+ // INITIALIZATION
98
+ // ══════════════════════════════════════════════════════════════════════════
99
+
100
+ /**
101
+ * Initializes the reactor by parsing Candid and generating all metadata.
102
+ */
103
+ public override async initialize(): Promise<void> {
104
+ await super.initialize()
105
+
106
+ // Generate metadata using visitors
107
+ this.generateMetadata()
108
+ }
109
+
110
+ /**
111
+ * Generate all metadata from the service interface using visitors.
112
+ */
113
+ private generateMetadata(): void {
114
+ const service = this.getServiceInterface()
115
+ if (!service) return
116
+
117
+ // Generate argument metadata
118
+ this.argumentMeta = service.accept(
119
+ MetadataDisplayReactor.argVisitor,
120
+ null as any
121
+ ) as ArgumentsServiceMeta<A>
122
+
123
+ // Generate result metadata
124
+ this.resultMeta = service.accept(
125
+ MetadataDisplayReactor.resultVisitor,
126
+ null as any
127
+ ) as ServiceMeta<A>
128
+ }
129
+
130
+ // ══════════════════════════════════════════════════════════════════════════
131
+ // METADATA ACCESS
132
+ // ══════════════════════════════════════════════════════════════════════════
133
+
134
+ /**
135
+ * Get input field metadata for a method.
136
+ * Use this to generate input forms.
137
+ *
138
+ * @param methodName The method name to get metadata for
139
+ * @returns ArgumentsMeta containing args, defaults, and validation schema
140
+ */
141
+ public getInputMeta<M extends FunctionName<A>>(
142
+ methodName: M
143
+ ): ArgumentsMeta<A, M> | undefined {
144
+ return this.argumentMeta?.[methodName]
145
+ }
146
+
147
+ /**
148
+ * Get output field metadata for a method.
149
+ * Use this to render results.
150
+ *
151
+ * @param methodName The method name to get metadata for
152
+ * @returns MethodMeta containing return schema and resolve function
153
+ */
154
+ public getOutputMeta<M extends FunctionName<A>>(
155
+ methodName: M
156
+ ): MethodMeta<A, M> | undefined {
157
+ return this.resultMeta?.[methodName]
158
+ }
159
+
160
+ /**
161
+ * Get all input metadata for all methods.
162
+ */
163
+ public getAllInputMeta(): ArgumentsServiceMeta<A> | null {
164
+ return this.argumentMeta
165
+ }
166
+
167
+ /**
168
+ * Get all output metadata for all methods.
169
+ */
170
+ public getAllOutputMeta(): ServiceMeta<A> | null {
171
+ return this.resultMeta
172
+ }
173
+
174
+ // ══════════════════════════════════════════════════════════════════════════
175
+ // DYNAMIC METHOD REGISTRATION
176
+ // ══════════════════════════════════════════════════════════════════════════
177
+ /**
178
+ * Register a dynamic method by its Candid signature.
179
+ * After registration, all DisplayReactor methods work with display type transformations.
180
+ */
181
+ public override async registerMethod(
182
+ options: DynamicMethodOptions
183
+ ): Promise<void> {
184
+ await super.registerMethod(options)
185
+
186
+ // Regenerate metadata
187
+ this.generateMetadata()
188
+ }
189
+
190
+ // ══════════════════════════════════════════════════════════════════════════
191
+ // DYNAMIC CALL SHORTCUTS
192
+ // ══════════════════════════════════════════════════════════════════════════
193
+ protected override transformResult<M extends FunctionName<A>>(
194
+ methodName: M,
195
+ result: ActorMethodReturnType<A[M]>
196
+ ): MethodResult<A> {
197
+ // Get metadata and generate resolved result
198
+ const meta = this.getOutputMeta(methodName)
199
+ if (!meta) {
200
+ throw new MetadataError(
201
+ `No output metadata found for method`,
202
+ String(methodName),
203
+ "method"
204
+ )
205
+ }
206
+
207
+ return meta.resolve(result)
208
+ }
209
+
210
+ /**
211
+ * Perform a dynamic call and return result with metadata.
212
+ *
213
+ * @param options Method registration and call options
214
+ * @returns Object containing the result and metadata
215
+ */
216
+ public async callDynamicWithMeta<T = unknown>(
217
+ options: DynamicMethodOptions & { args?: unknown[] }
218
+ ): Promise<{ result: T; meta: MethodMeta<A> }> {
219
+ await this.registerMethod(options)
220
+
221
+ const result = (await this.callMethod({
222
+ functionName: options.functionName as any,
223
+ args: options.args as any,
224
+ })) as T
225
+
226
+ const meta = this.getOutputMeta(options.functionName as any)!
227
+
228
+ return { result, meta }
229
+ }
230
+ }