Skip to content

Commit 90d77b6

Browse files
Merge pull request #557 from dao-xyz/feat/canonical-open-dx
feat: canonical open DX improvements
2 parents 3885fc9 + 75e08ab commit 90d77b6

File tree

7 files changed

+370
-25
lines changed

7 files changed

+370
-25
lines changed

packages/clients/canonical/client/src/auto.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getSchema } from "@dao-xyz/borsh";
12
import type {
23
Address,
34
OpenOptions,
@@ -13,12 +14,31 @@ export type CanonicalOpenResult<T> = {
1314
address?: Address;
1415
};
1516

17+
/**
18+
* Canonical open supports "proxy parents" (managed proxies returned by adapters),
19+
* which are not instances of `Program`. This widens `OpenOptions.parent` so apps
20+
* don't need `parent: proxy as any`.
21+
*/
22+
export type CanonicalOpenOptions<S extends Program<any>> = Omit<
23+
OpenOptions<S>,
24+
"parent"
25+
> & {
26+
parent?: unknown;
27+
};
28+
1629
export type CanonicalOpenAdapter<
1730
S extends Program<any> = Program<any>,
1831
T = unknown,
1932
> = {
2033
name: string;
21-
canOpen(program: Program<any>): program is S;
34+
/**
35+
* Optional list of borsh @variant strings this adapter can open.
36+
* If `canOpen` is omitted, canonical open will match by comparing
37+
* `getSchema(program.constructor).variant` against these values.
38+
*/
39+
variant?: string;
40+
variants?: string[];
41+
canOpen?(program: Program<any>): program is S;
2242
getKey?(program: S, options?: OpenOptions<S>): string | undefined;
2343
open(ctx: {
2444
program: S;
@@ -28,6 +48,41 @@ export type CanonicalOpenAdapter<
2848
}): Promise<CanonicalOpenResult<T>>;
2949
};
3050

51+
export const getProgramVariant = (program: Program<any>): string | undefined => {
52+
if (!program || typeof program !== "object") return undefined;
53+
try {
54+
const schema = getSchema((program as any).constructor);
55+
const variant = schema?.variant;
56+
return typeof variant === "string" ? variant : undefined;
57+
} catch {
58+
return undefined;
59+
}
60+
};
61+
62+
export const createVariantAdapter = <
63+
S extends Program<any> = Program<any>,
64+
T = unknown,
65+
>(options: {
66+
name: string;
67+
variant: string | string[];
68+
getKey?: (program: S, options?: OpenOptions<S>) => string | undefined;
69+
open: CanonicalOpenAdapter<S, T>["open"];
70+
}): CanonicalOpenAdapter<S, T> => {
71+
const variants = (
72+
Array.isArray(options.variant) ? options.variant : [options.variant]
73+
).map(String);
74+
return {
75+
name: options.name,
76+
variants,
77+
canOpen: (program: Program<any>): program is S => {
78+
const candidate = getProgramVariant(program);
79+
return !!candidate && variants.includes(candidate);
80+
},
81+
getKey: options.getKey,
82+
open: options.open,
83+
};
84+
};
85+
3186
type ManagedProxy = {
3287
parents: any[];
3388
children: any[];

packages/clients/canonical/client/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ export * from "./client.js";
22
export type {
33
CanonicalOpenAdapter,
44
CanonicalOpenMode,
5+
CanonicalOpenOptions,
56
CanonicalOpenResult,
67
} from "./auto.js";
8+
export { createVariantAdapter, getProgramVariant } from "./auto.js";
79
export * from "./service-worker.js";
810
export * from "./window.js";
911
export * from "./peerbit.js";

packages/clients/canonical/client/src/peerbit.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import type { Address, OpenOptions, ProgramClient } from "@peerbit/program";
1717
import {
1818
type CanonicalOpenAdapter,
1919
type CanonicalOpenMode,
20+
type CanonicalOpenOptions,
2021
createManagedProxy,
22+
getProgramVariant,
2123
} from "./auto.js";
2224
import type { CanonicalChannel } from "./client.js";
2325
import type { CanonicalClient } from "./index.js";
@@ -242,7 +244,7 @@ export class PeerbitCanonicalClient {
242244

243245
async open<S extends Program<any>>(
244246
storeOrAddress: S | Address,
245-
openOptions: OpenOptions<S> = {},
247+
openOptions: CanonicalOpenOptions<S> = {},
246248
): Promise<S> {
247249
const state = this.openState;
248250
if (!state || state.adapters.length === 0) {
@@ -269,16 +271,29 @@ export class PeerbitCanonicalClient {
269271
}
270272

271273
const program = storeOrAddress as Program<any>;
274+
const programVariant = getProgramVariant(program);
272275
const adapter = state.adapters.find((candidate) =>
273-
candidate.canOpen(program),
276+
typeof candidate.canOpen === "function"
277+
? candidate.canOpen(program)
278+
: !!programVariant &&
279+
(candidate.variants ?? (candidate.variant ? [candidate.variant] : []))
280+
.map(String)
281+
.includes(programVariant),
274282
);
275283
if (!adapter) {
284+
const knownVariants = state.adapters
285+
.flatMap((candidate) =>
286+
candidate.variants ?? (candidate.variant ? [candidate.variant] : []),
287+
)
288+
.filter((x): x is string => typeof x === "string" && x.length > 0);
276289
throw new Error(
277-
`No canonical adapter registered for ${program.constructor?.name ?? "program"}`,
290+
`No canonical adapter registered for ${program.constructor?.name ?? "program"}${
291+
programVariant ? ` (variant: '${programVariant}')` : ""
292+
}${knownVariants.length ? `. Known variants: ${knownVariants.join(", ")}` : ""}`,
278293
);
279294
}
280295

281-
const key = adapter.getKey?.(program as any, openOptions);
296+
const key = adapter.getKey?.(program as any, openOptions as OpenOptions<any>);
282297
if (adapter.getKey && key === undefined) {
283298
throw new Error(
284299
`Canonical adapter '${adapter.name}' requires a cache key (adapter.getKey returned undefined)`,
@@ -306,21 +321,21 @@ export class PeerbitCanonicalClient {
306321
if (openOptions?.parent) {
307322
PeerbitCanonicalClient.attachParent(
308323
existingProxy as any,
309-
openOptions.parent,
324+
openOptions.parent as any,
310325
);
311326
}
312327
return existingProxy as S;
313328
}
314329
}
315330

316-
const peer = this as any as ProgramClient;
317-
const openPromise = (async () => {
318-
const result = await adapter.open({
319-
program: program as any,
320-
options: openOptions,
321-
peer,
322-
client: this.canonical,
323-
});
331+
const peer = this as any as ProgramClient;
332+
const openPromise = (async () => {
333+
const result = await adapter.open({
334+
program: program as any,
335+
options: openOptions as OpenOptions<any>,
336+
peer,
337+
client: this.canonical,
338+
});
324339

325340
let managed: any;
326341
managed = createManagedProxy(result.proxy as any, {
@@ -336,7 +351,7 @@ export class PeerbitCanonicalClient {
336351

337352
this.openState?.proxies.add(managed);
338353
if (openOptions?.parent) {
339-
PeerbitCanonicalClient.attachParent(managed, openOptions.parent);
354+
PeerbitCanonicalClient.attachParent(managed, openOptions.parent as any);
340355
}
341356
return managed as S;
342357
})();

0 commit comments

Comments
 (0)