@ic-reactor/core 3.0.7-beta.1 → 3.0.7-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,7 +92,8 @@ import { idlFactory, type _SERVICE } from "./declarations/my_canister"
92
92
  const backend = new Reactor<_SERVICE>({
93
93
  clientManager,
94
94
  idlFactory,
95
- canisterId: "rrkah-fqaaa-aaaaa-aaaaq-cai",
95
+ name: "backend", // Required: explicit name
96
+ // canisterId: "...", // Optional: omitted if using environment variables
96
97
  })
97
98
  ```
98
99
 
@@ -131,6 +132,7 @@ interface ClientManagerParameters {
131
132
  port?: number // Local replica port (default: 4943)
132
133
  withLocalEnv?: boolean // Force local network
133
134
  withProcessEnv?: boolean // Read DFX_NETWORK from env
135
+ withCanisterEnv?: boolean // Read canister IDs from environment
134
136
  agentOptions?: HttpAgentOptions // Custom agent options
135
137
  authClient?: AuthClient // Pre-configured auth client
136
138
  }
@@ -199,8 +201,8 @@ clientManager.isLocal // boolean
199
201
  interface ReactorParameters<A> {
200
202
  clientManager: ClientManager
201
203
  idlFactory: IDL.InterfaceFactory
202
- canisterId: string | Principal
203
- name?: string // Optional display name
204
+ name: string // Required display name
205
+ canisterId?: string | Principal // Optional if using env vars
204
206
  pollingOptions?: PollingOptions // Custom polling for update calls
205
207
  }
206
208
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ic-reactor/core",
3
- "version": "3.0.7-beta.1",
3
+ "version": "3.0.7-beta.2",
4
4
  "description": "IC Reactor Core Library",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "files": [
18
18
  "dist",
19
+ "src",
19
20
  "README.md"
20
21
  ],
21
22
  "repository": {
package/src/client.ts ADDED
@@ -0,0 +1,570 @@
1
+ import type { Identity } from "@icp-sdk/core/agent"
2
+ import type { AuthClient, AuthClientLoginOptions } from "@icp-sdk/auth/client"
3
+ import type {
4
+ ClientManagerParameters,
5
+ AgentState,
6
+ AuthState,
7
+ } from "./types/client"
8
+ import type { Principal } from "@icp-sdk/core/principal"
9
+ import type { QueryClient } from "@tanstack/react-query"
10
+
11
+ import { HttpAgent } from "@icp-sdk/core/agent"
12
+ import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"
13
+ import {
14
+ IC_HOST_NETWORK_URI,
15
+ IC_INTERNET_IDENTITY_PROVIDER,
16
+ LOCAL_INTERNET_IDENTITY_PROVIDER,
17
+ } from "./utils/constants"
18
+ import { getNetworkByHostname, getProcessEnvNetwork } from "./utils/helper"
19
+
20
+ /**
21
+ * ClientManager is a central class for managing the Internet Computer (IC) agent and authentication state.
22
+ *
23
+ * It initializes the agent (connecting to local or mainnet), handles authentication via AuthClient,
24
+ * and integrates with TanStack Query's QueryClient for state management.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * import { ClientManager } from "@ic-reactor/core";
29
+ * import { QueryClient } from "@tanstack/react-query";
30
+ *
31
+ * const queryClient = new QueryClient();
32
+ * const clientManager = new ClientManager({
33
+ * queryClient,
34
+ * withLocalEnv: true, // Use local replica
35
+ * });
36
+ *
37
+ * await clientManager.initialize();
38
+ * ```
39
+ */
40
+ export class ClientManager {
41
+ #agent: HttpAgent
42
+ #authClient?: AuthClient
43
+ #identitySubscribers: Array<(identity: Identity) => void> = []
44
+ #agentStateSubscribers: Array<(state: AgentState) => void> = []
45
+ #authStateSubscribers: Array<(state: AuthState) => void> = []
46
+ #targetCanisterIds: Set<string> = new Set()
47
+
48
+ /**
49
+ * The TanStack QueryClient used for managing cached canister data and invalidating queries on identity changes.
50
+ */
51
+ public queryClient: QueryClient
52
+ /**
53
+ * Current state of the HttpAgent, including initialization status, network, and error information.
54
+ */
55
+ public agentState: AgentState
56
+ /**
57
+ * Current authentication state, including the active identity, authentication progress, and errors.
58
+ */
59
+ public authState: AuthState
60
+
61
+ private initPromise?: Promise<void>
62
+ private authPromise?: Promise<Identity | undefined>
63
+ private port: number
64
+ private internetIdentityId?: string
65
+
66
+ /**
67
+ * Creates a new instance of ClientManager.
68
+ *
69
+ * @param parameters - Configuration options for the agent and network environment.
70
+ */
71
+ constructor({
72
+ port = 4943,
73
+ withLocalEnv,
74
+ withProcessEnv,
75
+ withCanisterEnv,
76
+ agentOptions = {},
77
+ queryClient,
78
+ authClient,
79
+ }: ClientManagerParameters) {
80
+ this.queryClient = queryClient
81
+
82
+ this.agentState = {
83
+ isInitialized: false,
84
+ isInitializing: false,
85
+ error: undefined,
86
+ network: undefined,
87
+ isLocalhost: false,
88
+ }
89
+
90
+ this.authState = {
91
+ identity: null,
92
+ isAuthenticating: false,
93
+ isAuthenticated: false,
94
+ error: undefined,
95
+ }
96
+
97
+ this.port = port
98
+
99
+ // EXPERIMENTAL: Use canister environment from ic_env cookie when enabled
100
+ // ⚠️ This may cause issues with update calls on localhost development
101
+ if (withCanisterEnv) {
102
+ const canisterEnv =
103
+ typeof window !== "undefined" ? safeGetCanisterEnv() : undefined
104
+
105
+ if (canisterEnv) {
106
+ this.internetIdentityId =
107
+ canisterEnv["internet_identity"] ||
108
+ canisterEnv["PUBLIC_CANISTER_ID:internet_identity"] ||
109
+ canisterEnv["CANISTER_ID_INTERNET_IDENTITY"]
110
+ }
111
+
112
+ const isDev =
113
+ typeof import.meta !== "undefined" && (import.meta as any).env?.DEV
114
+
115
+ if (isDev && typeof window !== "undefined") {
116
+ agentOptions.host = agentOptions.host ?? window.location.origin
117
+ agentOptions.verifyQuerySignatures =
118
+ agentOptions.verifyQuerySignatures ?? false
119
+ } else {
120
+ agentOptions.verifyQuerySignatures =
121
+ agentOptions.verifyQuerySignatures ?? true
122
+ }
123
+
124
+ if (canisterEnv?.IC_ROOT_KEY) {
125
+ agentOptions.rootKey = agentOptions.rootKey ?? canisterEnv.IC_ROOT_KEY
126
+ }
127
+ }
128
+
129
+ if (withProcessEnv) {
130
+ const processNetwork = getProcessEnvNetwork()
131
+ if (processNetwork === "ic") {
132
+ agentOptions.host = IC_HOST_NETWORK_URI
133
+ } else if (processNetwork === "local") {
134
+ agentOptions.host =
135
+ typeof process !== "undefined" && process.env.IC_HOST
136
+ ? process.env.IC_HOST
137
+ : `http://127.0.0.1:${port}`
138
+ }
139
+ } else if (withLocalEnv) {
140
+ agentOptions.host = `http://127.0.0.1:${port}`
141
+ } else if (!withCanisterEnv) {
142
+ // Only set default host if withCanisterEnv hasn't already configured it
143
+ agentOptions.host = agentOptions.host ?? IC_HOST_NETWORK_URI
144
+ }
145
+
146
+ this.#agent = HttpAgent.createSync(agentOptions)
147
+ this.updateAgentState({
148
+ isLocalhost: this.isLocal,
149
+ network: this.network,
150
+ })
151
+
152
+ if (authClient) {
153
+ this.#authClient = authClient
154
+ const identity = this.#authClient.getIdentity()
155
+ this.updateAgent(identity)
156
+ this.authState = {
157
+ identity,
158
+ isAuthenticated: !identity.getPrincipal().isAnonymous(),
159
+ isAuthenticating: false,
160
+ error: undefined,
161
+ }
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Orchestrates the complete initialization of the ClientManager.
167
+ * This method awaits the agent's core initialization (e.g., fetching root keys)
168
+ * and triggers the authentication (session restoration) in the background.
169
+ *
170
+ * @returns A promise that resolves to the ClientManager instance when core initialization is complete.
171
+ */
172
+ public async initialize() {
173
+ await this.initializeAgent()
174
+ this.authenticate()
175
+ return this
176
+ }
177
+
178
+ /**
179
+ * Specifically initializes the HttpAgent.
180
+ * On local networks, this includes fetching the root key for certificate verification.
181
+ *
182
+ * @returns A promise that resolves when the agent is fully initialized.
183
+ */
184
+ public async initializeAgent() {
185
+ if (this.agentState.isInitialized) {
186
+ return
187
+ }
188
+ if (this.agentState.isInitializing) {
189
+ return this.initPromise
190
+ }
191
+
192
+ this.initPromise = (async () => {
193
+ this.updateAgentState({ isInitializing: true })
194
+ if (typeof window !== "undefined") {
195
+ console.info(
196
+ `%cic-reactor:%c Initializing agent for ${this.network} network`,
197
+ "color: #3b82f6; font-weight: bold",
198
+ "color: inherit",
199
+ {
200
+ host: this.agentHost?.toString(),
201
+ isLocal: this.isLocal,
202
+ }
203
+ )
204
+ }
205
+ try {
206
+ if (this.isLocal) {
207
+ await this.#agent.fetchRootKey()
208
+ }
209
+ this.updateAgentState({ isInitialized: true, isInitializing: false })
210
+ } catch (error) {
211
+ this.updateAgentState({
212
+ error: error as Error,
213
+ isInitializing: false,
214
+ })
215
+ this.initPromise = undefined
216
+ throw error
217
+ }
218
+ })()
219
+
220
+ return this.initPromise
221
+ }
222
+
223
+ private authModuleMissing = false
224
+
225
+ /**
226
+ * Attempts to initialize the authentication client and restore a previous session.
227
+ *
228
+ * If an `AuthClient` is already initialized (passed in constructor or previously created),
229
+ * it uses that instance. Otherwise, it dynamically imports the `@icp-sdk/auth` module
230
+ * and creates a new AuthClient.
231
+ *
232
+ * If the module is missing and no client is provided, it fails gracefully by marking authentication as unavailable.
233
+ *
234
+ * @returns A promise that resolves to the restored Identity, or undefined if auth fails or is unavailable.
235
+ */
236
+ public authenticate = async (): Promise<Identity | undefined> => {
237
+ if (this.authState.isAuthenticated) {
238
+ return this.authState.identity || undefined
239
+ }
240
+ if (this.authPromise) {
241
+ return this.authPromise
242
+ }
243
+
244
+ if (this.authModuleMissing) {
245
+ return undefined
246
+ }
247
+
248
+ this.authPromise = (async () => {
249
+ if (typeof window !== "undefined") {
250
+ console.info(
251
+ `%cic-reactor:%c Authenticating...`,
252
+ "color: #3b82f6; font-weight: bold",
253
+ "color: inherit",
254
+ {
255
+ network: this.network,
256
+ authClient: this.#authClient ? "Shared Instance" : "Dynamic Import",
257
+ }
258
+ )
259
+ }
260
+ this.updateAuthState({ isAuthenticating: true })
261
+ try {
262
+ if (!this.#authClient) {
263
+ const authModule = await import("@icp-sdk/auth/client").catch(() => {
264
+ this.authModuleMissing = true
265
+ return null
266
+ })
267
+
268
+ if (!authModule) {
269
+ this.authModuleMissing = true
270
+ this.updateAuthState({ isAuthenticating: false })
271
+ return undefined
272
+ }
273
+
274
+ const { AuthClient } = authModule
275
+ this.#authClient = await AuthClient.create()
276
+ }
277
+ const identity = this.#authClient.getIdentity()
278
+ this.updateAgent(identity)
279
+ this.updateAuthState({
280
+ identity,
281
+ isAuthenticated: !identity.getPrincipal().isAnonymous(),
282
+ isAuthenticating: false,
283
+ })
284
+ return identity
285
+ } catch (error) {
286
+ this.updateAuthState({ error: error as Error, isAuthenticating: false })
287
+ console.error("Authentication failed:", error)
288
+ throw error
289
+ } finally {
290
+ this.authPromise = undefined
291
+ }
292
+ })()
293
+
294
+ return this.authPromise
295
+ }
296
+
297
+ /**
298
+ * Triggers the login flow using the Internet Identity provider.
299
+ *
300
+ * @param loginOptions - Options for the login flow, including identity provider and callbacks.
301
+ * @throws An error if the authentication module is not installed.
302
+ */
303
+ public login = async (loginOptions?: AuthClientLoginOptions) => {
304
+ try {
305
+ // Ensure agent is initialized before login
306
+ if (!this.agentState.isInitialized) {
307
+ await this.initializeAgent()
308
+ }
309
+
310
+ if (!this.#authClient) {
311
+ await this.authenticate()
312
+ }
313
+
314
+ if (!this.#authClient) {
315
+ throw new Error(
316
+ "Authentication module is missing or failed to initialize. To use login, please install the auth package: npm install @icp-sdk/auth"
317
+ )
318
+ }
319
+
320
+ this.updateAuthState({ isAuthenticating: true, error: undefined })
321
+
322
+ // Auto-detect identity provider based on network if not provided
323
+ const identityProvider =
324
+ loginOptions?.identityProvider || this.getDefaultIdentityProvider()
325
+
326
+ await this.#authClient.login({
327
+ ...loginOptions,
328
+ identityProvider,
329
+ onSuccess: () => {
330
+ const identity = this.#authClient!.getIdentity()
331
+ if (identity) {
332
+ this.updateAgent(identity)
333
+ this.updateAuthState({
334
+ identity,
335
+ isAuthenticated: true,
336
+ isAuthenticating: false,
337
+ })
338
+ }
339
+ ;(loginOptions?.onSuccess as any)?.()
340
+ },
341
+ onError: (error) => {
342
+ this.updateAuthState({
343
+ error: new Error(error),
344
+ isAuthenticating: false,
345
+ })
346
+ loginOptions?.onError?.(error)
347
+ },
348
+ })
349
+ } catch (error) {
350
+ this.updateAuthState({
351
+ error: error as Error,
352
+ isAuthenticating: false,
353
+ })
354
+ throw error
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Logs out the user and reverts the agent to an anonymous identity.
360
+ *
361
+ * @throws An error if the authentication module is not installed.
362
+ */
363
+ public logout = async () => {
364
+ if (!this.#authClient) {
365
+ throw new Error(
366
+ "Authentication module is missing or failed to initialize. To use logout, please install the auth package: npm install @icp-sdk/auth"
367
+ )
368
+ }
369
+ this.updateAuthState({ isAuthenticating: true, error: undefined })
370
+ await this.#authClient.logout()
371
+ const identity = this.#authClient.getIdentity()
372
+ if (identity) {
373
+ this.updateAgent(identity)
374
+ this.updateAuthState({
375
+ identity,
376
+ isAuthenticated: false,
377
+ isAuthenticating: false,
378
+ })
379
+ }
380
+ }
381
+
382
+ /**
383
+ * The underlying HttpAgent managed by this class.
384
+ */
385
+ get agent() {
386
+ return this.#agent
387
+ }
388
+
389
+ /**
390
+ * The host URL of the current IC agent.
391
+ */
392
+ get agentHost(): URL | undefined {
393
+ return this.#agent.host
394
+ }
395
+
396
+ /**
397
+ * The hostname of the current IC agent.
398
+ */
399
+ get agentHostName() {
400
+ return this.agentHost?.hostname || ""
401
+ }
402
+
403
+ /**
404
+ * Returns true if the agent is connecting to a local environment.
405
+ */
406
+ get isLocal() {
407
+ return this.network !== "ic"
408
+ }
409
+
410
+ /**
411
+ * Returns the current network type ('ic' or 'local').
412
+ */
413
+ get network() {
414
+ const hostname = this.agentHostName
415
+ return getNetworkByHostname(hostname)
416
+ }
417
+
418
+ /**
419
+ * Returns the current user's Principal identity.
420
+ */
421
+ public getUserPrincipal() {
422
+ return this.#agent.getPrincipal()
423
+ }
424
+
425
+ /**
426
+ * Registers a canister ID that this agent will interact with.
427
+ * This is used for informational purposes and network detection.
428
+ */
429
+ public registerCanisterId(canisterId: string, name?: string): void {
430
+ if (typeof window !== "undefined") {
431
+ const actorName = name || canisterId
432
+ console.info(
433
+ `%cic-reactor:%c Adding actor ${actorName}`,
434
+ "color: #3b82f6; font-weight: bold",
435
+ "color: inherit",
436
+ {
437
+ network: this.network,
438
+ canisterId,
439
+ ...(name && { name }),
440
+ }
441
+ )
442
+ }
443
+ this.#targetCanisterIds.add(canisterId)
444
+ }
445
+
446
+ /**
447
+ * Returns a list of all canister IDs registered with this agent.
448
+ */
449
+ public connectedCanisterIds(): string[] {
450
+ return Array.from(this.#targetCanisterIds)
451
+ }
452
+
453
+ /**
454
+ * Get the subnet ID for a canister.
455
+ */
456
+ public getSubnetIdFromCanister(canisterId: string) {
457
+ return this.#agent.getSubnetIdFromCanister(canisterId)
458
+ }
459
+
460
+ /**
461
+ * Sync time with a specific subnet.
462
+ */
463
+ public syncTimeWithSubnet(subnetId: Principal) {
464
+ return this.#agent.syncTimeWithSubnet(subnetId)
465
+ }
466
+
467
+ private getDefaultIdentityProvider(): string {
468
+ if (this.isLocal) {
469
+ if (this.internetIdentityId) {
470
+ return `http://${this.internetIdentityId}.localhost:${this.port}`
471
+ }
472
+ return LOCAL_INTERNET_IDENTITY_PROVIDER
473
+ } else {
474
+ return IC_INTERNET_IDENTITY_PROVIDER
475
+ }
476
+ }
477
+
478
+ /**
479
+ * Subscribes to identity changes (e.g., after login/logout).
480
+ * @param callback - Function called with the new identity.
481
+ * @returns An unsubscribe function.
482
+ */
483
+ public subscribe(callback: (identity: Identity) => void) {
484
+ this.#identitySubscribers.push(callback)
485
+ return () => {
486
+ this.#identitySubscribers = this.#identitySubscribers.filter(
487
+ (sub) => sub !== callback
488
+ )
489
+ }
490
+ }
491
+
492
+ /**
493
+ * Subscribes to changes in the agent's initialization state.
494
+ * @param callback - Function called with the updated agent state.
495
+ * @returns An unsubscribe function.
496
+ */
497
+ public subscribeAgentState(callback: (state: AgentState) => void) {
498
+ this.#agentStateSubscribers.push(callback)
499
+ return () => {
500
+ this.#agentStateSubscribers = this.#agentStateSubscribers.filter(
501
+ (sub) => sub !== callback
502
+ )
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Subscribes to changes in the authentication state.
508
+ * @param callback - Function called with the updated authentication state.
509
+ * @returns An unsubscribe function.
510
+ */
511
+ public subscribeAuthState(callback: (state: AuthState) => void) {
512
+ this.#authStateSubscribers.push(callback)
513
+ return () => {
514
+ this.#authStateSubscribers = this.#authStateSubscribers.filter(
515
+ (sub) => sub !== callback
516
+ )
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Replaces the current agent's identity and invalidates TanStack queries.
522
+ * @param identity - The new identity to use.
523
+ */
524
+ public updateAgent(identity: Identity) {
525
+ if (typeof window !== "undefined") {
526
+ console.info(
527
+ `%cic-reactor:%c Updating agent identity`,
528
+ "color: #3b82f6; font-weight: bold",
529
+ "color: inherit",
530
+ {
531
+ principal: identity.getPrincipal().toText(),
532
+ }
533
+ )
534
+ }
535
+ // Cancel all queries for connected canisters to prevent race conditions
536
+ // with the old identity
537
+ this.connectedCanisterIds().forEach((canisterId) => {
538
+ this.queryClient.cancelQueries({
539
+ queryKey: [canisterId],
540
+ })
541
+ })
542
+
543
+ this.#agent.replaceIdentity(identity)
544
+ this.notifySubscribers(identity)
545
+ this.queryClient.invalidateQueries()
546
+ }
547
+
548
+ private notifySubscribers(identity: Identity) {
549
+ this.#identitySubscribers.forEach((sub) => sub(identity))
550
+ }
551
+
552
+ private notifyAgentStateSubscribers(state: AgentState) {
553
+ this.#agentStateSubscribers.forEach((sub) => sub(state))
554
+ }
555
+
556
+ private notifyAuthStateSubscribers(state: AuthState) {
557
+ this.#authStateSubscribers.forEach((sub) => sub(state))
558
+ }
559
+
560
+ private updateAgentState(newState: Partial<AgentState>) {
561
+ this.agentState = { ...this.agentState, ...newState }
562
+ this.notifyAgentStateSubscribers(this.agentState)
563
+ }
564
+
565
+ private updateAuthState(newState: Partial<AuthState>) {
566
+ console.debug("Updating Auth State:", newState)
567
+ this.authState = { ...this.authState, ...newState }
568
+ this.notifyAuthStateSubscribers(this.authState)
569
+ }
570
+ }
@@ -0,0 +1,92 @@
1
+ import { IDL } from "@icp-sdk/core/candid"
2
+ import { ActorDisplayCodec, DisplayOf, DisplayCodec } from "./types"
3
+ import { DisplayCodecVisitor } from "./visitor"
4
+
5
+ export function didToDisplayCodec<
6
+ TCandid = unknown,
7
+ TDisplay = DisplayOf<TCandid>,
8
+ >(didType: IDL.Type): ActorDisplayCodec<TCandid, TDisplay> {
9
+ const visitor = new DisplayCodecVisitor()
10
+ const codec = visitor.visitType(didType, null) as DisplayCodec<
11
+ TCandid,
12
+ TDisplay
13
+ >
14
+ // Return a unified interface with both native methods and convenience aliases
15
+ return {
16
+ codec,
17
+ asDisplay: (val: TCandid) => codec.decode(val) as TDisplay,
18
+ asCandid: (val: TDisplay) => codec.encode(val) as TCandid,
19
+ }
20
+ }
21
+
22
+ export function didToDisplayCodecs<TTypes extends Record<string, IDL.Type>>(
23
+ didTypes: TTypes
24
+ ): {
25
+ [K in keyof TTypes]: TTypes[K] extends IDL.Type<infer TCandid>
26
+ ? ActorDisplayCodec<TCandid>
27
+ : never
28
+ } {
29
+ const result = {} as any
30
+
31
+ for (const [name, idlType] of Object.entries(didTypes)) {
32
+ result[name] = didToDisplayCodec(idlType)
33
+ }
34
+
35
+ return result
36
+ }
37
+
38
+ export function transformArgsWithCodec<T extends unknown[]>(
39
+ argsCodec: ActorDisplayCodec,
40
+ args: unknown[] | undefined
41
+ ): T {
42
+ // Empty args - no transformation needed
43
+ // This handles methods with 0 arguments where codec is IDL.Null
44
+ if (!args || args.length === 0) {
45
+ return (args || []) as T
46
+ }
47
+
48
+ // Single argument - unwrap from array and transform
49
+ if (args.length === 1) {
50
+ try {
51
+ return [argsCodec.asCandid(args[0])] as T
52
+ } catch {
53
+ // Fallback: return as-is if transformation fails
54
+ return args as T
55
+ }
56
+ }
57
+
58
+ // Multiple arguments - transform as tuple
59
+ try {
60
+ return argsCodec.asCandid(args) as T
61
+ } catch {
62
+ // Fallback: return as-is if transformation fails
63
+ return args as T
64
+ }
65
+ }
66
+
67
+ export function transformResultWithCodec<T>(
68
+ resultCodec: ActorDisplayCodec,
69
+ result: unknown
70
+ ): T {
71
+ // Null/undefined results - no transformation needed
72
+ if (result === null || result === undefined) {
73
+ return result as T
74
+ }
75
+
76
+ try {
77
+ return resultCodec.asDisplay(result) as T
78
+ } catch {
79
+ // Fallback: return as-is if transformation fails
80
+ return result as T
81
+ }
82
+ }
83
+
84
+ export function didTypeFromArray(types: IDL.Type[]): IDL.Type {
85
+ if (types.length === 0) {
86
+ return IDL.Null
87
+ }
88
+ if (types.length === 1) {
89
+ return types[0]
90
+ }
91
+ return IDL.Tuple(...types)
92
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./visitor"
2
+ export * from "./helper"
3
+ export * from "./types"