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 5, 2024
1 parent 92fb83c commit f7ce0da
Showing 1 changed file with 135 additions and 69 deletions.
204 changes: 135 additions & 69 deletions apps/extension/src/content-scripts/injected-penumbra-global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
* connections.
*/

import { PenumbraInjection, PenumbraSymbol } from '@penumbra-zone/client';
import {
PenumbraInjection,
PenumbraInjectionState,
PenumbraInjectionStateEvent,
PenumbraSymbol,
} from '@penumbra-zone/client';

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

private connectCalled = false;
private requestCalled = false;
private disconnectCalled = false;

private stateEvents = new EventTarget();

private injection: Readonly<PenumbraInjection> = Object.freeze({
disconnect: () => this.endConnection(),
connect: () => (this.state() !== false ? this._connect.promise : this.connectionFailure),
isConnected: () => this.state(),
request: () => this.postRequest(),
connect: () => {
this.connectCalled = true;
return this.reduceConnectionState() !== false
? this._connect.promise
: this.connectionFailure();
},
disconnect: () => {
this.disconnectCalled = true;
return this.endConnection();
},
request: () => {
this.requestCalled = true;
return this.postRequest();
},
isConnected: () => this.reduceConnectionState(),

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

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

View workflow job for this annotation

GitHub Actions / Lint

Unsafe return of an `any` typed value

manifest: String(this.manifestUrl),

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

private constructor() {
if (PraxInjection.singleton) {
return PraxInjection.singleton;
}
if (PraxInjection.singleton) return PraxInjection.singleton;

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

const dispatchStateEvent = () =>
this.stateEvents.dispatchEvent(new PenumbraInjectionStateEvent(this.reduceInjectionState()));

Check failure on line 96 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 96 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) {
return false;
}
if (this.requestState === 'rejected') {
return false;
}
private reduceConnectionState(): boolean | undefined {
if (this.disconnectState) return false;
if (this.requestState === 'rejected') return false;
switch (this.connectState) {
case 'rejected':
return false;
Expand All @@ -104,8 +136,41 @@ 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(): PenumbraInjectionState {
if (
this.disconnectState === 'rejected' ||
this.connectState === 'rejected' ||
this.requestState === 'rejected'
)
return PenumbraInjectionState.Failed;

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

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access .Failed on an `error` typed value
switch (this.disconnectCalled && this.disconnectState) {
case false:
break;
default:
return PenumbraInjectionState.Disconnected;

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

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access .Disconnected on an `error` typed value
}
switch (this.connectCalled && this.connectState) {
case false:
break;
case 'fulfilled':
return PenumbraInjectionState.Connected;

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

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access .Connected on an `error` typed value
case undefined:
return PenumbraInjectionState.ConnectPending;

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

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access .ConnectPending on an `error` typed value
}
switch (this.requestCalled && this.requestState) {
case false:
break;
case 'fulfilled':
return PenumbraInjectionState.Requested;

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

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access .Requested on an `error` typed value
case undefined:
return PenumbraInjectionState.RequestPending;

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

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access .RequestPending on an `error` typed value
}
return PenumbraInjectionState.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 @@ -114,8 +179,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 @@ -124,11 +190,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 @@ -145,28 +212,24 @@ class PraxInjection {
}

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 @@ -189,13 +252,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

0 comments on commit f7ce0da

Please sign in to comment.