@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.
Files changed (94) 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 +8 -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 +906 -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 +30 -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-messenger.d.ts +25 -0
  50. package/dist/tunnel/tunnel-messenger.d.ts.map +1 -0
  51. package/dist/tunnel/tunnel-messenger.test.d.ts +2 -0
  52. package/dist/tunnel/tunnel-messenger.test.d.ts.map +1 -0
  53. package/dist/tunnel/tunnel.d.ts +62 -0
  54. package/dist/tunnel/tunnel.d.ts.map +1 -0
  55. package/dist/tunnel/tunnel.test.d.ts +2 -0
  56. package/dist/tunnel/tunnel.test.d.ts.map +1 -0
  57. package/dist/types.d.ts +1 -4
  58. package/dist/types.d.ts.map +1 -1
  59. package/dist/value-assertions.d.ts +13 -0
  60. package/dist/value-assertions.d.ts.map +1 -0
  61. package/package.json +1 -1
  62. package/src/__helpers__/jest.messagechannel.cjs +3 -0
  63. package/src/__mocks__/mock-finalization-registry.ts +13 -0
  64. package/src/__mocks__/mock-weak-ref.ts +10 -0
  65. package/src/constants.ts +10 -0
  66. package/src/cross-realm-object.ts +117 -0
  67. package/src/debuglog.ts +1 -1
  68. package/src/index.ts +4 -1
  69. package/src/message-wrapper.ts +35 -0
  70. package/src/object-simulator.test.ts +328 -0
  71. package/src/object-simulator.ts +145 -0
  72. package/src/object-walker.ts +132 -0
  73. package/src/promises/index.ts +2 -0
  74. package/src/promises/promise-wrappers.test.ts +63 -0
  75. package/src/promises/timed.ts +41 -0
  76. package/src/promises/wait.ts +10 -0
  77. package/src/remote-subject.ts +185 -0
  78. package/src/rpc/call-receiver.test.ts +90 -0
  79. package/src/rpc/call-receiver.ts +29 -0
  80. package/src/rpc/call-sender.test.ts +73 -0
  81. package/src/rpc/call-sender.ts +72 -0
  82. package/src/rpc/index.ts +2 -0
  83. package/src/tickets.ts +71 -0
  84. package/src/tunnel/index.ts +1 -0
  85. package/src/tunnel/tunnel-messenger.test.ts +183 -0
  86. package/src/tunnel/tunnel-messenger.ts +99 -0
  87. package/src/tunnel/tunnel.test.ts +211 -0
  88. package/src/tunnel/tunnel.ts +322 -0
  89. package/src/types.ts +3 -5
  90. package/src/value-assertions.ts +58 -0
  91. package/tsconfig.json +2 -6
  92. package/dist/timeout-promise.d.ts +0 -12
  93. package/dist/timeout-promise.d.ts.map +0 -1
  94. 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,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
+ }