@adobe/uix-core 0.7.0 → 0.7.1-nightly.20230115
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/constants.d.ts +2 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.js +136 -47
- package/dist/index.js.map +1 -1
- package/dist/object-simulator.d.ts +1 -1
- package/dist/object-simulator.d.ts.map +1 -1
- package/dist/object-walker.d.ts +2 -1
- package/dist/object-walker.d.ts.map +1 -1
- 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 +4 -0
- package/dist/tunnel/tunnel.d.ts.map +1 -1
- package/dist/value-assertions.d.ts +3 -0
- package/dist/value-assertions.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +5 -1
- package/src/object-simulator.test.ts +193 -0
- package/src/object-simulator.ts +12 -3
- package/src/object-walker.ts +45 -5
- package/src/tunnel/tunnel-messenger.test.ts +183 -0
- package/src/tunnel/tunnel-messenger.ts +99 -0
- package/src/tunnel/tunnel.test.ts +20 -5
- package/src/tunnel/tunnel.ts +22 -11
- package/src/value-assertions.ts +10 -0
- package/dist/tunnel/tunnel-message.d.ts +0 -19
- package/dist/tunnel/tunnel-message.d.ts.map +0 -1
- package/src/tunnel/tunnel-message.ts +0 -75
|
@@ -120,6 +120,199 @@ describe("function simulator exchanges functions and tickets", () => {
|
|
|
120
120
|
await expect(loneFn()).resolves.not.toThrowError();
|
|
121
121
|
expect(called).toBe(true);
|
|
122
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
|
+
|
|
123
316
|
it("notifies remote when FinalizationRegistry calls cleanup handler", async () => {
|
|
124
317
|
const willBeGCed = objectSimulator.simulate(() => {}) as DefMessage;
|
|
125
318
|
objectSimulator.materialize(willBeGCed);
|
package/src/object-simulator.ts
CHANGED
|
@@ -86,7 +86,7 @@ export class ObjectSimulator implements Simulator {
|
|
|
86
86
|
|
|
87
87
|
// #region Public Methods
|
|
88
88
|
|
|
89
|
-
makeReceiver(fn: CallableFunction) {
|
|
89
|
+
makeReceiver(fn: CallableFunction, parent?: Object) {
|
|
90
90
|
if (typeof fn !== "function") {
|
|
91
91
|
return NOT_TRANSFORMED;
|
|
92
92
|
}
|
|
@@ -95,9 +95,18 @@ export class ObjectSimulator implements Simulator {
|
|
|
95
95
|
fnTicket = {
|
|
96
96
|
fnId: `${fn.name || "<anonymous>"}_${++this.fnCounter}`,
|
|
97
97
|
};
|
|
98
|
-
|
|
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
|
+
);
|
|
99
108
|
this.subject.onOutOfScope(fnTicket, cleanup);
|
|
100
|
-
this.receiverTicketCache.set(
|
|
109
|
+
this.receiverTicketCache.set(boundFunction, fnTicket);
|
|
101
110
|
}
|
|
102
111
|
return wrap(fnTicket);
|
|
103
112
|
}
|
package/src/object-walker.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
isPlainObject,
|
|
6
6
|
isPrimitive,
|
|
7
7
|
isIterable,
|
|
8
|
+
isObjectWithPrototype,
|
|
8
9
|
} from "./value-assertions";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -58,35 +59,74 @@ export type Simulated<T> = {
|
|
|
58
59
|
};
|
|
59
60
|
|
|
60
61
|
export const NOT_TRANSFORMED = Symbol.for("NOT_TRANSFORMED");
|
|
62
|
+
export const CIRCULAR = "[[Circular]]";
|
|
61
63
|
|
|
62
64
|
export function transformRecursive<To>(
|
|
63
|
-
transform: (source: unknown) => To | typeof NOT_TRANSFORMED,
|
|
64
|
-
value: unknown
|
|
65
|
+
transform: (source: unknown, parent?: Object) => To | typeof NOT_TRANSFORMED,
|
|
66
|
+
value: unknown,
|
|
67
|
+
parent?: Object,
|
|
68
|
+
_refs: WeakSet<object> = new WeakSet()
|
|
65
69
|
): To {
|
|
66
70
|
if (isPrimitive(value)) {
|
|
67
71
|
return value as To;
|
|
68
72
|
}
|
|
69
|
-
const transformed = transform(value);
|
|
73
|
+
const transformed = transform(value, parent);
|
|
70
74
|
if (transformed !== NOT_TRANSFORMED) {
|
|
71
75
|
return transformed;
|
|
72
76
|
}
|
|
73
77
|
if (isIterable(value)) {
|
|
74
78
|
const outArray = [];
|
|
75
79
|
for (const item of value) {
|
|
76
|
-
outArray.push(transformRecursive(transform, item));
|
|
80
|
+
outArray.push(transformRecursive(transform, item, undefined, _refs));
|
|
77
81
|
}
|
|
78
82
|
return outArray as To;
|
|
79
83
|
}
|
|
80
84
|
if (isPlainObject(value)) {
|
|
85
|
+
if (_refs.has(value)) {
|
|
86
|
+
return CIRCULAR as To;
|
|
87
|
+
}
|
|
88
|
+
_refs.add(value);
|
|
81
89
|
const outObj = {};
|
|
82
90
|
for (const key of Reflect.ownKeys(value)) {
|
|
83
91
|
Reflect.set(
|
|
84
92
|
outObj,
|
|
85
93
|
key,
|
|
86
|
-
transformRecursive(transform, Reflect.get(value, key))
|
|
94
|
+
transformRecursive(transform, Reflect.get(value, key), undefined, _refs)
|
|
87
95
|
);
|
|
88
96
|
}
|
|
89
97
|
return outObj as To;
|
|
90
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
|
+
|
|
91
131
|
throw new Error(`Bad value! ${Object.prototype.toString.call(value)}`);
|
|
92
132
|
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { NS_ROOT, VERSION } from "../constants";
|
|
2
|
+
import { TunnelMessenger } from "./tunnel-messenger";
|
|
3
|
+
|
|
4
|
+
const fakeConsole = {
|
|
5
|
+
error: jest.fn(),
|
|
6
|
+
warn: jest.fn(),
|
|
7
|
+
} as unknown as jest.Mocked<Console>;
|
|
8
|
+
|
|
9
|
+
describe("tunnel negotiation message factory", () => {
|
|
10
|
+
let messenger: TunnelMessenger;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
messenger = new TunnelMessenger({
|
|
13
|
+
myOrigin: "https://me",
|
|
14
|
+
targetOrigin: "https://you",
|
|
15
|
+
logger: fakeConsole,
|
|
16
|
+
});
|
|
17
|
+
jest.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
afterAll(() => {
|
|
20
|
+
jest.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
it("makeAccepted", () => {
|
|
23
|
+
expect(messenger.makeAccepted("test1")).toMatchObject({
|
|
24
|
+
[NS_ROOT]: {
|
|
25
|
+
accepts: "test1",
|
|
26
|
+
version: VERSION,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
it("makeOffered", () => {
|
|
31
|
+
expect(messenger.makeOffered("test1")).toMatchObject({
|
|
32
|
+
[NS_ROOT]: {
|
|
33
|
+
offers: "test1",
|
|
34
|
+
version: VERSION,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
it("isHandshakeOffer", () => {
|
|
39
|
+
expect(
|
|
40
|
+
messenger.isHandshakeOffer({
|
|
41
|
+
[NS_ROOT]: {
|
|
42
|
+
offers: "test2",
|
|
43
|
+
version: VERSION,
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
).toBeTruthy();
|
|
47
|
+
expect(
|
|
48
|
+
messenger.isHandshakeOffer({
|
|
49
|
+
[NS_ROOT]: {
|
|
50
|
+
accepts: "test2",
|
|
51
|
+
version: VERSION,
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
).toBeFalsy();
|
|
55
|
+
expect(messenger.isHandshakeOffer({})).toBeFalsy();
|
|
56
|
+
});
|
|
57
|
+
it("isHandshakeAccepting(message, id) matches on id", () => {
|
|
58
|
+
expect(
|
|
59
|
+
messenger.isHandshakeAccepting(
|
|
60
|
+
{
|
|
61
|
+
[NS_ROOT]: {
|
|
62
|
+
accepts: "test3",
|
|
63
|
+
version: VERSION,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
"test3"
|
|
67
|
+
)
|
|
68
|
+
).toBeTruthy();
|
|
69
|
+
expect(
|
|
70
|
+
messenger.isHandshakeAccepting(
|
|
71
|
+
{
|
|
72
|
+
accepts: "test3",
|
|
73
|
+
version: VERSION,
|
|
74
|
+
},
|
|
75
|
+
"mismatch"
|
|
76
|
+
)
|
|
77
|
+
).toBeFalsy();
|
|
78
|
+
expect(messenger.isHandshakeAccepting({}, "test3")).toBeFalsy();
|
|
79
|
+
});
|
|
80
|
+
describe("isHandshake rejects malformed messages", () => {
|
|
81
|
+
it("non-plain-objects", () => {
|
|
82
|
+
expect(messenger.isHandshake([])).toBeFalsy();
|
|
83
|
+
expect(messenger.isHandshake("")).toBeFalsy();
|
|
84
|
+
expect(messenger.isHandshake(true)).toBeFalsy();
|
|
85
|
+
expect(fakeConsole.error).toHaveBeenCalledTimes(3);
|
|
86
|
+
expect(fakeConsole.error.mock.calls.map(([msg]) => msg))
|
|
87
|
+
.toMatchInlineSnapshot(`
|
|
88
|
+
[
|
|
89
|
+
"Malformed tunnel message sent from SDK at https://you to https://me:
|
|
90
|
+
[]
|
|
91
|
+
Message must be an object with "_$pg" property, which must be an object with a "version" string and an either an "accepts" or "offers" property containing an ID string.",
|
|
92
|
+
"Malformed tunnel message sent from SDK at https://you to https://me:
|
|
93
|
+
""
|
|
94
|
+
Message must be an object with "_$pg" property, which must be an object with a "version" string and an either an "accepts" or "offers" property containing an ID string.",
|
|
95
|
+
"Malformed tunnel message sent from SDK at https://you to https://me:
|
|
96
|
+
true
|
|
97
|
+
Message must be an object with "_$pg" property, which must be an object with a "version" string and an either an "accepts" or "offers" property containing an ID string.",
|
|
98
|
+
]
|
|
99
|
+
`);
|
|
100
|
+
});
|
|
101
|
+
it("without a sub-object at the expected root property", () => {
|
|
102
|
+
expect(
|
|
103
|
+
messenger.isHandshakeOffer({
|
|
104
|
+
someOtherRoot: false,
|
|
105
|
+
})
|
|
106
|
+
).toBeFalsy();
|
|
107
|
+
expect(
|
|
108
|
+
messenger.isHandshake({
|
|
109
|
+
[NS_ROOT]: 5,
|
|
110
|
+
})
|
|
111
|
+
).toBeFalsy();
|
|
112
|
+
expect(fakeConsole.error.mock.calls.map(([msg]) => msg))
|
|
113
|
+
.toMatchInlineSnapshot(`
|
|
114
|
+
[
|
|
115
|
+
"Malformed tunnel message sent from SDK at https://you to https://me:
|
|
116
|
+
{
|
|
117
|
+
"someOtherRoot": false
|
|
118
|
+
}
|
|
119
|
+
Message must be an object with "_$pg" property, which must be an object with a "version" string and an either an "accepts" or "offers" property containing an ID string.",
|
|
120
|
+
"Malformed tunnel message sent from SDK at https://you to https://me:
|
|
121
|
+
{
|
|
122
|
+
"_$pg": 5
|
|
123
|
+
}
|
|
124
|
+
Message must be an object with "_$pg" property, which must be an object with a "version" string and an either an "accepts" or "offers" property containing an ID string.",
|
|
125
|
+
]
|
|
126
|
+
`);
|
|
127
|
+
});
|
|
128
|
+
it("without accept or offers properties", () => {
|
|
129
|
+
expect(
|
|
130
|
+
messenger.isHandshake({
|
|
131
|
+
[NS_ROOT]: {
|
|
132
|
+
version: VERSION,
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
).toBeFalsy();
|
|
136
|
+
});
|
|
137
|
+
it("with no version string", () => {
|
|
138
|
+
expect(
|
|
139
|
+
messenger.isHandshake({
|
|
140
|
+
[NS_ROOT]: {
|
|
141
|
+
offers: "test4",
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
).toBeFalsy();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe("version mismatch handling", () => {
|
|
148
|
+
const withVersion = (version: string) => ({
|
|
149
|
+
[NS_ROOT]: {
|
|
150
|
+
offers: "test5",
|
|
151
|
+
version,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
it("warns in console, once for each version", () => {
|
|
155
|
+
expect(messenger.isHandshake(withVersion("bad-version"))).toBeTruthy();
|
|
156
|
+
expect(messenger.isHandshake(withVersion("worse-version"))).toBeTruthy();
|
|
157
|
+
expect(messenger.isHandshake(withVersion("bad-version"))).toBeTruthy();
|
|
158
|
+
expect(fakeConsole.warn).toHaveBeenCalledTimes(2);
|
|
159
|
+
expect(fakeConsole.warn.mock.calls.map(([msg]) => msg))
|
|
160
|
+
.toMatchInlineSnapshot(`
|
|
161
|
+
[
|
|
162
|
+
"SDK version mismatch. https://me is using v0.0.1-test, but received message from https://you using SDK vbad-version. Extensions may be broken or unresponsive.",
|
|
163
|
+
"SDK version mismatch. https://me is using v0.0.1-test, but received message from https://you using SDK vworse-version. Extensions may be broken or unresponsive.",
|
|
164
|
+
]
|
|
165
|
+
`);
|
|
166
|
+
});
|
|
167
|
+
it("resetWarnings() resets seen version warnings so they'll log again", () => {
|
|
168
|
+
messenger.resetWarnings();
|
|
169
|
+
expect(
|
|
170
|
+
messenger.isHandshake(withVersion("same-bad-version"))
|
|
171
|
+
).toBeTruthy();
|
|
172
|
+
expect(
|
|
173
|
+
messenger.isHandshake(withVersion("same-bad-version"))
|
|
174
|
+
).toBeTruthy();
|
|
175
|
+
expect(fakeConsole.warn).toHaveBeenCalledTimes(1);
|
|
176
|
+
messenger.resetWarnings();
|
|
177
|
+
expect(
|
|
178
|
+
messenger.isHandshake(withVersion("same-bad-version"))
|
|
179
|
+
).toBeTruthy();
|
|
180
|
+
expect(fakeConsole.warn).toHaveBeenCalledTimes(2);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { NS_ROOT, VERSION } from "../constants";
|
|
2
|
+
import { isPlainObject } from "../value-assertions";
|
|
3
|
+
import { WrappedMessage, isWrapped, wrap, unwrap } from "../message-wrapper";
|
|
4
|
+
import { HandshakeAcceptedTicket, HandshakeOfferedTicket } from "../tickets";
|
|
5
|
+
|
|
6
|
+
type Handshake = HandshakeAcceptedTicket | HandshakeOfferedTicket;
|
|
7
|
+
type HandshakeAccepted = WrappedMessage<HandshakeAcceptedTicket>;
|
|
8
|
+
type HandshakeOffered = WrappedMessage<HandshakeOfferedTicket>;
|
|
9
|
+
type HandshakeMessage = HandshakeAccepted | HandshakeOffered;
|
|
10
|
+
|
|
11
|
+
export class TunnelMessenger {
|
|
12
|
+
private myOrigin: string;
|
|
13
|
+
private remoteOrigin: string;
|
|
14
|
+
private logger: Console;
|
|
15
|
+
private versionWarnings = new Set<string>();
|
|
16
|
+
constructor(opts: {
|
|
17
|
+
myOrigin: string;
|
|
18
|
+
targetOrigin: string;
|
|
19
|
+
logger: Console;
|
|
20
|
+
}) {
|
|
21
|
+
this.myOrigin = opts.myOrigin;
|
|
22
|
+
this.remoteOrigin =
|
|
23
|
+
opts.targetOrigin === "*" ? "remote document" : opts.targetOrigin;
|
|
24
|
+
this.logger = opts.logger;
|
|
25
|
+
}
|
|
26
|
+
resetWarnings() {
|
|
27
|
+
this.versionWarnings.clear();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
makeAccepted(id: string): HandshakeAccepted {
|
|
31
|
+
return wrap({
|
|
32
|
+
accepts: id,
|
|
33
|
+
version: VERSION,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
makeOffered(id: string): HandshakeOffered {
|
|
37
|
+
return wrap({
|
|
38
|
+
offers: id,
|
|
39
|
+
version: VERSION,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
isHandshakeAccepting(
|
|
43
|
+
message: unknown,
|
|
44
|
+
id: string
|
|
45
|
+
): message is HandshakeAccepted {
|
|
46
|
+
return (
|
|
47
|
+
this.isHandshake(message) &&
|
|
48
|
+
unwrap(message as HandshakeAccepted).accepts === id
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
isHandshakeOffer(message: unknown): message is HandshakeOffered {
|
|
52
|
+
return (
|
|
53
|
+
this.isHandshake(message) &&
|
|
54
|
+
typeof unwrap(message as HandshakeOffered).offers === "string"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
isHandshake(message: unknown): message is HandshakeMessage {
|
|
58
|
+
if (!isWrapped(message)) {
|
|
59
|
+
this.logMalformed(message);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const tunnelData: Handshake = unwrap<Handshake>(
|
|
63
|
+
message as HandshakeMessage
|
|
64
|
+
);
|
|
65
|
+
if (
|
|
66
|
+
!isPlainObject(tunnelData) ||
|
|
67
|
+
typeof tunnelData.version !== "string" ||
|
|
68
|
+
!(Reflect.has(tunnelData, "accepts") || Reflect.has(tunnelData, "offers"))
|
|
69
|
+
) {
|
|
70
|
+
this.logMalformed(message);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const { version } = tunnelData;
|
|
74
|
+
if (version !== VERSION && !this.versionWarnings.has(version)) {
|
|
75
|
+
this.versionWarnings.add(version);
|
|
76
|
+
this.logger.warn(
|
|
77
|
+
`SDK version mismatch. ${this.myOrigin} is using v${VERSION}, but received message from ${this.remoteOrigin} using SDK v${version}. Extensions may be broken or unresponsive.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
private logMalformed(message: unknown) {
|
|
83
|
+
let inspectedMessage: string;
|
|
84
|
+
try {
|
|
85
|
+
inspectedMessage = JSON.stringify(message, null, 2);
|
|
86
|
+
} catch (_) {
|
|
87
|
+
try {
|
|
88
|
+
inspectedMessage = message.toString();
|
|
89
|
+
} catch (e) {
|
|
90
|
+
inspectedMessage = Object.prototype.toString.call(message);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
this.logger.error(
|
|
94
|
+
`Malformed tunnel message sent from SDK at ${this.remoteOrigin} to ${this.myOrigin}:
|
|
95
|
+
${inspectedMessage}
|
|
96
|
+
Message must be an object with "${NS_ROOT}" property, which must be an object with a "version" string and an either an "accepts" or "offers" property containing an ID string.`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { fireEvent } from "@testing-library/dom";
|
|
2
2
|
import { wait } from "../promises/wait";
|
|
3
3
|
import { Tunnel } from "./tunnel";
|
|
4
|
-
import {
|
|
4
|
+
import { TunnelMessenger } from "./tunnel-messenger";
|
|
5
|
+
|
|
6
|
+
const fakeConsole = {
|
|
7
|
+
error: jest.fn(),
|
|
8
|
+
warn: jest.fn(),
|
|
9
|
+
} as unknown as jest.Mocked<Console>;
|
|
5
10
|
|
|
6
11
|
const defaultTunnelConfig = {
|
|
7
12
|
targetOrigin: "*",
|
|
8
13
|
timeout: 4000,
|
|
14
|
+
logger: fakeConsole,
|
|
9
15
|
};
|
|
10
16
|
type TunnelHarness = { tunnel: Tunnel; port: MessagePort };
|
|
11
17
|
const openPorts: MessagePort[] = [];
|
|
@@ -163,20 +169,26 @@ describe("static Tunnel.toIframe(iframe, options)", () => {
|
|
|
163
169
|
let remoteTunnel: Tunnel;
|
|
164
170
|
const connectMessageHandler = jest.fn();
|
|
165
171
|
const acceptListener = jest.fn();
|
|
172
|
+
const targetOrigin = "https://example.com:4001";
|
|
166
173
|
const loadedFrame = document.createElement("iframe");
|
|
167
|
-
loadedFrame.src =
|
|
174
|
+
loadedFrame.src = targetOrigin;
|
|
168
175
|
document.body.appendChild(loadedFrame);
|
|
169
176
|
loadedFrame.contentWindow.addEventListener("message", acceptListener);
|
|
170
177
|
const localTunnel = Tunnel.toIframe(loadedFrame, {
|
|
171
|
-
targetOrigin
|
|
178
|
+
targetOrigin,
|
|
172
179
|
timeout: 9999,
|
|
173
180
|
});
|
|
181
|
+
const messenger = new TunnelMessenger({
|
|
182
|
+
myOrigin: "https://example.com",
|
|
183
|
+
targetOrigin,
|
|
184
|
+
logger: fakeConsole,
|
|
185
|
+
});
|
|
174
186
|
localTunnel.on("connected", connectMessageHandler);
|
|
175
187
|
await wait(100);
|
|
176
188
|
fireEvent(
|
|
177
189
|
window,
|
|
178
190
|
new MessageEvent("message", {
|
|
179
|
-
data: makeOffered("iframe-test-1"),
|
|
191
|
+
data: messenger.makeOffered("iframe-test-1"),
|
|
180
192
|
origin: loadedFrame.src,
|
|
181
193
|
source: loadedFrame.contentWindow,
|
|
182
194
|
})
|
|
@@ -184,7 +196,10 @@ describe("static Tunnel.toIframe(iframe, options)", () => {
|
|
|
184
196
|
await wait(100);
|
|
185
197
|
expect(acceptListener).toHaveBeenCalled();
|
|
186
198
|
const acceptEvent = acceptListener.mock.lastCall[0];
|
|
187
|
-
expect(acceptEvent).toHaveProperty(
|
|
199
|
+
expect(acceptEvent).toHaveProperty(
|
|
200
|
+
"data",
|
|
201
|
+
messenger.makeAccepted("iframe-test-1")
|
|
202
|
+
);
|
|
188
203
|
expect(acceptEvent.ports).toHaveLength(1);
|
|
189
204
|
remoteTunnel = new Tunnel(defaultTunnelConfig);
|
|
190
205
|
remoteTunnel.connect(acceptEvent.ports[0]);
|