@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,328 @@
|
|
|
1
|
+
import { NS_ROOT } from "./constants";
|
|
2
|
+
import { EventEmitter } from "eventemitter3";
|
|
3
|
+
import { ObjectSimulator } from "./object-simulator";
|
|
4
|
+
import { FakeFinalizationRegistry } from "./__mocks__/mock-finalization-registry";
|
|
5
|
+
import { wait } from "./promises/wait";
|
|
6
|
+
import { DefMessage } from "./object-walker";
|
|
7
|
+
|
|
8
|
+
describe("function simulator exchanges functions and tickets", () => {
|
|
9
|
+
let objectSimulator: ObjectSimulator;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.spyOn(console, "error").mockImplementation(() => {});
|
|
12
|
+
const emitter = new EventEmitter();
|
|
13
|
+
objectSimulator = ObjectSimulator.create(emitter, FakeFinalizationRegistry);
|
|
14
|
+
});
|
|
15
|
+
it("turns an object with functions into an object with tickets", async () => {
|
|
16
|
+
const invokeIt = (blorp: CallableFunction) => blorp();
|
|
17
|
+
const gnorf = {
|
|
18
|
+
slorf: {
|
|
19
|
+
blorf: (x: number) => x + 1,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
const toBeTicketed = {
|
|
23
|
+
list: [
|
|
24
|
+
{
|
|
25
|
+
what: 8,
|
|
26
|
+
doa: invokeIt,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
gnorf,
|
|
30
|
+
harbl: 3,
|
|
31
|
+
};
|
|
32
|
+
const ticketed = objectSimulator.simulate(toBeTicketed);
|
|
33
|
+
expect(ticketed).toMatchInlineSnapshot(`
|
|
34
|
+
{
|
|
35
|
+
"gnorf": {
|
|
36
|
+
"slorf": {
|
|
37
|
+
"blorf": {
|
|
38
|
+
"_\$pg": {
|
|
39
|
+
"fnId": "blorf_2",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
"harbl": 3,
|
|
45
|
+
"list": [
|
|
46
|
+
{
|
|
47
|
+
"doa": {
|
|
48
|
+
"_\$pg": {
|
|
49
|
+
"fnId": "invokeIt_1",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
"what": 8,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
}
|
|
56
|
+
`);
|
|
57
|
+
const unticketed = objectSimulator.materialize(ticketed);
|
|
58
|
+
expect(unticketed).toMatchInlineSnapshot(`
|
|
59
|
+
{
|
|
60
|
+
"gnorf": {
|
|
61
|
+
"slorf": {
|
|
62
|
+
"blorf": [Function],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
"harbl": 3,
|
|
66
|
+
"list": [
|
|
67
|
+
{
|
|
68
|
+
"doa": [Function],
|
|
69
|
+
"what": 8,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
}
|
|
73
|
+
`);
|
|
74
|
+
const remoteInvokeIt = unticketed.list[0].doa;
|
|
75
|
+
await expect(remoteInvokeIt(() => "oh noes")).resolves.toBe("oh noes");
|
|
76
|
+
await expect(unticketed.gnorf.slorf.blorf(9)).resolves.toBe(10);
|
|
77
|
+
});
|
|
78
|
+
it("dies when an object has an unrecognizable value", () => {
|
|
79
|
+
expect(() =>
|
|
80
|
+
objectSimulator.simulate({
|
|
81
|
+
lol: Symbol("lol"),
|
|
82
|
+
})
|
|
83
|
+
).toThrowError("Bad value");
|
|
84
|
+
});
|
|
85
|
+
it("passes through tickets when unexpected", () => {
|
|
86
|
+
const hasTicket = {
|
|
87
|
+
[NS_ROOT]: {
|
|
88
|
+
some: "ticket",
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
const doTicket = () => objectSimulator.simulate({ hasTicket });
|
|
92
|
+
expect(doTicket).not.toThrowError();
|
|
93
|
+
expect(doTicket()).toMatchObject({ hasTicket });
|
|
94
|
+
});
|
|
95
|
+
it("strips unserializable props, but throws on unserializable values", () => {
|
|
96
|
+
expect(() =>
|
|
97
|
+
objectSimulator.simulate({
|
|
98
|
+
lol: Symbol("lol"),
|
|
99
|
+
})
|
|
100
|
+
).toThrowError("Bad value");
|
|
101
|
+
expect(() =>
|
|
102
|
+
objectSimulator.simulate({
|
|
103
|
+
[Symbol("lol")]: "lol",
|
|
104
|
+
})
|
|
105
|
+
).not.toThrowError();
|
|
106
|
+
});
|
|
107
|
+
it("can handle root functions", async () => {
|
|
108
|
+
let called = false;
|
|
109
|
+
const ticketedLoneFn = objectSimulator.simulate(() => {
|
|
110
|
+
called = true;
|
|
111
|
+
});
|
|
112
|
+
expect(ticketedLoneFn).toMatchInlineSnapshot(`
|
|
113
|
+
{
|
|
114
|
+
"_$pg": {
|
|
115
|
+
"fnId": "<anonymous>_1",
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
`);
|
|
119
|
+
const loneFn = objectSimulator.materialize(ticketedLoneFn);
|
|
120
|
+
await expect(loneFn()).resolves.not.toThrowError();
|
|
121
|
+
expect(called).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
it("Unwraps prototypes and exchange all functions to tickets", async () => {
|
|
124
|
+
class ca {
|
|
125
|
+
pa: number;
|
|
126
|
+
constructor() {
|
|
127
|
+
this.pa = 4;
|
|
128
|
+
}
|
|
129
|
+
getPa() {
|
|
130
|
+
return this.pa;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
class cb {
|
|
134
|
+
pb: number;
|
|
135
|
+
ca: ca;
|
|
136
|
+
constructor(aa: number) {
|
|
137
|
+
this.pb = aa;
|
|
138
|
+
this.ca = new ca();
|
|
139
|
+
}
|
|
140
|
+
getNumber() {
|
|
141
|
+
return this.pb;
|
|
142
|
+
}
|
|
143
|
+
giftOne() {
|
|
144
|
+
this.pb--;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
class cd extends cb {
|
|
148
|
+
giftOne() {
|
|
149
|
+
this.pb++;
|
|
150
|
+
}
|
|
151
|
+
robOne() {
|
|
152
|
+
this.pb--;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const toBeTicketed = new cd(5);
|
|
157
|
+
const ticketed = objectSimulator.simulate(toBeTicketed);
|
|
158
|
+
expect(ticketed).toMatchInlineSnapshot(`
|
|
159
|
+
{
|
|
160
|
+
"ca": {
|
|
161
|
+
"getPa": {
|
|
162
|
+
"_\$pg": {
|
|
163
|
+
"fnId": "getPa_1",
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
"pa": 4,
|
|
167
|
+
},
|
|
168
|
+
"getNumber": {
|
|
169
|
+
"_\$pg": {
|
|
170
|
+
"fnId": "getNumber_4",
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
"giftOne": {
|
|
174
|
+
"_\$pg": {
|
|
175
|
+
"fnId": "giftOne_2",
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
"pb": 5,
|
|
179
|
+
"robOne": {
|
|
180
|
+
"_\$pg": {
|
|
181
|
+
"fnId": "robOne_3",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
`);
|
|
186
|
+
const unticketed = objectSimulator.materialize(ticketed);
|
|
187
|
+
expect(unticketed).toMatchInlineSnapshot(`
|
|
188
|
+
{
|
|
189
|
+
"ca": {
|
|
190
|
+
"getPa": [Function],
|
|
191
|
+
"pa": 4,
|
|
192
|
+
},
|
|
193
|
+
"getNumber": [Function],
|
|
194
|
+
"giftOne": [Function],
|
|
195
|
+
"pb": 5,
|
|
196
|
+
"robOne": [Function],
|
|
197
|
+
}
|
|
198
|
+
`);
|
|
199
|
+
|
|
200
|
+
await expect(unticketed.getNumber()).resolves.toBe(5);
|
|
201
|
+
await unticketed.giftOne();
|
|
202
|
+
await expect(unticketed.getNumber()).resolves.toBe(6);
|
|
203
|
+
await unticketed.robOne();
|
|
204
|
+
await expect(unticketed.getNumber()).resolves.toBe(5);
|
|
205
|
+
await expect(unticketed.ca.getPa()).resolves.toBe(4);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("Ignores circular dependencies in properties, but relove them through the methods", async () => {
|
|
209
|
+
class ca {
|
|
210
|
+
pa: ca;
|
|
211
|
+
constructor() {
|
|
212
|
+
this.pa = this;
|
|
213
|
+
}
|
|
214
|
+
getPa() {
|
|
215
|
+
return this.pa;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const toBeTicketed = new ca();
|
|
220
|
+
const ticketed = objectSimulator.simulate(toBeTicketed);
|
|
221
|
+
expect(ticketed).toMatchInlineSnapshot(`
|
|
222
|
+
{
|
|
223
|
+
"getPa": {
|
|
224
|
+
"_$pg": {
|
|
225
|
+
"fnId": "getPa_1",
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
"pa": "[[Circular]]",
|
|
229
|
+
}
|
|
230
|
+
`);
|
|
231
|
+
const unticketed = objectSimulator.materialize(ticketed);
|
|
232
|
+
expect(unticketed).toMatchInlineSnapshot(`
|
|
233
|
+
{
|
|
234
|
+
"getPa": [Function],
|
|
235
|
+
"pa": "[[Circular]]",
|
|
236
|
+
}
|
|
237
|
+
`);
|
|
238
|
+
expect(
|
|
239
|
+
Reflect.has(
|
|
240
|
+
await (await (await unticketed.getPa()).getPa()).getPa(),
|
|
241
|
+
"getPa"
|
|
242
|
+
)
|
|
243
|
+
).toBeTruthy();
|
|
244
|
+
});
|
|
245
|
+
it("Supports classes wrapped in other classes", async () => {
|
|
246
|
+
class ca {
|
|
247
|
+
pa: number;
|
|
248
|
+
constructor() {
|
|
249
|
+
this.pa = 5;
|
|
250
|
+
}
|
|
251
|
+
getPa() {
|
|
252
|
+
return this.pa;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
class cb {
|
|
256
|
+
pb: ca;
|
|
257
|
+
constructor() {
|
|
258
|
+
this.pb = new ca();
|
|
259
|
+
}
|
|
260
|
+
getCaValue() {
|
|
261
|
+
return this.pb.getPa();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const toBeTicketed = new cb();
|
|
265
|
+
const ticketed = objectSimulator.simulate(toBeTicketed);
|
|
266
|
+
expect(ticketed).toMatchInlineSnapshot(`
|
|
267
|
+
{
|
|
268
|
+
"getCaValue": {
|
|
269
|
+
"_$pg": {
|
|
270
|
+
"fnId": "getCaValue_2",
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
"pb": {
|
|
274
|
+
"getPa": {
|
|
275
|
+
"_$pg": {
|
|
276
|
+
"fnId": "getPa_1",
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
"pa": 5,
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
`);
|
|
283
|
+
const unticketed = objectSimulator.materialize(ticketed);
|
|
284
|
+
expect(unticketed).toMatchInlineSnapshot(`
|
|
285
|
+
{
|
|
286
|
+
"getCaValue": [Function],
|
|
287
|
+
"pb": {
|
|
288
|
+
"getPa": [Function],
|
|
289
|
+
"pa": 5,
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
`);
|
|
293
|
+
await expect(unticketed.pb.getPa()).resolves.toBe(5);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("Supports objects with null prototypes", async () => {
|
|
297
|
+
const toBeTicketed = Object.create(null);
|
|
298
|
+
toBeTicketed["key1"] = "val1";
|
|
299
|
+
toBeTicketed["key2"] = "val2";
|
|
300
|
+
const ticketed = objectSimulator.simulate(toBeTicketed);
|
|
301
|
+
expect(ticketed).toMatchInlineSnapshot(`
|
|
302
|
+
{
|
|
303
|
+
"key1": "val1",
|
|
304
|
+
"key2": "val2",
|
|
305
|
+
}
|
|
306
|
+
`);
|
|
307
|
+
const unticketed = objectSimulator.materialize(ticketed);
|
|
308
|
+
expect(unticketed).toMatchInlineSnapshot(`
|
|
309
|
+
{
|
|
310
|
+
"key1": "val1",
|
|
311
|
+
"key2": "val2",
|
|
312
|
+
}
|
|
313
|
+
`);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("notifies remote when FinalizationRegistry calls cleanup handler", async () => {
|
|
317
|
+
const willBeGCed = objectSimulator.simulate(() => {}) as DefMessage;
|
|
318
|
+
objectSimulator.materialize(willBeGCed);
|
|
319
|
+
const { subject } = objectSimulator;
|
|
320
|
+
const fakeTicket = willBeGCed[NS_ROOT];
|
|
321
|
+
const gcHandler = jest.fn();
|
|
322
|
+
subject.onOutOfScope(fakeTicket, gcHandler);
|
|
323
|
+
const lastCleanupHandler = FakeFinalizationRegistry.mock.cleanupHandler;
|
|
324
|
+
lastCleanupHandler(fakeTicket.fnId);
|
|
325
|
+
await wait(100);
|
|
326
|
+
expect(gcHandler).toHaveBeenCalled();
|
|
327
|
+
});
|
|
328
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { isWrapped, unwrap, wrap } from "./message-wrapper";
|
|
2
|
+
import EventEmitter from "eventemitter3";
|
|
3
|
+
import type { DefMessage, Materialized, Simulated } from "./object-walker";
|
|
4
|
+
import { NOT_TRANSFORMED, transformRecursive } from "./object-walker";
|
|
5
|
+
import { makeCallSender, receiveCalls } from "./rpc";
|
|
6
|
+
import type { Simulator } from "./remote-subject";
|
|
7
|
+
import { RemoteSubject } from "./remote-subject";
|
|
8
|
+
import type { DefTicket } from "./tickets";
|
|
9
|
+
import { hasProp } from "./value-assertions";
|
|
10
|
+
|
|
11
|
+
function isDefMessage(value: unknown): value is DefMessage {
|
|
12
|
+
return isWrapped(value) && hasProp(unwrap(value), "fnId");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const bindAll = <T>(inst: T, methods: (keyof T)[]) => {
|
|
16
|
+
for (const methodName of methods) {
|
|
17
|
+
const method = inst[methodName];
|
|
18
|
+
if (typeof method === "function") {
|
|
19
|
+
inst[methodName] = method.bind(inst);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
interface CleanupNotifier {
|
|
25
|
+
// #region Public Methods
|
|
26
|
+
|
|
27
|
+
register(obj: any, heldValue: string, ref?: any): void;
|
|
28
|
+
unregister(ref: any): void;
|
|
29
|
+
|
|
30
|
+
// #endregion Public Methods
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface CleanupNotifierConstructor {
|
|
34
|
+
new (callback: (heldValue: unknown) => void): CleanupNotifier;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class ObjectSimulator implements Simulator {
|
|
38
|
+
// #region Properties
|
|
39
|
+
|
|
40
|
+
private cleanupNotifier: CleanupNotifier;
|
|
41
|
+
private fnCounter = 0;
|
|
42
|
+
private receiverTicketCache: WeakMap<CallableFunction, DefTicket> =
|
|
43
|
+
new WeakMap();
|
|
44
|
+
private senderCache: WeakMap<DefTicket, CallableFunction> = new WeakMap();
|
|
45
|
+
|
|
46
|
+
subject: RemoteSubject;
|
|
47
|
+
|
|
48
|
+
// #endregion Properties
|
|
49
|
+
|
|
50
|
+
// #region Constructors
|
|
51
|
+
|
|
52
|
+
constructor(subject: RemoteSubject, cleanupNotifier: CleanupNotifier) {
|
|
53
|
+
this.cleanupNotifier = cleanupNotifier;
|
|
54
|
+
this.subject = subject;
|
|
55
|
+
|
|
56
|
+
bindAll(this, ["makeSender", "makeReceiver", "simulate", "materialize"]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// #endregion Constructors
|
|
60
|
+
|
|
61
|
+
// #region Public Static Methods
|
|
62
|
+
|
|
63
|
+
static create(
|
|
64
|
+
emitter: EventEmitter,
|
|
65
|
+
Cleanup: CleanupNotifierConstructor
|
|
66
|
+
): ObjectSimulator {
|
|
67
|
+
let simulator: Simulator;
|
|
68
|
+
// proxy simulator, so as not to have cyclic dependency
|
|
69
|
+
const simulatorInterface: Simulator = {
|
|
70
|
+
simulate: (x) => simulator.simulate(x),
|
|
71
|
+
materialize: (x) => simulator.materialize(x),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const subject = new RemoteSubject(emitter, simulatorInterface);
|
|
75
|
+
|
|
76
|
+
const cleanupNotifier = new Cleanup((fnId: string) => {
|
|
77
|
+
return subject.notifyCleanup({ fnId });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
simulator = new ObjectSimulator(subject, cleanupNotifier);
|
|
81
|
+
|
|
82
|
+
return simulator as ObjectSimulator;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// #endregion Public Static Methods
|
|
86
|
+
|
|
87
|
+
// #region Public Methods
|
|
88
|
+
|
|
89
|
+
makeReceiver(fn: CallableFunction, parent?: Object) {
|
|
90
|
+
if (typeof fn !== "function") {
|
|
91
|
+
return NOT_TRANSFORMED;
|
|
92
|
+
}
|
|
93
|
+
let fnTicket = this.receiverTicketCache.get(fn);
|
|
94
|
+
if (!fnTicket) {
|
|
95
|
+
fnTicket = {
|
|
96
|
+
fnId: `${fn.name || "<anonymous>"}_${++this.fnCounter}`,
|
|
97
|
+
};
|
|
98
|
+
// Bind function to parent object if it exists
|
|
99
|
+
let boundFunction = fn;
|
|
100
|
+
if (parent) {
|
|
101
|
+
boundFunction = fn.bind(parent);
|
|
102
|
+
}
|
|
103
|
+
const cleanup = receiveCalls(
|
|
104
|
+
boundFunction,
|
|
105
|
+
fnTicket,
|
|
106
|
+
new WeakRef(this.subject)
|
|
107
|
+
);
|
|
108
|
+
this.subject.onOutOfScope(fnTicket, cleanup);
|
|
109
|
+
this.receiverTicketCache.set(boundFunction, fnTicket);
|
|
110
|
+
}
|
|
111
|
+
return wrap(fnTicket);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
makeSender(message: unknown) {
|
|
115
|
+
if (!isDefMessage(message)) {
|
|
116
|
+
return NOT_TRANSFORMED;
|
|
117
|
+
}
|
|
118
|
+
const ticket = unwrap(message);
|
|
119
|
+
/* istanbul ignore else: preopt */
|
|
120
|
+
if (!this.senderCache.has(ticket)) {
|
|
121
|
+
const sender = makeCallSender(ticket, new WeakRef(this.subject));
|
|
122
|
+
this.cleanupNotifier.register(sender, ticket.fnId, sender);
|
|
123
|
+
this.senderCache.set(ticket, sender);
|
|
124
|
+
return sender;
|
|
125
|
+
} else {
|
|
126
|
+
return this.senderCache.get(ticket) as CallableFunction;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
materialize<T>(simulated: T) {
|
|
131
|
+
return transformRecursive<CallableFunction>(
|
|
132
|
+
this.makeSender,
|
|
133
|
+
simulated
|
|
134
|
+
) as Materialized<T>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
simulate<T>(localObject: T) {
|
|
138
|
+
return transformRecursive<DefMessage>(
|
|
139
|
+
this.makeReceiver,
|
|
140
|
+
localObject
|
|
141
|
+
) as Simulated<T>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// #endregion Public Methods
|
|
145
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { WrappedMessage } from "./message-wrapper";
|
|
2
|
+
import type { DefTicket } from "./tickets";
|
|
3
|
+
import {
|
|
4
|
+
Primitive,
|
|
5
|
+
isPlainObject,
|
|
6
|
+
isPrimitive,
|
|
7
|
+
isIterable,
|
|
8
|
+
isObjectWithPrototype,
|
|
9
|
+
} from "./value-assertions";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extract keys of T whose values are assignable to U.
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
type ExtractKeys<T, U> = {
|
|
16
|
+
[P in keyof T]: T[P] extends U ? P : never;
|
|
17
|
+
}[keyof T];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Convert all functions anywhere in T to async functions.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export type Asynced<T> = T extends (...args: infer A) => infer R
|
|
24
|
+
? (...args: A) => Promise<R>
|
|
25
|
+
: {
|
|
26
|
+
[K in ExtractKeys<
|
|
27
|
+
T,
|
|
28
|
+
Function | object | any[] | [any, any]
|
|
29
|
+
>]: T[K] extends (...args: any) => PromiseLike<any>
|
|
30
|
+
? T[K]
|
|
31
|
+
: T[K] extends [infer U, infer V]
|
|
32
|
+
? [Asynced<U>, Asynced<V>]
|
|
33
|
+
: T[K] extends (infer U)[]
|
|
34
|
+
? Asynced<U>[]
|
|
35
|
+
: T[K] extends (...args: infer A) => infer R
|
|
36
|
+
? (...args: A) => Promise<R>
|
|
37
|
+
: Asynced<T[K]>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** @internal */
|
|
41
|
+
export type Materialized<T> = T extends Primitive
|
|
42
|
+
? T
|
|
43
|
+
: // : T extends (...args: infer A) => infer R
|
|
44
|
+
// ? (...args: A) => Promise<R>
|
|
45
|
+
T extends Simulated<infer U>
|
|
46
|
+
? Asynced<U>
|
|
47
|
+
: Asynced<T>;
|
|
48
|
+
|
|
49
|
+
/** @internal */
|
|
50
|
+
export type DefMessage = WrappedMessage<DefTicket>;
|
|
51
|
+
|
|
52
|
+
/** @internal */
|
|
53
|
+
export type Simulated<T> = {
|
|
54
|
+
[K in ExtractKeys<T, Function | object>]: T[K] extends (
|
|
55
|
+
...args: unknown[]
|
|
56
|
+
) => unknown
|
|
57
|
+
? DefMessage
|
|
58
|
+
: Simulated<T[K]>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const NOT_TRANSFORMED = Symbol.for("NOT_TRANSFORMED");
|
|
62
|
+
export const CIRCULAR = "[[Circular]]";
|
|
63
|
+
|
|
64
|
+
export function transformRecursive<To>(
|
|
65
|
+
transform: (source: unknown, parent?: Object) => To | typeof NOT_TRANSFORMED,
|
|
66
|
+
value: unknown,
|
|
67
|
+
parent?: Object,
|
|
68
|
+
_refs: WeakSet<object> = new WeakSet()
|
|
69
|
+
): To {
|
|
70
|
+
if (isPrimitive(value)) {
|
|
71
|
+
return value as To;
|
|
72
|
+
}
|
|
73
|
+
const transformed = transform(value, parent);
|
|
74
|
+
if (transformed !== NOT_TRANSFORMED) {
|
|
75
|
+
return transformed;
|
|
76
|
+
}
|
|
77
|
+
if (isIterable(value)) {
|
|
78
|
+
const outArray = [];
|
|
79
|
+
for (const item of value) {
|
|
80
|
+
outArray.push(transformRecursive(transform, item, undefined, _refs));
|
|
81
|
+
}
|
|
82
|
+
return outArray as To;
|
|
83
|
+
}
|
|
84
|
+
if (isPlainObject(value)) {
|
|
85
|
+
if (_refs.has(value)) {
|
|
86
|
+
return CIRCULAR as To;
|
|
87
|
+
}
|
|
88
|
+
_refs.add(value);
|
|
89
|
+
const outObj = {};
|
|
90
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
91
|
+
Reflect.set(
|
|
92
|
+
outObj,
|
|
93
|
+
key,
|
|
94
|
+
transformRecursive(transform, Reflect.get(value, key), undefined, _refs)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return outObj as To;
|
|
98
|
+
}
|
|
99
|
+
if (isObjectWithPrototype(value)) {
|
|
100
|
+
if (_refs.has(value)) {
|
|
101
|
+
return CIRCULAR as To;
|
|
102
|
+
}
|
|
103
|
+
_refs.add(value);
|
|
104
|
+
const getObjectKeys = (obj: Object): (string | symbol)[] => {
|
|
105
|
+
const result: Set<string | symbol> = new Set();
|
|
106
|
+
do {
|
|
107
|
+
if (Reflect.getPrototypeOf(obj) !== null) {
|
|
108
|
+
for (const prop of Object.getOwnPropertyNames(obj)) {
|
|
109
|
+
if (prop === "constructor") {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
result.add(prop);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} while ((obj = Reflect.getPrototypeOf(obj)));
|
|
116
|
+
|
|
117
|
+
return [...result];
|
|
118
|
+
};
|
|
119
|
+
const outObj = {};
|
|
120
|
+
const properties = getObjectKeys(value);
|
|
121
|
+
for (const key of properties) {
|
|
122
|
+
Reflect.set(
|
|
123
|
+
outObj,
|
|
124
|
+
key,
|
|
125
|
+
transformRecursive(transform, Reflect.get(value, key), value, _refs)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return outObj as To;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
throw new Error(`Bad value! ${Object.prototype.toString.call(value)}`);
|
|
132
|
+
}
|
|
@@ -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
|
+
}
|