@adobe/uix-core 0.6.5 → 0.7.1-nightly.20230114
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/dist/__helpers__/jest.messagechannel.d.cts +2 -0
- package/dist/__helpers__/jest.messagechannel.d.cts.map +1 -0
- package/dist/__mocks__/mock-finalization-registry.d.ts +11 -0
- package/dist/__mocks__/mock-finalization-registry.d.ts.map +1 -0
- package/dist/__mocks__/mock-weak-ref.d.ts +7 -0
- package/dist/__mocks__/mock-weak-ref.d.ts.map +1 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/cross-realm-object.d.ts +44 -0
- package/dist/cross-realm-object.d.ts.map +1 -0
- package/dist/debuglog.d.ts +11 -0
- package/dist/debuglog.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +906 -7
- package/dist/index.js.map +1 -1
- package/dist/message-wrapper.d.ts +9 -0
- package/dist/message-wrapper.d.ts.map +1 -0
- package/dist/object-simulator.d.ts +28 -0
- package/dist/object-simulator.d.ts.map +1 -0
- package/dist/object-simulator.test.d.ts +2 -0
- package/dist/object-simulator.test.d.ts.map +1 -0
- package/dist/object-walker.d.ts +30 -0
- package/dist/object-walker.d.ts.map +1 -0
- package/dist/promises/index.d.ts +3 -0
- package/dist/promises/index.d.ts.map +1 -0
- package/dist/promises/promise-wrappers.test.d.ts +2 -0
- package/dist/promises/promise-wrappers.test.d.ts.map +1 -0
- package/dist/promises/timed.d.ts +15 -0
- package/dist/promises/timed.d.ts.map +1 -0
- package/dist/promises/wait.d.ts +7 -0
- package/dist/promises/wait.d.ts.map +1 -0
- package/dist/remote-subject.d.ts +70 -0
- package/dist/remote-subject.d.ts.map +1 -0
- package/dist/rpc/call-receiver.d.ts +4 -0
- package/dist/rpc/call-receiver.d.ts.map +1 -0
- package/dist/rpc/call-receiver.test.d.ts +2 -0
- package/dist/rpc/call-receiver.test.d.ts.map +1 -0
- package/dist/rpc/call-sender.d.ts +4 -0
- package/dist/rpc/call-sender.d.ts.map +1 -0
- package/dist/rpc/call-sender.test.d.ts +2 -0
- package/dist/rpc/call-sender.test.d.ts.map +1 -0
- package/dist/rpc/index.d.ts +3 -0
- package/dist/rpc/index.d.ts.map +1 -0
- package/dist/tickets.d.ts +34 -0
- package/dist/tickets.d.ts.map +1 -0
- package/dist/tunnel/index.d.ts +2 -0
- package/dist/tunnel/index.d.ts.map +1 -0
- package/dist/tunnel/tunnel-messenger.d.ts +25 -0
- package/dist/tunnel/tunnel-messenger.d.ts.map +1 -0
- package/dist/tunnel/tunnel-messenger.test.d.ts +2 -0
- package/dist/tunnel/tunnel-messenger.test.d.ts.map +1 -0
- package/dist/tunnel/tunnel.d.ts +62 -0
- package/dist/tunnel/tunnel.d.ts.map +1 -0
- package/dist/tunnel/tunnel.test.d.ts +2 -0
- package/dist/tunnel/tunnel.test.d.ts.map +1 -0
- package/dist/types.d.ts +1 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/value-assertions.d.ts +13 -0
- package/dist/value-assertions.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/__helpers__/jest.messagechannel.cjs +3 -0
- package/src/__mocks__/mock-finalization-registry.ts +13 -0
- package/src/__mocks__/mock-weak-ref.ts +10 -0
- package/src/constants.ts +10 -0
- package/src/cross-realm-object.ts +117 -0
- package/src/debuglog.ts +1 -1
- package/src/index.ts +4 -1
- package/src/message-wrapper.ts +35 -0
- package/src/object-simulator.test.ts +328 -0
- package/src/object-simulator.ts +145 -0
- package/src/object-walker.ts +132 -0
- package/src/promises/index.ts +2 -0
- package/src/promises/promise-wrappers.test.ts +63 -0
- package/src/promises/timed.ts +41 -0
- package/src/promises/wait.ts +10 -0
- package/src/remote-subject.ts +185 -0
- package/src/rpc/call-receiver.test.ts +90 -0
- package/src/rpc/call-receiver.ts +29 -0
- package/src/rpc/call-sender.test.ts +73 -0
- package/src/rpc/call-sender.ts +72 -0
- package/src/rpc/index.ts +2 -0
- package/src/tickets.ts +71 -0
- package/src/tunnel/index.ts +1 -0
- package/src/tunnel/tunnel-messenger.test.ts +183 -0
- package/src/tunnel/tunnel-messenger.ts +99 -0
- package/src/tunnel/tunnel.test.ts +211 -0
- package/src/tunnel/tunnel.ts +322 -0
- package/src/types.ts +3 -5
- package/src/value-assertions.ts +58 -0
- package/tsconfig.json +2 -6
- package/dist/timeout-promise.d.ts +0 -12
- package/dist/timeout-promise.d.ts.map +0 -1
- package/src/timeout-promise.ts +0 -36
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CallArgsTicket,
|
|
3
|
+
CallTicket,
|
|
4
|
+
DefTicket,
|
|
5
|
+
RejectTicket,
|
|
6
|
+
ResolveTicket,
|
|
7
|
+
RespondTicket,
|
|
8
|
+
CleanupTicket,
|
|
9
|
+
} from "./tickets";
|
|
10
|
+
import type { Materialized, Simulated } from "./object-walker";
|
|
11
|
+
import EventEmitter from "eventemitter3";
|
|
12
|
+
|
|
13
|
+
type EvTypeDef = `${string}_f`;
|
|
14
|
+
type EvTypeGC = `${string}_g`;
|
|
15
|
+
type EvTypeCall = `${string}_c`;
|
|
16
|
+
type EvTypeRespond = `${string}_r`;
|
|
17
|
+
type EvTypeDestroyed = "destroyed";
|
|
18
|
+
type EvTypeConnected = "connected";
|
|
19
|
+
type EvTypeError = "error";
|
|
20
|
+
|
|
21
|
+
type RemoteDefEvent = {
|
|
22
|
+
type: EvTypeDef;
|
|
23
|
+
payload: DefTicket;
|
|
24
|
+
};
|
|
25
|
+
type RemoteCallEvent = {
|
|
26
|
+
type: EvTypeCall;
|
|
27
|
+
payload: CallArgsTicket;
|
|
28
|
+
};
|
|
29
|
+
type RemoteResolveEvent = {
|
|
30
|
+
type: EvTypeRespond;
|
|
31
|
+
payload: ResolveTicket;
|
|
32
|
+
};
|
|
33
|
+
type RemoteRejectEvent = {
|
|
34
|
+
type: EvTypeRespond;
|
|
35
|
+
payload: RejectTicket;
|
|
36
|
+
};
|
|
37
|
+
type RemoteCleanupEvent = {
|
|
38
|
+
type: EvTypeGC;
|
|
39
|
+
payload: CleanupTicket;
|
|
40
|
+
};
|
|
41
|
+
type RemoteReconnectedEvent = {
|
|
42
|
+
type: EvTypeConnected;
|
|
43
|
+
payload: void;
|
|
44
|
+
};
|
|
45
|
+
type RemoteDestroyedEvent = {
|
|
46
|
+
type: EvTypeDestroyed;
|
|
47
|
+
payload: void;
|
|
48
|
+
};
|
|
49
|
+
type RemoteErrorEvent = {
|
|
50
|
+
type: EvTypeError;
|
|
51
|
+
payload: Error;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type RemoteEvents =
|
|
55
|
+
| RemoteDefEvent
|
|
56
|
+
| RemoteCallEvent
|
|
57
|
+
| RemoteResolveEvent
|
|
58
|
+
| RemoteRejectEvent
|
|
59
|
+
| RemoteCleanupEvent
|
|
60
|
+
| RemoteReconnectedEvent
|
|
61
|
+
| RemoteDestroyedEvent
|
|
62
|
+
| RemoteErrorEvent;
|
|
63
|
+
|
|
64
|
+
type Simulates = <T>(localObject: T) => Simulated<T>;
|
|
65
|
+
type Materializes = <T>(simulatedObject: T) => Materialized<T>;
|
|
66
|
+
|
|
67
|
+
export interface Simulator {
|
|
68
|
+
// #region Properties
|
|
69
|
+
|
|
70
|
+
materialize: Materializes;
|
|
71
|
+
simulate: Simulates;
|
|
72
|
+
|
|
73
|
+
// #endregion Properties
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type Mapper = Simulates | Materializes;
|
|
77
|
+
|
|
78
|
+
export class RemoteSubject {
|
|
79
|
+
// #region Properties
|
|
80
|
+
|
|
81
|
+
private emitter: EventEmitter;
|
|
82
|
+
private simulator: Simulator;
|
|
83
|
+
|
|
84
|
+
// #endregion Properties
|
|
85
|
+
|
|
86
|
+
// #region Constructors
|
|
87
|
+
|
|
88
|
+
constructor(emitter: EventEmitter, simulator: Simulator) {
|
|
89
|
+
this.emitter = emitter;
|
|
90
|
+
this.simulator = simulator;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// #endregion Constructors
|
|
94
|
+
|
|
95
|
+
// #region Public Methods
|
|
96
|
+
|
|
97
|
+
notifyCleanup(ticket: DefTicket) {
|
|
98
|
+
return this.emitter.emit(`${ticket.fnId}_g`, {});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
notifyConnect() {
|
|
102
|
+
return this.emitter.emit("connected");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
notifyDestroy() {
|
|
106
|
+
return this.emitter.emit("destroyed");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
onCall(ticket: DefTicket, handler: (ticket: CallArgsTicket) => void) {
|
|
110
|
+
return this.subscribe(`${ticket.fnId}_c`, (ticket: CallArgsTicket) =>
|
|
111
|
+
handler(this.processCallTicket(ticket, this.simulator.materialize))
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
onConnected(handler: () => void) {
|
|
116
|
+
return this.subscribe("connected", handler);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
onDestroyed(handler: () => void) {
|
|
120
|
+
return this.subscribe("destroyed", handler);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
onOutOfScope(ticket: DefTicket, handler: () => void) {
|
|
124
|
+
return this.subscribeOnce(`${ticket.fnId}_g`, handler);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
onRespond(ticket: CallTicket, handler: (ticket: RespondTicket) => void) {
|
|
128
|
+
const fnAndCall = `${ticket.fnId}${ticket.callId}`;
|
|
129
|
+
return this.subscribeOnce(`${fnAndCall}_r`, (ticket: RespondTicket) =>
|
|
130
|
+
handler(this.processResponseTicket(ticket, this.simulator.materialize))
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
respond(ticket: RespondTicket) {
|
|
135
|
+
const fnAndCall = `${ticket.fnId}${ticket.callId}`;
|
|
136
|
+
return this.emitter.emit(
|
|
137
|
+
`${fnAndCall}_r`,
|
|
138
|
+
this.processResponseTicket(ticket, this.simulator.simulate)
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
send(ticket: CallArgsTicket) {
|
|
143
|
+
return this.emitter.emit(
|
|
144
|
+
`${ticket.fnId}_c`,
|
|
145
|
+
this.processCallTicket(ticket, this.simulator.simulate)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// #endregion Public Methods
|
|
150
|
+
|
|
151
|
+
// #region Private Methods
|
|
152
|
+
|
|
153
|
+
private processCallTicket(
|
|
154
|
+
{ args, ...ticket }: CallArgsTicket,
|
|
155
|
+
mapper: Mapper
|
|
156
|
+
) {
|
|
157
|
+
return {
|
|
158
|
+
...ticket,
|
|
159
|
+
args: args.map(mapper),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private processResponseTicket(ticket: RespondTicket, mapper: Mapper) {
|
|
164
|
+
return ticket.status === "resolve"
|
|
165
|
+
? { ...ticket, value: mapper(ticket.value) }
|
|
166
|
+
: ticket;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private subscribe(type: string, handler: (arg: unknown) => void) {
|
|
170
|
+
this.emitter.on(type, handler);
|
|
171
|
+
return () => {
|
|
172
|
+
this.emitter.off(type, handler);
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private subscribeOnce(type: string, handler: (arg: unknown) => void) {
|
|
177
|
+
const once = (arg: unknown) => {
|
|
178
|
+
this.emitter.off(type, once);
|
|
179
|
+
handler(arg);
|
|
180
|
+
};
|
|
181
|
+
return this.subscribe(type, once);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// #endregion Private Methods
|
|
185
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { wait } from "../promises/wait";
|
|
2
|
+
import EventEmitter from "eventemitter3";
|
|
3
|
+
import { RemoteSubject } from "../remote-subject";
|
|
4
|
+
import { receiveCalls } from "./call-receiver";
|
|
5
|
+
import { FakeFinalizationRegistry } from "../__mocks__/mock-finalization-registry";
|
|
6
|
+
import { FakeWeakRef } from "../__mocks__/mock-weak-ref";
|
|
7
|
+
import { ObjectSimulator } from "../object-simulator";
|
|
8
|
+
|
|
9
|
+
describe("a listener for remote calls to a local function", () => {
|
|
10
|
+
const MURMURS = ["baa", "moo", "ahoy"];
|
|
11
|
+
const FARAWAY_SOUND = "(distant) riiiiicolaaaa";
|
|
12
|
+
const ECHOES = "(echoes) riiiiicolaaaa";
|
|
13
|
+
const village = jest.fn().mockReturnValue(MURMURS);
|
|
14
|
+
const villageId = "village_1";
|
|
15
|
+
const villageTicket = { fnId: villageId };
|
|
16
|
+
let emitter: EventEmitter;
|
|
17
|
+
let subject: RemoteSubject;
|
|
18
|
+
let simulator: ObjectSimulator;
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
village.mockClear();
|
|
21
|
+
jest.spyOn(console, "error").mockImplementation(() => {});
|
|
22
|
+
emitter = new EventEmitter();
|
|
23
|
+
simulator = ObjectSimulator.create(emitter, FakeFinalizationRegistry);
|
|
24
|
+
subject = simulator.subject;
|
|
25
|
+
receiveCalls(village, villageTicket, new FakeWeakRef(subject));
|
|
26
|
+
});
|
|
27
|
+
it("turns fn_call events into calls to local function", async () => {
|
|
28
|
+
const responder = jest.fn();
|
|
29
|
+
const call4Ticket = {
|
|
30
|
+
...villageTicket,
|
|
31
|
+
callId: 4,
|
|
32
|
+
};
|
|
33
|
+
subject.onRespond(call4Ticket, responder);
|
|
34
|
+
subject.send({
|
|
35
|
+
...call4Ticket,
|
|
36
|
+
args: [FARAWAY_SOUND, ECHOES],
|
|
37
|
+
});
|
|
38
|
+
await expect(village).toHaveBeenCalledWith(FARAWAY_SOUND, ECHOES);
|
|
39
|
+
expect(responder).toHaveBeenCalledWith(
|
|
40
|
+
expect.objectContaining({
|
|
41
|
+
...call4Ticket,
|
|
42
|
+
status: "resolve",
|
|
43
|
+
value: expect.arrayContaining(["baa", "moo", "ahoy"]),
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
it("sends events notifying of rejections", async () => {
|
|
48
|
+
const responder = jest.fn();
|
|
49
|
+
const call500Ticket = {
|
|
50
|
+
...villageTicket,
|
|
51
|
+
callId: 500,
|
|
52
|
+
};
|
|
53
|
+
subject.onRespond(call500Ticket, responder);
|
|
54
|
+
village.mockRejectedValueOnce(new Error("what is that infernal noise"));
|
|
55
|
+
subject.send({
|
|
56
|
+
...call500Ticket,
|
|
57
|
+
args: [FARAWAY_SOUND],
|
|
58
|
+
});
|
|
59
|
+
await expect(village).toHaveBeenCalledWith(FARAWAY_SOUND);
|
|
60
|
+
await wait(100);
|
|
61
|
+
expect(responder).toHaveBeenCalledWith(
|
|
62
|
+
expect.objectContaining({
|
|
63
|
+
...call500Ticket,
|
|
64
|
+
status: "reject",
|
|
65
|
+
error: expect.any(Error),
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
it.skip("unsubscribes itself when receiving a cleanup event", async () => {
|
|
70
|
+
const responder = jest.fn();
|
|
71
|
+
const call76Ticket = {
|
|
72
|
+
...villageTicket,
|
|
73
|
+
callId: 76,
|
|
74
|
+
};
|
|
75
|
+
subject.onRespond(call76Ticket, responder);
|
|
76
|
+
village.mockRejectedValueOnce(new Error("what is that infernal noise"));
|
|
77
|
+
|
|
78
|
+
subject.notifyCleanup(call76Ticket);
|
|
79
|
+
|
|
80
|
+
await wait(100);
|
|
81
|
+
|
|
82
|
+
subject.send({
|
|
83
|
+
...call76Ticket,
|
|
84
|
+
args: [FARAWAY_SOUND],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await wait(100);
|
|
88
|
+
expect(responder).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CallArgsTicket, DefTicket } from "../tickets";
|
|
2
|
+
import type { RemoteSubject } from "../remote-subject";
|
|
3
|
+
|
|
4
|
+
export function receiveCalls(
|
|
5
|
+
fn: CallableFunction,
|
|
6
|
+
ticket: DefTicket,
|
|
7
|
+
remote: WeakRef<RemoteSubject>
|
|
8
|
+
) {
|
|
9
|
+
const responder = async ({ fnId, callId, args }: CallArgsTicket) => {
|
|
10
|
+
/* istanbul ignore next: should never happen */
|
|
11
|
+
try {
|
|
12
|
+
const value = await fn(...args);
|
|
13
|
+
remote.deref().respond({
|
|
14
|
+
fnId,
|
|
15
|
+
callId,
|
|
16
|
+
value,
|
|
17
|
+
status: "resolve",
|
|
18
|
+
});
|
|
19
|
+
} catch (error) {
|
|
20
|
+
remote.deref().respond({
|
|
21
|
+
fnId,
|
|
22
|
+
callId,
|
|
23
|
+
status: "reject",
|
|
24
|
+
error,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
return remote.deref().onCall(ticket, responder);
|
|
29
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { RemoteSubject } from "../remote-subject";
|
|
2
|
+
import { makeCallSender } from "./call-sender";
|
|
3
|
+
import { ObjectSimulator } from "../object-simulator";
|
|
4
|
+
import { FakeFinalizationRegistry } from "../__mocks__/mock-finalization-registry";
|
|
5
|
+
import { FakeWeakRef } from "../__mocks__/mock-weak-ref";
|
|
6
|
+
import { wait } from "../promises/wait";
|
|
7
|
+
import EventEmitter from "eventemitter3";
|
|
8
|
+
|
|
9
|
+
describe("an proxy representing a function in the other realm", () => {
|
|
10
|
+
const SOUND = "RIIIICOLAAAA";
|
|
11
|
+
const FARAWAY_SOUND = "(distant) riiiiicolaaaa";
|
|
12
|
+
const alpenhorn = jest.fn().mockReturnValue(SOUND);
|
|
13
|
+
const alpenhornId = "alpenhorn_1";
|
|
14
|
+
let simulator: ObjectSimulator;
|
|
15
|
+
let emitter;
|
|
16
|
+
let subject: RemoteSubject;
|
|
17
|
+
let remoteAlpenhorn: ((...args: any[]) => Promise<unknown>) | (() => any);
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
alpenhorn.mockClear();
|
|
20
|
+
emitter = new EventEmitter();
|
|
21
|
+
simulator = ObjectSimulator.create(emitter, FakeFinalizationRegistry);
|
|
22
|
+
subject = simulator.subject;
|
|
23
|
+
remoteAlpenhorn = makeCallSender(
|
|
24
|
+
{ fnId: alpenhornId },
|
|
25
|
+
new FakeWeakRef(subject)
|
|
26
|
+
);
|
|
27
|
+
jest.spyOn(console, "error").mockImplementation(() => {});
|
|
28
|
+
});
|
|
29
|
+
it("resolves through the emitter", async () => {
|
|
30
|
+
subject.onCall(
|
|
31
|
+
{
|
|
32
|
+
fnId: alpenhornId,
|
|
33
|
+
},
|
|
34
|
+
(callTicket) => {
|
|
35
|
+
const { callId, fnId } = callTicket;
|
|
36
|
+
subject.respond({
|
|
37
|
+
callId,
|
|
38
|
+
fnId,
|
|
39
|
+
status: "resolve",
|
|
40
|
+
value: FARAWAY_SOUND,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
await expect(remoteAlpenhorn()).resolves.toBe(FARAWAY_SOUND);
|
|
45
|
+
});
|
|
46
|
+
it("rejects through the emitter", async () => {
|
|
47
|
+
subject.onCall({ fnId: alpenhornId }, (callTicket) => {
|
|
48
|
+
const { callId, fnId } = callTicket;
|
|
49
|
+
subject.respond({
|
|
50
|
+
callId,
|
|
51
|
+
fnId,
|
|
52
|
+
status: "reject",
|
|
53
|
+
error: new Error("bonk"),
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
await expect(remoteAlpenhorn()).rejects.toThrowError("bonk");
|
|
57
|
+
});
|
|
58
|
+
it("destroys itself on disconnect", async () => {
|
|
59
|
+
subject.onCall({ fnId: alpenhornId }, async (callTicket) => {
|
|
60
|
+
const { callId, fnId } = callTicket;
|
|
61
|
+
subject.notifyDestroy();
|
|
62
|
+
await wait(100);
|
|
63
|
+
subject.respond({
|
|
64
|
+
callId,
|
|
65
|
+
fnId,
|
|
66
|
+
status: "reject",
|
|
67
|
+
error: new Error("bonk"),
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
await expect(remoteAlpenhorn()).rejects.toThrowError("destroyed");
|
|
71
|
+
await expect(remoteAlpenhorn()).rejects.toThrowError("destroyed");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { CallArgsTicket, DefTicket } from "../tickets";
|
|
2
|
+
import type { RemoteSubject } from "../remote-subject";
|
|
3
|
+
|
|
4
|
+
type RejectionPool = Set<(e: Error) => unknown>;
|
|
5
|
+
|
|
6
|
+
class DisconnectionError extends Error {
|
|
7
|
+
constructor() {
|
|
8
|
+
super(
|
|
9
|
+
"Function belongs to a simulated remote object which has been disconnected! The tunnel may have been destroyed by page navigation or reload."
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function dispatch(
|
|
15
|
+
subject: RemoteSubject,
|
|
16
|
+
callTicket: CallArgsTicket,
|
|
17
|
+
rejectionPool: RejectionPool,
|
|
18
|
+
resolve: { (value: unknown): void; (arg0: any): void },
|
|
19
|
+
reject: { (reason?: string): void; (arg0: any): void }
|
|
20
|
+
) {
|
|
21
|
+
subject.onRespond(callTicket, (responseTicket) => {
|
|
22
|
+
rejectionPool.delete(reject);
|
|
23
|
+
if (responseTicket.status === "resolve") {
|
|
24
|
+
resolve(responseTicket.value);
|
|
25
|
+
} else {
|
|
26
|
+
reject(responseTicket.error);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
subject.send(callTicket);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function makeCallSender(
|
|
33
|
+
{ fnId }: DefTicket,
|
|
34
|
+
subjectRef: WeakRef<RemoteSubject>
|
|
35
|
+
) {
|
|
36
|
+
let callCounter = 0;
|
|
37
|
+
const rejectionPool: RejectionPool = new Set();
|
|
38
|
+
let sender = function (...args: unknown[]) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
rejectionPool.add(reject);
|
|
41
|
+
const callId = ++callCounter;
|
|
42
|
+
const callTicket: CallArgsTicket = {
|
|
43
|
+
fnId,
|
|
44
|
+
callId,
|
|
45
|
+
args,
|
|
46
|
+
};
|
|
47
|
+
return dispatch(
|
|
48
|
+
subjectRef.deref(),
|
|
49
|
+
callTicket,
|
|
50
|
+
rejectionPool,
|
|
51
|
+
resolve,
|
|
52
|
+
reject
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
const destroy = () => {
|
|
57
|
+
subjectRef = null;
|
|
58
|
+
sender = () => {
|
|
59
|
+
throw new DisconnectionError();
|
|
60
|
+
};
|
|
61
|
+
for (const reject of rejectionPool) {
|
|
62
|
+
reject(new DisconnectionError());
|
|
63
|
+
}
|
|
64
|
+
rejectionPool.clear();
|
|
65
|
+
};
|
|
66
|
+
subjectRef.deref().onDestroyed(destroy);
|
|
67
|
+
const facade = async function (...args: unknown[]) {
|
|
68
|
+
return sender(...args);
|
|
69
|
+
};
|
|
70
|
+
Object.defineProperty(facade, "name", { value: fnId });
|
|
71
|
+
return facade;
|
|
72
|
+
}
|
package/src/rpc/index.ts
ADDED
package/src/tickets.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { INIT_CALLBACK } from "./constants";
|
|
2
|
+
|
|
3
|
+
export interface HandshakeAcceptedTicket {
|
|
4
|
+
// #region Properties
|
|
5
|
+
|
|
6
|
+
accepts: string;
|
|
7
|
+
version: string;
|
|
8
|
+
|
|
9
|
+
// #endregion Properties
|
|
10
|
+
}
|
|
11
|
+
export interface HandshakeOfferedTicket {
|
|
12
|
+
// #region Properties
|
|
13
|
+
|
|
14
|
+
offers: string;
|
|
15
|
+
version: string;
|
|
16
|
+
|
|
17
|
+
// #endregion Properties
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** @internal */
|
|
21
|
+
export interface DefTicket {
|
|
22
|
+
// #region Properties
|
|
23
|
+
|
|
24
|
+
fnId: string;
|
|
25
|
+
|
|
26
|
+
// #endregion Properties
|
|
27
|
+
}
|
|
28
|
+
export interface InitTicket extends DefTicket {
|
|
29
|
+
// #region Properties
|
|
30
|
+
|
|
31
|
+
fnId: typeof INIT_CALLBACK;
|
|
32
|
+
|
|
33
|
+
// #endregion Properties
|
|
34
|
+
}
|
|
35
|
+
export interface CallTicket extends DefTicket {
|
|
36
|
+
// #region Properties
|
|
37
|
+
|
|
38
|
+
callId: number;
|
|
39
|
+
|
|
40
|
+
// #endregion Properties
|
|
41
|
+
}
|
|
42
|
+
export interface CallArgsTicket extends CallTicket {
|
|
43
|
+
// #region Properties
|
|
44
|
+
|
|
45
|
+
args: any[];
|
|
46
|
+
|
|
47
|
+
// #endregion Properties
|
|
48
|
+
}
|
|
49
|
+
export interface ResolveTicket extends CallTicket {
|
|
50
|
+
// #region Properties
|
|
51
|
+
|
|
52
|
+
status: "resolve";
|
|
53
|
+
value: any;
|
|
54
|
+
|
|
55
|
+
// #endregion Properties
|
|
56
|
+
}
|
|
57
|
+
export interface RejectTicket extends CallTicket {
|
|
58
|
+
// #region Properties
|
|
59
|
+
|
|
60
|
+
error: Error;
|
|
61
|
+
status: "reject";
|
|
62
|
+
|
|
63
|
+
// #endregion Properties
|
|
64
|
+
}
|
|
65
|
+
export type RespondTicket = ResolveTicket | RejectTicket;
|
|
66
|
+
|
|
67
|
+
export type CleanupTicket = {};
|
|
68
|
+
|
|
69
|
+
export const INIT_TICKET: InitTicket = {
|
|
70
|
+
fnId: INIT_CALLBACK,
|
|
71
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./tunnel";
|