Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
turbocrime committed Jun 25, 2024
1 parent a55d34b commit b607f24
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .changeset/moody-bulldogs-mate.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'@penumbra-zone/client': major
---

add disconnect method
add disconnect method, add `create` module
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ lerna-debug.log*
*_pk.bin

# pack outputs
penumbra-zone-*.tgz
packages/*/penumbra-zone-*.tgz
packages/*/repo-*-*.tgz
packages/*/package

tsconfig.tsbuildinfo
10 changes: 0 additions & 10 deletions apps/extension/src/content-scripts/injected-disconnect-listener.ts

This file was deleted.

10 changes: 8 additions & 2 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,24 @@
"dist"
],
"exports": {
".": "./src/index.ts"
".": "./src/index.ts",
"./create": "./src/create.ts"
},
"publishConfig": {
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./create": {
"types": "./dist/create.d.ts",
"default": "./dist/create.js"
}
}
},
"peerDependencies": {
"@penumbra-zone/protobuf": "workspace:*",
"@penumbra-zone/transport-dom": "workspace:*"
"@penumbra-zone/transport-dom": "workspace:*",
"@connectrpc/connect": "^1.4.0"
}
}
127 changes: 127 additions & 0 deletions packages/client/src/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { createPromiseClient, type Transport } from '@connectrpc/connect';
import type { PenumbraService } from '@penumbra-zone/protobuf';
import { jsonOptions } from '@penumbra-zone/protobuf';
import {
createChannelTransport,
type ChannelTransportOptions,
} from '@penumbra-zone/transport-dom/create';
import { PenumbraSymbol, type PenumbraInjection } from '.';

// Naively return the first available provider origin, or `undefined`.
const availableOrigin = () => Object.keys(window[PenumbraSymbol] ?? {})[0];

/**
* Given a specific origin, identify the relevant injection or throw. An
* `undefined` origin is accepted but will throw.
*/
const assertProvider = (providerOrigin?: string): PenumbraInjection => {
const provider = providerOrigin && window[PenumbraSymbol]?.[providerOrigin];
if (!provider) throw new Error(`Provider ${providerOrigin} not available`);
return provider;
};

/**
* Given a specific origin, identify the relevant injection, and confirm its
* manifest is actually present or throw. An `undefined` origin is accepted but
* will throw.
*/
const assertProviderManifest = async (providerOrigin?: string): Promise<PenumbraInjection> => {
// confirm the provider injection is present
const provider = assertProvider(providerOrigin);

// confirm the provider manifest is located at the expected origin
if (new URL(provider.manifest).origin !== providerOrigin)
throw new Error('Provider manifest origin mismatch');

// confirm the provider manifest exists, can be fetched, and is json
try {
const req = await fetch(provider.manifest);
const manifest: unknown = await req.json();
if (!manifest) throw new Error('Provider manifest not present');
} catch {
throw new Error('Provider manifest not present');
}

return provider;
};

/**
* Asynchronously get a connection to the specified provider, or the first
* available provider if unspecified.
*
* Confirms presence of the provider's manifest. Will attempt to request
* approval if connection is not already active.
*
* @param requireProvider optional string identifying a provider origin
*/
export const getPenumbraPort = async (requireProvider?: string) => {
const provider = await assertProviderManifest(requireProvider ?? availableOrigin());
if (provider.isConnected() === undefined) await provider.request();
return provider.connect();
};

/**
* Synchronously create a channel transport for the specified provider, or the
* first available provider if unspecified.
*
* Will always succeed, but the transport may fail if the provider is not
* present, or if the provider rejects the connection.
*
* Confirms presence of the provider's manifest. Will attempt to request
* approval if connection is not already active.
*
* @param requireProvider optional string identifying a provider origin
* @param transportOptions optional `ChannelTransportOptions` without `getPort`
*/
export const syncCreatePenumbraChannelTransport = (
requireProvider?: string,
transportOptions: Omit<ChannelTransportOptions, 'getPort'> = { jsonOptions },
): Transport =>
createChannelTransport({
...transportOptions,
getPort: () => getPenumbraPort(requireProvider),
});

/**
* Asynchronously create a channel transport for the specified provider, or the
* first available provider if unspecified.
*
* Like `syncCreatePenumbraChannelTransport`, but awaits connection init.
*/
export const createPenumbraChannelTransport = async (
requireProvider?: string,
transportOptions: Omit<ChannelTransportOptions, 'getPort'> = { jsonOptions },
): Promise<Transport> => {
const port = await getPenumbraPort(requireProvider);
return createChannelTransport({
...transportOptions,
getPort: () => Promise.resolve(port),
});
};

/**
* Synchronously create a client for `service` from the specified provider, or the
* first available provider if unspecified.
*
* If the provider is unavailable, the client will fail to make requests.
*/
export const syncCreatePenumbraClient = <P extends PenumbraService>(
service: P,
requireProvider?: string,
) => createPromiseClient(service, syncCreatePenumbraChannelTransport(requireProvider));

/**
* Asynchronously create a client for `service` from the specified provider, or
* the first available provider if unspecified.
*
* Like `syncCreatePenumbraClient`, but awaits connection init.
*/
export const createPenumbraClient = async <P extends PenumbraService>(
service: P,
requireProvider?: string,
transportOptions?: Omit<ChannelTransportOptions, 'getPort'>,
) =>
createPromiseClient(
service,
await createPenumbraChannelTransport(requireProvider, transportOptions),
);
60 changes: 60 additions & 0 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,76 @@ export * from './error';

export const PenumbraSymbol = Symbol.for('penumbra');

/** This interface describes the simple API to request, connect, or disconnect a provider.
*
* There are three states for each provider, which may be identified by calling
* the synchronous method `isConnected()`:
* - `true`: a connection is available, and a call to `connect` should resolve
* - `false`: no connection available. calls to `connect` or `request` will fail
* - `undefined`: a `request` may be pending, or no `request` has been made
*
* Any script in scope may create an object like this, so clients should confirm
* a provider is actually present by confirming provider origin and fetching the
* provider manifest. Provider details such as name, version, website, brief
* descriptive text, and icons should be available in the manifest.
*
* Presently clients may expect the manifest is a chrome extension manifest v3.
* @see https://developer.chrome.com/docs/extensions/reference/manifest
*
* Clients may `request()` approval to connect. This method may reject if the
* provider chooses to deny approval.
*
* Clients must `connect()` to acquire a `MessagePort`. The resulting
* `MessagePort` represents an active, type-safe communication channel to the
* provider. It is convenient to provide the `connect` method as the `getPort`
* option for `createChannelTransport` from `@penumbra-zone/transport-dom`.
*
```js
import { jsonOptions } from '@penumbra-zone/protobuf';
import { createChannelTransport } from '@penumbra-zone/transport-dom';
// naively get first available provider
const provider = Object.values(window[Symbol.for('penumbra')])[0];
void provider.request();
// establish a transport
const transport = createChannelTransport({ jsonOptions, getPort: provider.connect });
// export function to create client
export const createPenumbraClient = serviceType => createPromiseClient(serviceType, transport);
```
*
*
*/
export interface PenumbraInjection {
/** Should be called by a page creating a channel transport. It returns a
* promise that may resolve with an active `MessagePort` providing a channel
* to this provider. */
readonly connect: () => Promise<MessagePort>;

/** Should be called by a page that does not yet have approval to connect.
* Returns a promise that may reject with an enumerated failure reason. */
readonly request: () => Promise<void>;

/** Indicate the provider should revoke approval of this origin. */
readonly disconnect: () => Promise<void>;

/** Should synchronously return the present connection state.
*
* - `true` indicates active connection.
* - `false` indicates connection is closed or rejected.
* - `undefined` indicates connection may be attempted.
*/
readonly isConnected: () => boolean | undefined;

/** Should contain a URI located at the provider's origin, which returns a
* chrome extension manifest v3 describing this provider. */
readonly manifest: string;
}

declare global {
interface Window {
/** Record upon this global should identify themselves by a field name matching their origin. */
readonly [PenumbraSymbol]?: undefined | Readonly<Record<string, PenumbraInjection>>;
}
}
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit b607f24

Please sign in to comment.