@tokamakdev/plugin 0.1.0-beta.33
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/package.json +19 -0
- package/src/index.ts +167 -0
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tokamakdev/plugin",
|
|
3
|
+
"version": "0.1.0-beta.33",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/mantty/tokamak.git",
|
|
7
|
+
"directory": "plugins/core"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
interface NativeTransport {
|
|
2
|
+
postMessage(message: string): void;
|
|
3
|
+
onmessage: ((event: { data: string }) => void) | null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface NativeError {
|
|
7
|
+
name: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface NativeResponse {
|
|
12
|
+
session: string;
|
|
13
|
+
id: number;
|
|
14
|
+
value?: unknown;
|
|
15
|
+
error?: NativeError;
|
|
16
|
+
done: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface PendingCall {
|
|
20
|
+
kind: "call";
|
|
21
|
+
resolve(value: unknown): void;
|
|
22
|
+
reject(error: DOMException): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PendingSubscription {
|
|
26
|
+
kind: "subscription";
|
|
27
|
+
next(value: unknown): void;
|
|
28
|
+
error(error: DOMException): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type Pending = PendingCall | PendingSubscription;
|
|
32
|
+
|
|
33
|
+
declare global {
|
|
34
|
+
var __tokamakNative: NativeTransport | undefined;
|
|
35
|
+
var __tokamakReceive: ((response: NativeResponse) => void) | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let nextRequestId = 1;
|
|
39
|
+
const session = `${String(Date.now())}-${String(Math.random())}`;
|
|
40
|
+
let connected: NativeTransport | undefined;
|
|
41
|
+
const pending = new Map<number, Pending>();
|
|
42
|
+
|
|
43
|
+
export abstract class FrontendPlugin {
|
|
44
|
+
protected constructor(private readonly pluginId: string) {}
|
|
45
|
+
|
|
46
|
+
protected get hasNativeTransport(): boolean {
|
|
47
|
+
return nativeTransport() !== undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
protected call<T>(method: string, arguments_: unknown = null): Promise<T> {
|
|
51
|
+
const transport = requireNativeTransport();
|
|
52
|
+
const id = nextRequestId++;
|
|
53
|
+
return new Promise<T>((resolve, reject) => {
|
|
54
|
+
pending.set(id, {
|
|
55
|
+
kind: "call",
|
|
56
|
+
resolve: (value) => {
|
|
57
|
+
resolve(value as T);
|
|
58
|
+
},
|
|
59
|
+
reject,
|
|
60
|
+
});
|
|
61
|
+
post(
|
|
62
|
+
transport,
|
|
63
|
+
{
|
|
64
|
+
type: "call",
|
|
65
|
+
session,
|
|
66
|
+
id,
|
|
67
|
+
plugin: this.pluginId,
|
|
68
|
+
method,
|
|
69
|
+
arguments: arguments_,
|
|
70
|
+
},
|
|
71
|
+
id,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
protected subscribe(
|
|
77
|
+
method: string,
|
|
78
|
+
next: (value: unknown) => void,
|
|
79
|
+
error: (error: DOMException) => void,
|
|
80
|
+
arguments_: unknown = null,
|
|
81
|
+
): () => void {
|
|
82
|
+
const transport = requireNativeTransport();
|
|
83
|
+
const id = nextRequestId++;
|
|
84
|
+
pending.set(id, {
|
|
85
|
+
kind: "subscription",
|
|
86
|
+
next: (value) => {
|
|
87
|
+
next(value);
|
|
88
|
+
},
|
|
89
|
+
error,
|
|
90
|
+
});
|
|
91
|
+
post(
|
|
92
|
+
transport,
|
|
93
|
+
{
|
|
94
|
+
type: "subscribe",
|
|
95
|
+
session,
|
|
96
|
+
id,
|
|
97
|
+
plugin: this.pluginId,
|
|
98
|
+
method,
|
|
99
|
+
arguments: arguments_,
|
|
100
|
+
},
|
|
101
|
+
id,
|
|
102
|
+
);
|
|
103
|
+
return () => {
|
|
104
|
+
if (!pending.delete(id)) return;
|
|
105
|
+
transport.postMessage(JSON.stringify({ type: "cancel", session, id }));
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function nativeTransport(): NativeTransport | undefined {
|
|
111
|
+
const transport = globalThis.__tokamakNative;
|
|
112
|
+
if (!transport || typeof transport.postMessage !== "function") return undefined;
|
|
113
|
+
if (connected !== transport) {
|
|
114
|
+
connected = transport;
|
|
115
|
+
transport.onmessage = ({ data }) => {
|
|
116
|
+
receive(JSON.parse(data) as NativeResponse);
|
|
117
|
+
};
|
|
118
|
+
globalThis.__tokamakReceive = receive;
|
|
119
|
+
transport.postMessage(JSON.stringify({ type: "reset", session }));
|
|
120
|
+
}
|
|
121
|
+
return transport;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function requireNativeTransport(): NativeTransport {
|
|
125
|
+
const transport = nativeTransport();
|
|
126
|
+
if (transport) return transport;
|
|
127
|
+
throw new DOMException("Native plugin transport is unavailable", "NotSupportedError");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function post(transport: NativeTransport, message: object, id: number): void {
|
|
131
|
+
try {
|
|
132
|
+
transport.postMessage(JSON.stringify(message));
|
|
133
|
+
} catch (error) {
|
|
134
|
+
pending.delete(id);
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function receive(response: NativeResponse): void {
|
|
140
|
+
if (response.session !== session) return;
|
|
141
|
+
const request = pending.get(response.id);
|
|
142
|
+
if (!request) return;
|
|
143
|
+
if (response.done) pending.delete(response.id);
|
|
144
|
+
|
|
145
|
+
if (response.error) {
|
|
146
|
+
const error = new DOMException(response.error.message, response.error.name);
|
|
147
|
+
if (request.kind === "call") request.reject(error);
|
|
148
|
+
else request.error(error);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (request.kind === "call") request.resolve(response.value);
|
|
153
|
+
else request.next(response.value);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const eventTarget: {
|
|
157
|
+
addEventListener?: (type: string, listener: () => void) => void;
|
|
158
|
+
} = globalThis;
|
|
159
|
+
|
|
160
|
+
eventTarget.addEventListener?.("pagehide", () => {
|
|
161
|
+
if (!connected) return;
|
|
162
|
+
for (const [id, request] of pending) {
|
|
163
|
+
if (request.kind !== "subscription") continue;
|
|
164
|
+
pending.delete(id);
|
|
165
|
+
connected.postMessage(JSON.stringify({ type: "cancel", session, id }));
|
|
166
|
+
}
|
|
167
|
+
});
|