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