@adobe/uix-core 0.6.5-1 → 0.7.0

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.
Files changed (91) hide show
  1. package/dist/__helpers__/jest.messagechannel.d.cts +2 -0
  2. package/dist/__helpers__/jest.messagechannel.d.cts.map +1 -0
  3. package/dist/__mocks__/mock-finalization-registry.d.ts +11 -0
  4. package/dist/__mocks__/mock-finalization-registry.d.ts.map +1 -0
  5. package/dist/__mocks__/mock-weak-ref.d.ts +7 -0
  6. package/dist/__mocks__/mock-weak-ref.d.ts.map +1 -0
  7. package/dist/constants.d.ts +7 -0
  8. package/dist/constants.d.ts.map +1 -0
  9. package/dist/cross-realm-object.d.ts +44 -0
  10. package/dist/cross-realm-object.d.ts.map +1 -0
  11. package/dist/debuglog.d.ts +11 -0
  12. package/dist/debuglog.d.ts.map +1 -1
  13. package/dist/index.d.ts +4 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +817 -7
  16. package/dist/index.js.map +1 -1
  17. package/dist/message-wrapper.d.ts +9 -0
  18. package/dist/message-wrapper.d.ts.map +1 -0
  19. package/dist/object-simulator.d.ts +28 -0
  20. package/dist/object-simulator.d.ts.map +1 -0
  21. package/dist/object-simulator.test.d.ts +2 -0
  22. package/dist/object-simulator.test.d.ts.map +1 -0
  23. package/dist/object-walker.d.ts +29 -0
  24. package/dist/object-walker.d.ts.map +1 -0
  25. package/dist/promises/index.d.ts +3 -0
  26. package/dist/promises/index.d.ts.map +1 -0
  27. package/dist/promises/promise-wrappers.test.d.ts +2 -0
  28. package/dist/promises/promise-wrappers.test.d.ts.map +1 -0
  29. package/dist/promises/timed.d.ts +15 -0
  30. package/dist/promises/timed.d.ts.map +1 -0
  31. package/dist/promises/wait.d.ts +7 -0
  32. package/dist/promises/wait.d.ts.map +1 -0
  33. package/dist/remote-subject.d.ts +70 -0
  34. package/dist/remote-subject.d.ts.map +1 -0
  35. package/dist/rpc/call-receiver.d.ts +4 -0
  36. package/dist/rpc/call-receiver.d.ts.map +1 -0
  37. package/dist/rpc/call-receiver.test.d.ts +2 -0
  38. package/dist/rpc/call-receiver.test.d.ts.map +1 -0
  39. package/dist/rpc/call-sender.d.ts +4 -0
  40. package/dist/rpc/call-sender.d.ts.map +1 -0
  41. package/dist/rpc/call-sender.test.d.ts +2 -0
  42. package/dist/rpc/call-sender.test.d.ts.map +1 -0
  43. package/dist/rpc/index.d.ts +3 -0
  44. package/dist/rpc/index.d.ts.map +1 -0
  45. package/dist/tickets.d.ts +34 -0
  46. package/dist/tickets.d.ts.map +1 -0
  47. package/dist/tunnel/index.d.ts +2 -0
  48. package/dist/tunnel/index.d.ts.map +1 -0
  49. package/dist/tunnel/tunnel-message.d.ts +19 -0
  50. package/dist/tunnel/tunnel-message.d.ts.map +1 -0
  51. package/dist/tunnel/tunnel.d.ts +58 -0
  52. package/dist/tunnel/tunnel.d.ts.map +1 -0
  53. package/dist/tunnel/tunnel.test.d.ts +2 -0
  54. package/dist/tunnel/tunnel.test.d.ts.map +1 -0
  55. package/dist/types.d.ts +1 -4
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/value-assertions.d.ts +10 -0
  58. package/dist/value-assertions.d.ts.map +1 -0
  59. package/package.json +4 -1
  60. package/src/__helpers__/jest.messagechannel.cjs +3 -0
  61. package/src/__mocks__/mock-finalization-registry.ts +13 -0
  62. package/src/__mocks__/mock-weak-ref.ts +10 -0
  63. package/src/constants.ts +6 -0
  64. package/src/cross-realm-object.ts +117 -0
  65. package/src/debuglog.ts +1 -1
  66. package/src/index.ts +4 -1
  67. package/src/message-wrapper.ts +35 -0
  68. package/src/object-simulator.test.ts +135 -0
  69. package/src/object-simulator.ts +136 -0
  70. package/src/object-walker.ts +92 -0
  71. package/src/promises/index.ts +2 -0
  72. package/src/promises/promise-wrappers.test.ts +63 -0
  73. package/src/promises/timed.ts +41 -0
  74. package/src/promises/wait.ts +10 -0
  75. package/src/remote-subject.ts +185 -0
  76. package/src/rpc/call-receiver.test.ts +90 -0
  77. package/src/rpc/call-receiver.ts +29 -0
  78. package/src/rpc/call-sender.test.ts +73 -0
  79. package/src/rpc/call-sender.ts +72 -0
  80. package/src/rpc/index.ts +2 -0
  81. package/src/tickets.ts +71 -0
  82. package/src/tunnel/index.ts +1 -0
  83. package/src/tunnel/tunnel-message.ts +75 -0
  84. package/src/tunnel/tunnel.test.ts +196 -0
  85. package/src/tunnel/tunnel.ts +311 -0
  86. package/src/types.ts +3 -5
  87. package/src/value-assertions.ts +48 -0
  88. package/tsconfig.json +2 -6
  89. package/dist/timeout-promise.d.ts +0 -12
  90. package/dist/timeout-promise.d.ts.map +0 -1
  91. package/src/timeout-promise.ts +0 -36
@@ -0,0 +1,92 @@
1
+ import type { WrappedMessage } from "./message-wrapper";
2
+ import type { DefTicket } from "./tickets";
3
+ import {
4
+ Primitive,
5
+ isPlainObject,
6
+ isPrimitive,
7
+ isIterable,
8
+ } from "./value-assertions";
9
+
10
+ /**
11
+ * Extract keys of T whose values are assignable to U.
12
+ * @internal
13
+ */
14
+ type ExtractKeys<T, U> = {
15
+ [P in keyof T]: T[P] extends U ? P : never;
16
+ }[keyof T];
17
+
18
+ /**
19
+ * Convert all functions anywhere in T to async functions.
20
+ * @internal
21
+ */
22
+ export type Asynced<T> = T extends (...args: infer A) => infer R
23
+ ? (...args: A) => Promise<R>
24
+ : {
25
+ [K in ExtractKeys<
26
+ T,
27
+ Function | object | any[] | [any, any]
28
+ >]: T[K] extends (...args: any) => PromiseLike<any>
29
+ ? T[K]
30
+ : T[K] extends [infer U, infer V]
31
+ ? [Asynced<U>, Asynced<V>]
32
+ : T[K] extends (infer U)[]
33
+ ? Asynced<U>[]
34
+ : T[K] extends (...args: infer A) => infer R
35
+ ? (...args: A) => Promise<R>
36
+ : Asynced<T[K]>;
37
+ };
38
+
39
+ /** @internal */
40
+ export type Materialized<T> = T extends Primitive
41
+ ? T
42
+ : // : T extends (...args: infer A) => infer R
43
+ // ? (...args: A) => Promise<R>
44
+ T extends Simulated<infer U>
45
+ ? Asynced<U>
46
+ : Asynced<T>;
47
+
48
+ /** @internal */
49
+ export type DefMessage = WrappedMessage<DefTicket>;
50
+
51
+ /** @internal */
52
+ export type Simulated<T> = {
53
+ [K in ExtractKeys<T, Function | object>]: T[K] extends (
54
+ ...args: unknown[]
55
+ ) => unknown
56
+ ? DefMessage
57
+ : Simulated<T[K]>;
58
+ };
59
+
60
+ export const NOT_TRANSFORMED = Symbol.for("NOT_TRANSFORMED");
61
+
62
+ export function transformRecursive<To>(
63
+ transform: (source: unknown) => To | typeof NOT_TRANSFORMED,
64
+ value: unknown
65
+ ): To {
66
+ if (isPrimitive(value)) {
67
+ return value as To;
68
+ }
69
+ const transformed = transform(value);
70
+ if (transformed !== NOT_TRANSFORMED) {
71
+ return transformed;
72
+ }
73
+ if (isIterable(value)) {
74
+ const outArray = [];
75
+ for (const item of value) {
76
+ outArray.push(transformRecursive(transform, item));
77
+ }
78
+ return outArray as To;
79
+ }
80
+ if (isPlainObject(value)) {
81
+ const outObj = {};
82
+ for (const key of Reflect.ownKeys(value)) {
83
+ Reflect.set(
84
+ outObj,
85
+ key,
86
+ transformRecursive(transform, Reflect.get(value, key))
87
+ );
88
+ }
89
+ return outObj as To;
90
+ }
91
+ throw new Error(`Bad value! ${Object.prototype.toString.call(value)}`);
92
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./timed";
2
+ export * from "./wait";
@@ -0,0 +1,63 @@
1
+ import { wait } from "./wait";
2
+ import { timeoutPromise } from "./timed";
3
+
4
+ describe("promise wrappers", () => {
5
+ const DONE = {};
6
+ const finish = jest.fn().mockReturnValue(DONE);
7
+ beforeEach(() => {
8
+ finish.mockClear();
9
+ jest.useFakeTimers();
10
+ });
11
+ afterEach(() => {
12
+ jest.useRealTimers();
13
+ });
14
+ describe("wait(ms)", () => {
15
+ it("returns a promise that resolves after n ms", async () => {
16
+ const sayDone = wait(500).then(finish);
17
+ jest.advanceTimersByTime(300);
18
+ expect(finish).not.toHaveBeenCalled();
19
+ jest.advanceTimersByTime(300);
20
+ await expect(sayDone).resolves.toBe(DONE);
21
+ expect(finish).toHaveBeenCalled();
22
+ });
23
+ });
24
+ describe("timeoutPromise(promise, ms, cleanup) returns a promise", () => {
25
+ const cleanup = jest.fn();
26
+ beforeEach(() => cleanup.mockReset());
27
+ it("that resolves with the original promise within ms", async () => {
28
+ const makesIt = timeoutPromise(
29
+ "should not fail",
30
+ wait(50).then(finish),
31
+ 100,
32
+ cleanup
33
+ );
34
+ jest.advanceTimersByTime(75);
35
+ await expect(makesIt).resolves.toBe(DONE);
36
+ expect(cleanup).not.toHaveBeenCalled();
37
+ });
38
+ it("that rejects if the original promise did not resolve within ms", async () => {
39
+ const makesIt = timeoutPromise(
40
+ "took too long",
41
+ wait(200).then(finish),
42
+ 100,
43
+ cleanup
44
+ );
45
+ jest.advanceTimersByTime(200);
46
+ await expect(makesIt).rejects.toThrowError(
47
+ "took too long timed out after 100ms"
48
+ );
49
+ expect(cleanup).toHaveBeenCalled();
50
+ });
51
+ it("runs the cleanup function if the promise rejects before timeout", async () => {
52
+ const ngmi = timeoutPromise(
53
+ "not a chance",
54
+ Promise.reject(new Error("loool")),
55
+ 100,
56
+ cleanup
57
+ );
58
+ jest.advanceTimersByTime(50);
59
+ await expect(ngmi).rejects.toThrowError("loool");
60
+ expect(cleanup).toHaveBeenCalled();
61
+ });
62
+ });
63
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Add a timeout to a Promise. The returned Promise will resolve to the value of
3
+ * the original Promise, but if it doesn't resolve within the timeout interval,
4
+ * it will reject with a timeout error.
5
+ *
6
+ * @param description - Job description to be used in the timeout error
7
+ * @param promise - Original promise to set a timeout for
8
+ * @param timeoutMs - Time to wait (ms) before rejecting
9
+ * @param onReject - Run when promise times out to clean up handles
10
+ * @returns - Promise that rejects with informative error after X milliseconds have passed
11
+ *
12
+ * @internal
13
+ */
14
+ export function timeoutPromise<T>(
15
+ description: string,
16
+ promise: Promise<T>,
17
+ ms: number,
18
+ onReject: (e: Error) => void
19
+ ): Promise<T> {
20
+ return new Promise((resolve, reject) => {
21
+ const cleanupAndReject = async (e: Error) => {
22
+ try {
23
+ await onReject(e);
24
+ } finally {
25
+ reject(e);
26
+ }
27
+ };
28
+ const timeout = setTimeout(() => {
29
+ cleanupAndReject(new Error(`${description} timed out after ${ms}ms`));
30
+ }, ms);
31
+ promise
32
+ .then((result) => {
33
+ clearTimeout(timeout);
34
+ resolve(result);
35
+ })
36
+ .catch((e) => {
37
+ clearTimeout(timeout);
38
+ cleanupAndReject(e);
39
+ });
40
+ });
41
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Promise that resolves after a specific time
3
+ *
4
+ * @internal
5
+ */
6
+ export function wait(ms: number) {
7
+ return new Promise((resolve) => {
8
+ setTimeout(resolve, ms);
9
+ });
10
+ }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./call-receiver";
2
+ export * from "./call-sender";