Skip to content

Commit

Permalink
fulfill injection state interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
turbocrime committed Jul 15, 2024
1 parent 7031110 commit 9bbbe1b
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 159 deletions.
2 changes: 1 addition & 1 deletion apps/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"@connectrpc/connect-web": "^1.4.0",
"@penumbra-labs/registry": "10.0.0",
"@penumbra-zone/bech32m": "^6.1.1",
"@penumbra-zone/client": "^11.0.1",
"@penumbra-zone/client": "^11.1.0",
"@penumbra-zone/crypto-web": "^13.0.0",
"@penumbra-zone/getters": "^11.0.0",
"@penumbra-zone/keys": "^4.2.1",
Expand Down
212 changes: 141 additions & 71 deletions apps/extension/src/content-scripts/injected-penumbra-global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* to them.
*
* The global is identified by `Symbol.for('penumbra')` and consists of a record
* with string keys referring to `PenumbraInjection` objects that contain a
* with string keys referring to `PenumbraProvider` objects that contain a
* simple API. The identifiers on this record should be unique, and correspond
* to a browser extension id. Providers should provide a link to their extension
* manifest in their record entry.
Expand All @@ -17,7 +17,12 @@
* connections.
*/

import { PenumbraInjection, PenumbraSymbol } from '@penumbra-zone/client';
import {
PenumbraProvider,
PenumbraState,
PenumbraSymbol,
PenumbraStateEvent,
} from '@penumbra-zone/client';

import {
isPraxFailureMessageEvent,
Expand Down Expand Up @@ -46,21 +51,41 @@ class PraxInjection {
private requestState?: PromiseSettledResultStatus;
private disconnectState?: PromiseSettledResultStatus;

private injection: Readonly<PenumbraInjection> = Object.freeze({
disconnect: () => this.endConnection(),
connect: () => (this.state() !== false ? this._connect.promise : this.connectionFailure),
isConnected: () => this.state(),
request: () => this.postRequest(),
manifest: String(this.manifestUrl),
state: () => {
throw new Error('not yet implemented');
private connectCalled = false;
private requestCalled = false;
private disconnectCalled = false;

private stateEvents = new EventTarget();

private injection: Readonly<PenumbraProvider> = Object.freeze({
connect: () => {
this.connectCalled = true;
return this.reduceConnectionState() !== false
? this._connect.promise
: this.connectionFailure();
},
addEventListener: () => {
throw new Error('not yet implemented');

disconnect: () => {
this.disconnectCalled = true;
return this.endConnection();
},
removeEventListener: () => {
throw new Error('not yet implemented');

request: () => {
this.requestCalled = true;
return this.postRequest();
},

isConnected: () => this.reduceConnectionState(),

state: () => this.reduceInjectionState(),

manifest: String(this.manifestUrl),

addEventListener: ((...params) =>
this.stateEvents.addEventListener(...params)) as EventTarget['addEventListener'],

removeEventListener: ((...params) =>
this.stateEvents.removeEventListener(...params)) as EventTarget['removeEventListener'],
});

private constructor() {
Expand All @@ -69,35 +94,45 @@ class PraxInjection {
}

window.addEventListener('message', this.connectionListener);
void this._connect.promise.finally(() =>
window.removeEventListener('message', this.connectionListener),
);

const dispatchStateEvent = () =>
this.stateEvents.dispatchEvent(
new PenumbraStateEvent(PRAX_ORIGIN, this.reduceInjectionState()),

Check failure on line 103 in apps/extension/src/content-scripts/injected-penumbra-global.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `Event`

Check failure on line 103 in apps/extension/src/content-scripts/injected-penumbra-global.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe construction of an any type value
);

void this._connect.promise
.then(
() => (this.connectState ??= 'fulfilled'),
() => (this.connectState ??= 'rejected'),
)
.finally(() => window.removeEventListener('message', this.connectionListener));
.finally(dispatchStateEvent);

void this._disconnect.promise.then(
() => (this.disconnectState = 'fulfilled'),
() => (this.disconnectState = 'rejected'),
);
void this._disconnect.promise
.then(
() => (this.disconnectState ??= 'fulfilled'),
() => (this.disconnectState ??= 'rejected'),
)
.finally(dispatchStateEvent);

void this._request.promise.then(
() => (this.requestState = 'fulfilled'),
() => (this.requestState = 'rejected'),
);
void this._request.promise
.then(
() => (this.requestState ??= 'fulfilled'),
() => (this.requestState ??= 'rejected'),
)
.finally(dispatchStateEvent);
}

/**
* Calling this function will synchronously return a unified
* true/false/undefined answer to the page connection state of this provider.
*
* `true` indicates active connection.
* `false` indicates connection is closed or rejected.
* `undefined` indicates connection may be attempted.
/** Synchronously return the true/false/undefined page connection state of this
* provider, without respect to what methods have been called.
* - `true` indicates active connection.
* - `false` indicates connection is closed or rejected.
* - `undefined` indicates connection may be attempted.
*/
private state(): boolean | undefined {
if (this.disconnectState !== undefined) {
private reduceConnectionState(): boolean | undefined {
if (this.disconnectState) {
return false;
}
if (this.requestState === 'rejected') {
Expand All @@ -113,8 +148,42 @@ class PraxInjection {
}
}

// this listener will resolve the connection promise AND request promise when
// the isolated content script injected-connection-port sends a `MessagePort`
/** Returns a single overall injection state. */
private reduceInjectionState(): PenumbraState {
if (
this.disconnectState === 'rejected' ||
this.connectState === 'rejected' ||
this.requestState === 'rejected'
) {
return PenumbraState.Failed;
}
switch (this.disconnectCalled && this.disconnectState) {
case false:
break;
default:
return PenumbraState.Disconnected;
}
switch (this.connectCalled && this.connectState) {
case false:
break;
case 'fulfilled':
return PenumbraState.Connected;
case undefined:
return PenumbraState.ConnectPending;
}
switch (this.requestCalled && this.requestState) {
case false:
break;
case 'fulfilled':
return PenumbraState.Requested;
case undefined:
return PenumbraState.RequestPending;
}
return PenumbraState.Present;
}

/** this listener will resolve the connection promise AND request promise when
* the isolated content script injected-connection-port sends a `MessagePort` */
private connectionListener = (msg: MessageEvent<unknown>) => {
if (msg.origin === window.origin && isPraxPortMessageEvent(msg)) {
const praxPort = unwrapPraxMessageEvent(msg);
Expand All @@ -123,8 +192,9 @@ class PraxInjection {
}
};

// this listener only rejects the request promise. success of the request
// promise is indicated by the connection promise being resolved.
/** this listener only rejects the request promise. success of the request
* promise is indicated by the connection promise being resolved.
*/
private requestFailureListener = (msg: MessageEvent<unknown>) => {
if (msg.origin === window.origin && isPraxFailureMessageEvent(msg)) {
const cause = unwrapPraxMessageEvent(msg);
Expand All @@ -133,11 +203,12 @@ class PraxInjection {
}
};

// always reject with the most important reason at time of access
// 1. disconnect
// 2. connection failure
// 3. request
private get connectionFailure() {
/** rejects with the most relevant reason
* - disconnect
* - connection failure
* - request failure
*/
private connectionFailure(): Promise<never> {
// Promise.race checks in order of the list index. so if more than one
// promise in the list is already settled, it responds with the result of
// the earlier index
Expand All @@ -149,33 +220,29 @@ class PraxInjection {
// rejects with previous failure, or 'Disconnected' if request was successful
this._request.promise.then(() => Promise.reject(Error('Disconnected'))),
// this should be unreachable
Promise.reject(Error('Unknown failure')),
Promise.resolve(null as never),
]);
}

private postRequest() {
const state = this.state();
if (state === true) {
// connection is already active
this._request.resolve();
} else if (state === false) {
// connection is already failed
const failure = this.connectionFailure;
failure.catch((u: unknown) => this._request.reject(u));
// a previous request may have succeeded, so return the failure directly
return failure;
} else {
// no request made yet. attach listener and emit
window.addEventListener('message', this.requestFailureListener);
void this._request.promise.finally(() =>
window.removeEventListener('message', this.requestFailureListener),
);
window.postMessage(
{
[PRAX]: PraxConnection.Request,
} satisfies PraxMessage<PraxConnection.Request>,
window.origin,
);
switch (this.reduceConnectionState()) {
case true: // connection is already active
this._request.resolve();
break;
case false: // connection is already failed
void this.connectionFailure().catch((u: unknown) => this._request.reject(u));
// a previous request may have succeeded, so also return the failure directly
return this.connectionFailure();
case undefined: // no request made yet. attach listener and emit
window.addEventListener('message', this.requestFailureListener);
void this._request.promise.finally(() =>
window.removeEventListener('message', this.requestFailureListener),
);
window.postMessage(
{ [PRAX]: PraxConnection.Request } satisfies PraxMessage<PraxConnection.Request>,
window.origin,
);
break;
}

return this._request.promise;
Expand All @@ -198,13 +265,16 @@ class PraxInjection {
);

// resolve the promise by state
const state = this.state();
if (state === true) {
this._disconnect.resolve();
} else if (state === false) {
this._disconnect.reject(Error('Connection already inactive'));
} else {
this._disconnect.reject(Error('Connection not yet active'));
switch (this.reduceConnectionState()) {
case true: // connection was active, will now become now disconnected
this._disconnect.resolve();
break;
case false: // connection was already inactive. can't disconnect in this state
this._disconnect.reject(Error('Connection already inactive'));
break;
case undefined: // connection was never attempted. can't disconnect in this state
this._disconnect.reject(Error('Connection not yet active'));
break;
}

return this._disconnect.promise;
Expand Down
Loading

0 comments on commit 9bbbe1b

Please sign in to comment.