chrome-in-iframe 2.0.0 → 2.1.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.
package/dist/index.js CHANGED
@@ -1,6 +1,32 @@
1
1
  import { nanoid } from 'nanoid';
2
2
  import { LRUCache } from 'lru-cache';
3
3
 
4
+ const TRANSPORT_DETACHED = Symbol.for('chrome-in-iframe.transport-detached');
5
+ function isTransportDetachedError(err) {
6
+ return err instanceof Error && err[TRANSPORT_DETACHED] === true;
7
+ }
8
+ function createTransportDetachedError(message) {
9
+ const err = new Error(message);
10
+ Object.defineProperty(err, TRANSPORT_DETACHED, { value: true, enumerable: false });
11
+ return err;
12
+ }
13
+
14
+ const PREFIX = 'chrome-in-iframe';
15
+ const defaultLogger = (scope, ...args) => {
16
+ if (typeof console !== 'undefined' && typeof console.warn === 'function') {
17
+ console.warn(`[${PREFIX}] ${scope}`, ...args);
18
+ }
19
+ };
20
+ let activeLogger = defaultLogger;
21
+ function setLogger(logger) {
22
+ activeLogger = logger;
23
+ }
24
+ function warn(scope, ...args) {
25
+ if (!activeLogger)
26
+ return;
27
+ activeLogger(scope, ...args);
28
+ }
29
+
4
30
  const WELL_KNOWN_SYMBOLS = {
5
31
  asyncIterator: Symbol.asyncIterator,
6
32
  hasInstance: Symbol.hasInstance,
@@ -16,22 +42,52 @@ const WELL_KNOWN_SYMBOLS = {
16
42
  toStringTag: Symbol.toStringTag,
17
43
  unscopables: Symbol.unscopables,
18
44
  };
45
+ const WELL_KNOWN_SYMBOL_NAMES = new Map(Object.entries(WELL_KNOWN_SYMBOLS).map(([name, symbol]) => [symbol, name]));
19
46
  function getWellKnownSymbolName(value) {
20
- for (const [name, symbol] of Object.entries(WELL_KNOWN_SYMBOLS)) {
21
- if (symbol === value)
22
- return name;
23
- }
24
- return undefined;
47
+ return WELL_KNOWN_SYMBOL_NAMES.get(value);
25
48
  }
26
49
  function getWellKnownSymbol(name) {
27
50
  return WELL_KNOWN_SYMBOLS[name];
28
51
  }
52
+ function encodeSymbolToken(value, context) {
53
+ const globalKey = Symbol.keyFor(value);
54
+ if (globalKey)
55
+ return `global:${globalKey}`;
56
+ const wellKnownName = getWellKnownSymbolName(value);
57
+ if (wellKnownName)
58
+ return `wellKnown:${wellKnownName}`;
59
+ throw new TypeError(context ? `Cannot serialize non-global symbol ${context}` : 'Cannot serialize non-global symbol');
60
+ }
61
+ function decodeSymbolToken(value, context) {
62
+ if (value.startsWith('global:')) {
63
+ return Symbol.for(value.slice('global:'.length));
64
+ }
65
+ if (value.startsWith('wellKnown:')) {
66
+ const symbol = getWellKnownSymbol(value.slice('wellKnown:'.length));
67
+ if (symbol)
68
+ return symbol;
69
+ }
70
+ throw new TypeError(context ? `Cannot deserialize symbol ${context}` : 'Cannot deserialize symbol');
71
+ }
72
+ function setOwnProperty(target, key, value) {
73
+ Object.defineProperty(target, key, {
74
+ value,
75
+ enumerable: true,
76
+ configurable: true,
77
+ writable: true,
78
+ });
79
+ }
29
80
  function serializeThrownError(err) {
30
81
  if (err instanceof Error) {
31
82
  return { message: err.message, stack: err.stack };
32
83
  }
33
84
  return { message: String(err) };
34
85
  }
86
+ function isPromiseLike(value) {
87
+ return (value !== null &&
88
+ (typeof value === 'object' || typeof value === 'function') &&
89
+ typeof value.then === 'function');
90
+ }
35
91
 
36
92
  function serializePath(path) {
37
93
  return path.map((key) => {
@@ -39,7 +95,7 @@ function serializePath(path) {
39
95
  return key;
40
96
  return {
41
97
  $type: 'symbol',
42
- value: serializeSymbol$1(key),
98
+ value: encodeSymbolToken(key, 'path key'),
43
99
  };
44
100
  });
45
101
  }
@@ -47,7 +103,7 @@ function deserializePath(path) {
47
103
  return path.map((key) => {
48
104
  if (typeof key === 'string')
49
105
  return key;
50
- return deserializeSymbol$1(key.value);
106
+ return decodeSymbolToken(key.value, 'path key');
51
107
  });
52
108
  }
53
109
  function isSerializedPath(value) {
@@ -61,26 +117,6 @@ function isSerializedPathKey(value) {
61
117
  const record = value;
62
118
  return record.$type === 'symbol' && typeof record.value === 'string';
63
119
  }
64
- function serializeSymbol$1(value) {
65
- const globalKey = Symbol.keyFor(value);
66
- if (globalKey)
67
- return `global:${globalKey}`;
68
- const wellKnownName = getWellKnownSymbolName(value);
69
- if (wellKnownName)
70
- return `wellKnown:${wellKnownName}`;
71
- throw new TypeError('Cannot serialize non-global symbol path key');
72
- }
73
- function deserializeSymbol$1(value) {
74
- if (value.startsWith('global:')) {
75
- return Symbol.for(value.slice('global:'.length));
76
- }
77
- if (value.startsWith('wellKnown:')) {
78
- const symbol = getWellKnownSymbol(value.slice('wellKnown:'.length));
79
- if (symbol)
80
- return symbol;
81
- }
82
- throw new TypeError('Cannot deserialize symbol path key');
83
- }
84
120
 
85
121
  function createMessageSender(poster, key, instanceId) {
86
122
  return {
@@ -91,7 +127,7 @@ function createMessageSender(poster, key, instanceId) {
91
127
  data: serializeMessageData(type, data),
92
128
  senderInstanceId: instanceId,
93
129
  };
94
- if (targetInstanceId)
130
+ if (targetInstanceId !== undefined)
95
131
  body.targetInstanceId = targetInstanceId;
96
132
  poster.postMessage(JSON.stringify(body));
97
133
  },
@@ -115,17 +151,58 @@ function serializeMessageData(type, data) {
115
151
  return data;
116
152
  }
117
153
 
154
+ const MAX_PATH_LENGTH = 64;
155
+ const MAX_INVOKE_ARGS = 1024;
156
+ const MAX_MESSAGE_LENGTH = 1000000;
157
+ const MAX_RELEASE_IDS = 10000;
158
+ const MAX_ID_LENGTH = 200;
159
+ const MAX_TYPE_LENGTH = 64;
160
+ const MESSAGE_SPECS = {
161
+ connectRequest: { validate: isConnectMessage },
162
+ connectResponse: { validate: isConnectMessage },
163
+ invokeRequest: {
164
+ validate: isInvokeRequest,
165
+ preDeserialize: (data) => ({ ...data, path: deserializePath(data.path) }),
166
+ errorResponse: { type: 'invokeResponse', extractId: (data) => data.id },
167
+ },
168
+ invokeResponse: { validate: isResponse },
169
+ accessPropertyRequest: {
170
+ validate: isAccessPropertyRequest,
171
+ preDeserialize: (data) => ({ ...data, path: deserializePath(data.path) }),
172
+ errorResponse: { type: 'accessPropertyResponse', extractId: (data) => data.id },
173
+ },
174
+ accessPropertyResponse: { validate: isResponse },
175
+ invokeFunctionByIdRequest: {
176
+ validate: isCallbackRequest,
177
+ errorResponse: { type: 'invokeFunctionByIdResponse', extractId: (data) => data.callId },
178
+ },
179
+ invokeFunctionByIdResponse: { validate: isResponse },
180
+ releaseCallbacks: { validate: isReleaseCallbacks },
181
+ destroyEndpoint: { validate: isDestroyEndpoint },
182
+ };
118
183
  function createMessageChannel(poster, key, instanceId, context, processorRegistry) {
119
184
  const sender = createMessageSender(poster, key, instanceId);
185
+ const keyMatch = `"key":${JSON.stringify(key)}`;
120
186
  const listener = (event) => {
187
+ if (typeof event.data !== 'string')
188
+ return;
189
+ if (event.data.length === 0 || event.data.charCodeAt(0) !== 123)
190
+ return;
191
+ if (event.data.length > MAX_MESSAGE_LENGTH) {
192
+ warn('createMessageChannel', `dropping oversized message (${event.data.length} > ${MAX_MESSAGE_LENGTH})`);
193
+ return;
194
+ }
195
+ if (event.data.indexOf(keyMatch) === -1)
196
+ return;
121
197
  let body;
122
198
  try {
123
199
  body = JSON.parse(event.data);
124
200
  }
125
- catch {
201
+ catch (err) {
202
+ warn('createMessageChannel', 'failed to parse incoming message as JSON', err);
126
203
  return;
127
204
  }
128
- if (!isMessageBody(body))
205
+ if (!isMessageEnvelope(body))
129
206
  return;
130
207
  if (body.key !== key)
131
208
  return;
@@ -136,7 +213,10 @@ function createMessageChannel(poster, key, instanceId, context, processorRegistr
136
213
  const handler = processorRegistry.get(body.type);
137
214
  if (!handler)
138
215
  return;
216
+ if (!isValidMessagePayload(body))
217
+ return;
139
218
  const meta = { senderInstanceId: body.senderInstanceId };
219
+ context.noteRemoteSeen(body.senderInstanceId);
140
220
  try {
141
221
  handler(deserializeMessageData(body), context, meta);
142
222
  }
@@ -149,63 +229,55 @@ function createMessageChannel(poster, key, instanceId, context, processorRegistr
149
229
  getSender() {
150
230
  return sender;
151
231
  },
152
- getContext() {
153
- return context;
154
- },
155
- getPoster() {
156
- return poster;
157
- },
158
- getKey() {
159
- return key;
160
- },
161
- getInstanceId() {
162
- return instanceId;
163
- },
164
232
  destroy() {
165
233
  poster.removeEventListener('message', listener);
166
234
  },
167
235
  };
168
236
  }
169
- function isMessageBody(value) {
237
+ function isMessageEnvelope(value) {
170
238
  if (!isRecord(value))
171
239
  return false;
172
- if (typeof value.type !== 'string')
240
+ if (typeof value.type !== 'string' || value.type.length === 0 || value.type.length > MAX_TYPE_LENGTH)
173
241
  return false;
174
242
  if (typeof value.key !== 'string')
175
243
  return false;
176
- if (typeof value.senderInstanceId !== 'string')
244
+ if (typeof value.senderInstanceId !== 'string' ||
245
+ value.senderInstanceId.length === 0 ||
246
+ value.senderInstanceId.length > MAX_ID_LENGTH)
177
247
  return false;
178
- if ('targetInstanceId' in value && typeof value.targetInstanceId !== 'string' && value.targetInstanceId !== undefined)
248
+ if ('targetInstanceId' in value &&
249
+ value.targetInstanceId !== undefined &&
250
+ (typeof value.targetInstanceId !== 'string' || value.targetInstanceId.length > MAX_ID_LENGTH))
179
251
  return false;
180
- switch (value.type) {
181
- case 'invokeRequest':
182
- return isInvokeRequest(value.data);
183
- case 'invokeResponse':
184
- return isResponse(value.data);
185
- case 'accessPropertyRequest':
186
- return isAccessPropertyRequest(value.data);
187
- case 'accessPropertyResponse':
188
- return isResponse(value.data);
189
- case 'invokeFunctionByIdRequest':
190
- return isCallbackRequest(value.data);
191
- case 'invokeFunctionByIdResponse':
192
- return isResponse(value.data);
193
- case 'releaseCallbacks':
194
- return isReleaseCallbacks(value.data);
195
- case 'destroyEndpoint':
196
- return isDestroyEndpoint(value.data);
197
- default:
198
- return false;
199
- }
252
+ return true;
253
+ }
254
+ function isValidMessagePayload(value) {
255
+ const spec = MESSAGE_SPECS[value.type];
256
+ return spec ? spec.validate(value.data) : false;
257
+ }
258
+ function isConnectMessage(value) {
259
+ return isRecord(value) && isMessageId(value.instanceId);
200
260
  }
201
261
  function isInvokeRequest(value) {
202
- return isRecord(value) && isMessageId(value.id) && isSerializedPath(value.path) && Array.isArray(value.args);
262
+ return (isRecord(value) &&
263
+ isMessageId(value.id) &&
264
+ isSerializedPath(value.path) &&
265
+ value.path.length <= MAX_PATH_LENGTH &&
266
+ Array.isArray(value.args) &&
267
+ value.args.length <= MAX_INVOKE_ARGS);
203
268
  }
204
269
  function isAccessPropertyRequest(value) {
205
- return isRecord(value) && isMessageId(value.id) && isSerializedPath(value.path);
270
+ return (isRecord(value) &&
271
+ isMessageId(value.id) &&
272
+ isSerializedPath(value.path) &&
273
+ value.path.length <= MAX_PATH_LENGTH);
206
274
  }
207
275
  function isCallbackRequest(value) {
208
- return isRecord(value) && isMessageId(value.id) && isMessageId(value.callId) && Array.isArray(value.args);
276
+ return (isRecord(value) &&
277
+ isMessageId(value.id) &&
278
+ isMessageId(value.callId) &&
279
+ Array.isArray(value.args) &&
280
+ value.args.length <= MAX_INVOKE_ARGS);
209
281
  }
210
282
  function isResponse(value) {
211
283
  return (isRecord(value) &&
@@ -214,31 +286,21 @@ function isResponse(value) {
214
286
  ('data' in value || 'error' in value));
215
287
  }
216
288
  function isReleaseCallbacks(value) {
217
- return isRecord(value) && Array.isArray(value.ids) && value.ids.every((id) => typeof id === 'string');
289
+ return (isRecord(value) && Array.isArray(value.ids) && value.ids.length <= MAX_RELEASE_IDS && value.ids.every(isMessageId));
218
290
  }
219
291
  function isDestroyEndpoint(value) {
220
- return isRecord(value) && typeof value.instanceId === 'string';
292
+ return isRecord(value) && isMessageId(value.instanceId);
221
293
  }
222
294
  function isSerializedError(value) {
223
295
  return (isRecord(value) && typeof value.message === 'string' && (!('stack' in value) || typeof value.stack === 'string'));
224
296
  }
225
297
  function isMessageId(value) {
226
- return typeof value === 'string' && value.length > 0;
298
+ return typeof value === 'string' && value.length > 0 && value.length <= MAX_ID_LENGTH;
227
299
  }
228
300
  function deserializeMessageData(body) {
229
- if (body.type === 'invokeRequest') {
230
- const data = body.data;
231
- return {
232
- ...data,
233
- path: deserializePath(data.path),
234
- };
235
- }
236
- if (body.type === 'accessPropertyRequest') {
237
- const data = body.data;
238
- return {
239
- ...data,
240
- path: deserializePath(data.path),
241
- };
301
+ const spec = MESSAGE_SPECS[body.type];
302
+ if (spec.preDeserialize) {
303
+ return spec.preDeserialize(body.data);
242
304
  }
243
305
  return body.data;
244
306
  }
@@ -246,38 +308,39 @@ function isRecord(value) {
246
308
  return typeof value === 'object' && value !== null;
247
309
  }
248
310
  function sendProcessorError(body, sender, err) {
249
- if (body.type === 'invokeRequest') {
250
- const data = body.data;
251
- sender.sendMessage('invokeResponse', {
252
- id: data.id,
253
- error: serializeThrownError(err),
254
- }, body.senderInstanceId);
255
- return;
256
- }
257
- if (body.type === 'accessPropertyRequest') {
258
- const data = body.data;
259
- sender.sendMessage('accessPropertyResponse', {
260
- id: data.id,
261
- error: serializeThrownError(err),
262
- }, body.senderInstanceId);
311
+ const spec = MESSAGE_SPECS[body.type];
312
+ if (!spec.errorResponse)
263
313
  return;
264
- }
314
+ const error = serializeThrownError(err);
265
315
  if (body.type === 'invokeFunctionByIdRequest') {
266
316
  const data = body.data;
267
- const error = serializeThrownError(err);
268
- console.warn(`chrome-in-iframe: callback '${data.id}' failed: ${error.message}`);
269
- sender.sendMessage('invokeFunctionByIdResponse', {
270
- id: data.callId,
271
- error,
272
- }, body.senderInstanceId);
317
+ warn('sendProcessorError', `callback '${data.id}' failed: ${error.message}`);
273
318
  }
319
+ sender.sendMessage(spec.errorResponse.type, { id: spec.errorResponse.extractId(body.data), error }, body.senderInstanceId);
320
+ }
321
+
322
+ function isListenerRegistrationPath(path) {
323
+ const last = path[path.length - 1];
324
+ return last === 'addListener';
325
+ }
326
+ function isListenerRemovalPath(path) {
327
+ const last = path[path.length - 1];
328
+ return last === 'removeListener';
329
+ }
330
+ function isLikelyListenerPath(path) {
331
+ const last = path[path.length - 1];
332
+ return typeof last === 'string' && /^on[A-Z]/.test(last);
274
333
  }
275
334
 
276
335
  const TYPE_KEY = '$cii$';
336
+ const MAX_SERIALIZE_DEPTH = 200;
277
337
  function serialize(arg, registerFunction, options = {}) {
278
- return serialize0(arg, undefined, registerFunction, options, new WeakSet());
338
+ return serialize0(arg, undefined, registerFunction, options, new WeakSet(), 0);
279
339
  }
280
- function serialize0(arg, owner, registerFunction, options, seen) {
340
+ function serialize0(arg, owner, registerFunction, options, seen, depth) {
341
+ if (depth > MAX_SERIALIZE_DEPTH) {
342
+ throw new TypeError(`Serialization exceeded maximum depth of ${MAX_SERIALIZE_DEPTH}`);
343
+ }
281
344
  if (arg === undefined)
282
345
  return { [TYPE_KEY]: 'undef' };
283
346
  if (arg === null)
@@ -296,15 +359,22 @@ function serialize0(arg, owner, registerFunction, options, seen) {
296
359
  return options.persistent ? { [TYPE_KEY]: 'fn', id, persistent: true } : { [TYPE_KEY]: 'fn', id };
297
360
  }
298
361
  if (typeOf === 'symbol')
299
- return { [TYPE_KEY]: 'sym', v: serializeSymbol(arg) };
362
+ return { [TYPE_KEY]: 'sym', v: encodeSymbolToken(arg) };
300
363
  if (typeOf === 'object') {
301
364
  if (seen.has(arg)) {
302
365
  throw new TypeError('Cannot serialize circular structure');
303
366
  }
304
367
  seen.add(arg);
305
368
  try {
369
+ if (Array.isArray(arg)) {
370
+ return arg.map((item) => serialize0(item, arg, registerFunction, options, seen, depth + 1));
371
+ }
372
+ const proto = Object.getPrototypeOf(arg);
373
+ if (proto === Object.prototype || proto === null) {
374
+ return serializePlainObject(arg, registerFunction, options, seen, depth);
375
+ }
306
376
  if (arg instanceof Error)
307
- return serializeError(arg, owner, registerFunction, options, seen);
377
+ return serializeError(arg, owner, registerFunction, options, seen, depth);
308
378
  if (arg instanceof Date)
309
379
  return { [TYPE_KEY]: 'date', v: arg.getTime() };
310
380
  if (arg instanceof RegExp)
@@ -313,8 +383,8 @@ function serialize0(arg, owner, registerFunction, options, seen) {
313
383
  const entries = [];
314
384
  for (const [k, v] of arg.entries()) {
315
385
  entries.push([
316
- serialize0(k, arg, registerFunction, options, seen),
317
- serialize0(v, arg, registerFunction, options, seen),
386
+ serialize0(k, arg, registerFunction, options, seen, depth + 1),
387
+ serialize0(v, arg, registerFunction, options, seen, depth + 1),
318
388
  ]);
319
389
  }
320
390
  return { [TYPE_KEY]: 'map', entries };
@@ -322,14 +392,11 @@ function serialize0(arg, owner, registerFunction, options, seen) {
322
392
  if (arg instanceof Set) {
323
393
  const values = [];
324
394
  for (const v of arg.values()) {
325
- values.push(serialize0(v, arg, registerFunction, options, seen));
395
+ values.push(serialize0(v, arg, registerFunction, options, seen, depth + 1));
326
396
  }
327
397
  return { [TYPE_KEY]: 'set', values };
328
398
  }
329
- if (Array.isArray(arg)) {
330
- return arg.map((item) => serialize0(item, arg, registerFunction, options, seen));
331
- }
332
- return serializePlainObject(arg, registerFunction, options, seen);
399
+ return serializePlainObject(arg, registerFunction, options, seen, depth);
333
400
  }
334
401
  finally {
335
402
  seen.delete(arg);
@@ -337,7 +404,7 @@ function serialize0(arg, owner, registerFunction, options, seen) {
337
404
  }
338
405
  return null;
339
406
  }
340
- function serializeError(err, owner, registerFunction, options, seen) {
407
+ function serializeError(err, owner, registerFunction, options, seen, depth) {
341
408
  const result = { [TYPE_KEY]: 'err', message: err.message };
342
409
  if (err.name && err.name !== 'Error')
343
410
  result.name = err.name;
@@ -345,7 +412,7 @@ function serializeError(err, owner, registerFunction, options, seen) {
345
412
  result.stack = err.stack;
346
413
  const cause = err.cause;
347
414
  if (cause !== undefined) {
348
- result.cause = serialize0(cause, owner, registerFunction, options, seen);
415
+ result.cause = serialize0(cause, owner, registerFunction, options, seen, depth + 1);
349
416
  }
350
417
  const RESERVED_ERROR_KEYS = new Set(['message', 'name', 'stack', 'cause']);
351
418
  const extras = [];
@@ -354,14 +421,16 @@ function serializeError(err, owner, registerFunction, options, seen) {
354
421
  continue;
355
422
  if (RESERVED_ERROR_KEYS.has(k))
356
423
  continue;
424
+ if (!isEnumerable(err, k))
425
+ continue;
357
426
  const value = err[k];
358
- extras.push([k, serialize0(value, owner, registerFunction, options, seen)]);
427
+ extras.push([k, serialize0(value, owner, registerFunction, options, seen, depth + 1)]);
359
428
  }
360
429
  if (extras.length > 0)
361
430
  result.extras = extras;
362
431
  return result;
363
432
  }
364
- function serializePlainObject(source, registerFunction, options, seen) {
433
+ function serializePlainObject(source, registerFunction, options, seen, depth) {
365
434
  const record = source;
366
435
  const ownKeys = Reflect.ownKeys(source).filter((k) => isEnumerable(source, k));
367
436
  const symbolKeys = [];
@@ -379,19 +448,25 @@ function serializePlainObject(source, registerFunction, options, seen) {
379
448
  if (symbolKeys.length > 0 || hasReservedKey) {
380
449
  const entries = [];
381
450
  for (const k of stringKeys) {
382
- entries.push([k, serialize0(record[k], source, registerFunction, options, seen)]);
451
+ entries.push([k, serialize0(record[k], source, registerFunction, options, seen, depth + 1)]);
383
452
  }
384
453
  for (const k of symbolKeys) {
385
454
  entries.push([
386
- { [TYPE_KEY]: 'sym', v: serializeSymbol(k) },
387
- serialize0(record[k], source, registerFunction, options, seen),
455
+ { [TYPE_KEY]: 'sym', v: encodeSymbolToken(k) },
456
+ serialize0(record[k], source, registerFunction, options, seen, depth + 1),
388
457
  ]);
389
458
  }
390
459
  return { [TYPE_KEY]: 'obj', entries };
391
460
  }
392
461
  const result = {};
393
462
  for (const k of stringKeys) {
394
- setOwnProperty$1(result, k, serialize0(record[k], source, registerFunction, options, seen));
463
+ const value = serialize0(record[k], source, registerFunction, options, seen, depth + 1);
464
+ if (k === '__proto__') {
465
+ setOwnProperty(result, k, value);
466
+ }
467
+ else {
468
+ result[k] = value;
469
+ }
395
470
  }
396
471
  return result;
397
472
  }
@@ -415,56 +490,61 @@ function serializeNumber(value) {
415
490
  return { [TYPE_KEY]: 'num', v: '-0' };
416
491
  return value;
417
492
  }
418
- function setOwnProperty$1(target, key, value) {
419
- Object.defineProperty(target, key, {
420
- value,
421
- enumerable: true,
422
- configurable: true,
423
- writable: true,
424
- });
425
- }
426
- function serializeSymbol(value) {
427
- const globalKey = Symbol.keyFor(value);
428
- if (globalKey)
429
- return `global:${globalKey}`;
430
- const wellKnownName = getWellKnownSymbolName(value);
431
- if (wellKnownName)
432
- return `wellKnown:${wellKnownName}`;
433
- throw new TypeError('Cannot serialize non-global symbol');
434
- }
435
493
 
436
- const ERROR_CONSTRUCTORS = {
437
- Error,
438
- TypeError: TypeError,
439
- RangeError: RangeError,
440
- SyntaxError: SyntaxError,
441
- ReferenceError: ReferenceError,
442
- URIError: URIError,
443
- EvalError: EvalError,
494
+ const MAX_DESERIALIZE_DEPTH = 200;
495
+ const ERROR_FACTORIES = {
496
+ Error: (m) => new Error(m),
497
+ TypeError: (m) => new TypeError(m),
498
+ RangeError: (m) => new RangeError(m),
499
+ SyntaxError: (m) => new SyntaxError(m),
500
+ ReferenceError: (m) => new ReferenceError(m),
501
+ URIError: (m) => new URIError(m),
502
+ EvalError: (m) => new EvalError(m),
444
503
  };
504
+ const globalScope = globalThis;
505
+ const AggregateErrorCtorRef = globalScope.AggregateError;
506
+ if (typeof AggregateErrorCtorRef === 'function') {
507
+ ERROR_FACTORIES.AggregateError = (m) => new AggregateErrorCtorRef([], m);
508
+ }
509
+ const DOMExceptionCtorRef = globalScope.DOMException;
510
+ if (typeof DOMExceptionCtorRef === 'function') {
511
+ ERROR_FACTORIES.DOMException = (m) => new DOMExceptionCtorRef(m);
512
+ }
445
513
  function deserialize(arg, generateCallback, getRemoteCallback) {
446
- return deserialize0(arg, generateCallback, getRemoteCallback);
514
+ return deserialize0(arg, generateCallback, getRemoteCallback, 0);
447
515
  }
448
- function deserialize0(arg, generateCallback, getRemoteCallback) {
516
+ function deserialize0(arg, generateCallback, getRemoteCallback, depth) {
517
+ if (depth > MAX_DESERIALIZE_DEPTH) {
518
+ throw new TypeError(`Deserialization exceeded maximum depth of ${MAX_DESERIALIZE_DEPTH}`);
519
+ }
449
520
  if (arg === null)
450
521
  return null;
451
522
  if (typeof arg !== 'object')
452
523
  return arg;
453
524
  if (Array.isArray(arg)) {
454
- return arg.map((item) => deserialize0(item, generateCallback, getRemoteCallback));
525
+ return arg.map((item) => deserialize0(item, generateCallback, getRemoteCallback, depth + 1));
455
526
  }
456
527
  const source = arg;
457
528
  const tag = source[TYPE_KEY];
458
529
  if (typeof tag === 'string') {
459
- return deserializeTagged(tag, source, generateCallback, getRemoteCallback);
530
+ return deserializeTagged(tag, source, generateCallback, getRemoteCallback, depth);
460
531
  }
532
+ return deserializePlainObject(source, generateCallback, getRemoteCallback, depth);
533
+ }
534
+ function deserializePlainObject(source, generateCallback, getRemoteCallback, depth) {
461
535
  const result = {};
462
536
  for (const key of Object.keys(source)) {
463
- setOwnProperty(result, key, deserialize0(source[key], generateCallback, getRemoteCallback));
537
+ const value = deserialize0(source[key], generateCallback, getRemoteCallback, depth + 1);
538
+ if (key === '__proto__') {
539
+ setOwnProperty(result, key, value);
540
+ }
541
+ else {
542
+ result[key] = value;
543
+ }
464
544
  }
465
545
  return result;
466
546
  }
467
- function deserializeTagged(tag, source, generateCallback, getRemoteCallback) {
547
+ function deserializeTagged(tag, source, generateCallback, getRemoteCallback, depth) {
468
548
  switch (tag) {
469
549
  case 'undef':
470
550
  return undefined;
@@ -475,26 +555,21 @@ function deserializeTagged(tag, source, generateCallback, getRemoteCallback) {
475
555
  case 'num':
476
556
  return deserializeNumber(expectString(source.v, 'number'));
477
557
  case 'err':
478
- return deserializeError(source, generateCallback, getRemoteCallback);
558
+ return deserializeError(source, generateCallback, getRemoteCallback, depth);
479
559
  case 'date':
480
560
  return deserializeDate(source.v);
481
561
  case 're':
482
562
  return deserializeRegExp(source.source, source.flags);
483
563
  case 'map':
484
- return deserializeMap(source.entries, generateCallback, getRemoteCallback);
564
+ return deserializeMap(source.entries, generateCallback, getRemoteCallback, depth);
485
565
  case 'set':
486
- return deserializeSet(source.values, generateCallback, getRemoteCallback);
566
+ return deserializeSet(source.values, generateCallback, getRemoteCallback, depth);
487
567
  case 'fn':
488
568
  return deserializeFunction(source, generateCallback, getRemoteCallback);
489
569
  case 'obj':
490
- return deserializeWrappedObject(source.entries, generateCallback, getRemoteCallback);
491
- default: {
492
- const result = {};
493
- for (const key of Object.keys(source)) {
494
- setOwnProperty(result, key, deserialize0(source[key], generateCallback, getRemoteCallback));
495
- }
496
- return result;
497
- }
570
+ return deserializeWrappedObject(source.entries, generateCallback, getRemoteCallback, depth);
571
+ default:
572
+ return deserializePlainObject(source, generateCallback, getRemoteCallback, depth);
498
573
  }
499
574
  }
500
575
  function deserializeBigInt(raw) {
@@ -525,25 +600,25 @@ function deserializeRegExp(rawSource, rawFlags) {
525
600
  throw new TypeError(`Invalid regexp payload: ${err instanceof Error ? err.message : String(err)}`);
526
601
  }
527
602
  }
528
- function deserializeMap(entries, generateCallback, getRemoteCallback) {
603
+ function deserializeMap(entries, generateCallback, getRemoteCallback, depth) {
529
604
  const result = new Map();
530
605
  if (!Array.isArray(entries))
531
606
  return result;
532
607
  for (const entry of entries) {
533
608
  if (!Array.isArray(entry) || entry.length !== 2)
534
609
  continue;
535
- const k = deserialize0(entry[0], generateCallback, getRemoteCallback);
536
- const v = deserialize0(entry[1], generateCallback, getRemoteCallback);
610
+ const k = deserialize0(entry[0], generateCallback, getRemoteCallback, depth + 1);
611
+ const v = deserialize0(entry[1], generateCallback, getRemoteCallback, depth + 1);
537
612
  result.set(k, v);
538
613
  }
539
614
  return result;
540
615
  }
541
- function deserializeSet(values, generateCallback, getRemoteCallback) {
616
+ function deserializeSet(values, generateCallback, getRemoteCallback, depth) {
542
617
  const result = new Set();
543
618
  if (!Array.isArray(values))
544
619
  return result;
545
620
  for (const v of values) {
546
- result.add(deserialize0(v, generateCallback, getRemoteCallback));
621
+ result.add(deserialize0(v, generateCallback, getRemoteCallback, depth + 1));
547
622
  }
548
623
  return result;
549
624
  }
@@ -561,23 +636,35 @@ function deserializeFunction(source, generateCallback, getRemoteCallback) {
561
636
  };
562
637
  return getRemoteCallback ? getRemoteCallback(id, invoke, fnOptions) : (...args) => invoke(args);
563
638
  }
564
- function deserializeError(source, generateCallback, getRemoteCallback) {
565
- const message = typeof source.message === 'string' ? source.message : '';
566
- const name = typeof source.name === 'string' ? source.name : 'Error';
567
- const Ctor = ERROR_CONSTRUCTORS[name] ?? Error;
568
- const error = new Ctor(message);
569
- if (name && name !== Ctor.name) {
639
+ function rebuildErrorFromSerialized(serialized) {
640
+ const name = typeof serialized.name === 'string' && serialized.name ? serialized.name : 'Error';
641
+ const factory = ERROR_FACTORIES[name] ?? ERROR_FACTORIES.Error;
642
+ const error = factory(serialized.message);
643
+ if (name && error.name !== name) {
570
644
  try {
571
645
  error.name = name;
572
646
  }
573
647
  catch {
574
- // ignore non-writable name
648
+ try {
649
+ Object.defineProperty(error, 'name', { value: name, configurable: true, writable: true });
650
+ }
651
+ catch (err) {
652
+ warn('rebuildErrorFromSerialized', 'failed to set error.name', name, err);
653
+ }
575
654
  }
576
655
  }
577
- if (typeof source.stack === 'string')
578
- error.stack = source.stack;
656
+ if (typeof serialized.stack === 'string')
657
+ error.stack = serialized.stack;
658
+ return error;
659
+ }
660
+ function deserializeError(source, generateCallback, getRemoteCallback, depth) {
661
+ const error = rebuildErrorFromSerialized({
662
+ message: typeof source.message === 'string' ? source.message : '',
663
+ name: typeof source.name === 'string' ? source.name : undefined,
664
+ stack: typeof source.stack === 'string' ? source.stack : undefined,
665
+ });
579
666
  if (source.cause !== undefined) {
580
- error.cause = deserialize0(source.cause, generateCallback, getRemoteCallback);
667
+ error.cause = deserialize0(source.cause, generateCallback, getRemoteCallback, depth + 1);
581
668
  }
582
669
  if (Array.isArray(source.extras)) {
583
670
  for (const entry of source.extras) {
@@ -587,16 +674,16 @@ function deserializeError(source, generateCallback, getRemoteCallback) {
587
674
  if (typeof rawKey !== 'string')
588
675
  continue;
589
676
  try {
590
- error[rawKey] = deserialize0(rawValue, generateCallback, getRemoteCallback);
677
+ setOwnProperty(error, rawKey, deserialize0(rawValue, generateCallback, getRemoteCallback, depth + 1));
591
678
  }
592
- catch {
593
- // ignore non-writable property
679
+ catch (err) {
680
+ warn('deserializeError', 'failed to set error property', rawKey, err);
594
681
  }
595
682
  }
596
683
  }
597
684
  return error;
598
685
  }
599
- function deserializeWrappedObject(entries, generateCallback, getRemoteCallback) {
686
+ function deserializeWrappedObject(entries, generateCallback, getRemoteCallback, depth) {
600
687
  const result = {};
601
688
  if (!Array.isArray(entries))
602
689
  return result;
@@ -604,18 +691,13 @@ function deserializeWrappedObject(entries, generateCallback, getRemoteCallback)
604
691
  if (!Array.isArray(entry) || entry.length !== 2)
605
692
  continue;
606
693
  const rawKey = entry[0];
607
- const value = deserialize0(entry[1], generateCallback, getRemoteCallback);
694
+ const value = deserialize0(entry[1], generateCallback, getRemoteCallback, depth + 1);
608
695
  const key = resolveObjectKey(rawKey);
609
696
  if (key === undefined) {
610
- console.warn('chrome-in-iframe: dropping wrapped-object entry with unresolvable key', rawKey);
697
+ warn('deserializeWrappedObject', 'dropping wrapped-object entry with unresolvable key', rawKey);
611
698
  continue;
612
699
  }
613
- Object.defineProperty(result, key, {
614
- value,
615
- enumerable: true,
616
- configurable: true,
617
- writable: true,
618
- });
700
+ setOwnProperty(result, key, value);
619
701
  }
620
702
  return result;
621
703
  }
@@ -628,7 +710,8 @@ function resolveObjectKey(raw) {
628
710
  try {
629
711
  return deserializeSymbol(tagged.v);
630
712
  }
631
- catch {
713
+ catch (err) {
714
+ warn('resolveObjectKey', 'failed to deserialize symbol key', tagged.v, err);
632
715
  return undefined;
633
716
  }
634
717
  }
@@ -652,39 +735,88 @@ function deserializeNumber(raw) {
652
735
  return Number(raw);
653
736
  }
654
737
  function deserializeSymbol(value) {
655
- if (value.startsWith('global:')) {
656
- return Symbol.for(value.slice('global:'.length));
657
- }
658
- if (value.startsWith('wellKnown:')) {
659
- const symbol = getWellKnownSymbol(value.slice('wellKnown:'.length));
660
- if (symbol)
661
- return symbol;
662
- }
663
- throw new TypeError('Cannot deserialize symbol');
664
- }
665
- function setOwnProperty(target, key, value) {
666
- Object.defineProperty(target, key, {
667
- value,
668
- enumerable: true,
669
- configurable: true,
670
- writable: true,
671
- });
738
+ return decodeSymbolToken(value);
672
739
  }
673
740
 
674
- function isListenerRegistrationPath(path) {
675
- const last = path[path.length - 1];
676
- return last === 'addListener' || last === 'hasListener';
741
+ function createResponder(ctx, targetInstanceId, responseType, id) {
742
+ const sender = () => ctx.getMessageChannel().getSender();
743
+ const sendError = (error) => {
744
+ sender().sendMessage(responseType, { id, error }, targetInstanceId);
745
+ };
746
+ return {
747
+ respondError(message) {
748
+ sendError({ message });
749
+ },
750
+ respondThrown(err) {
751
+ sendError(serializeThrownError(err));
752
+ },
753
+ respondSuccess(value) {
754
+ try {
755
+ sender().sendMessage(responseType, { id, data: ctx.serializeForRemote(value, targetInstanceId) }, targetInstanceId);
756
+ }
757
+ catch (err) {
758
+ sendError(serializeThrownError(err));
759
+ }
760
+ },
761
+ };
677
762
  }
678
- function isListenerRemovalPath(path) {
679
- const last = path[path.length - 1];
680
- return last === 'removeListener';
763
+ function createScopedRemoteCallback(ctx, remoteInstanceId) {
764
+ return (id, invoke, options) => ctx.getRemoteCallback(id, invoke, options, remoteInstanceId);
681
765
  }
682
- function isLikelyListenerPath(path) {
683
- const last = path[path.length - 1];
684
- return typeof last === 'string' && /^on[A-Z]/.test(last);
766
+ function createGenerateCallback(ctx, remoteInstanceId) {
767
+ return (id, args, options) => ctx.invokeFunctionById(id, args, options, remoteInstanceId);
768
+ }
769
+ function safeInvokeReject(callbacks, scope, warn) {
770
+ if (!callbacks.onReject)
771
+ return;
772
+ try {
773
+ callbacks.onReject();
774
+ }
775
+ catch (err) {
776
+ warn(scope, 'onReject hook threw', err);
777
+ }
778
+ }
779
+ function safeInvokeResolve(callbacks, scope, warn) {
780
+ if (!callbacks.onResolve)
781
+ return;
782
+ try {
783
+ callbacks.onResolve();
784
+ }
785
+ catch (err) {
786
+ warn(scope, 'onResolve hook threw', err);
787
+ }
788
+ }
789
+ function createResponseHandler(scope, warn) {
790
+ return function handleResponse(data, ctx, meta) {
791
+ const pending = ctx.getAndRemovePendingPromise(data.id, meta.senderInstanceId);
792
+ if (!pending)
793
+ return;
794
+ if (data.error) {
795
+ const err = rebuildErrorFromSerialized(data.error);
796
+ safeInvokeReject(pending, scope, warn);
797
+ pending.reject(err);
798
+ return;
799
+ }
800
+ try {
801
+ safeInvokeResolve(pending, scope, warn);
802
+ pending.resolve(deserialize(data.data, createGenerateCallback(ctx, meta.senderInstanceId), createScopedRemoteCallback(ctx, meta.senderInstanceId)));
803
+ }
804
+ catch (err) {
805
+ safeInvokeReject(pending, scope, warn);
806
+ pending.reject(err instanceof Error ? err : new Error(String(err)));
807
+ }
808
+ };
685
809
  }
686
810
 
687
- const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
811
+ const DANGEROUS_KEYS = new Set([
812
+ '__proto__',
813
+ 'constructor',
814
+ 'prototype',
815
+ '__defineGetter__',
816
+ '__defineSetter__',
817
+ '__lookupGetter__',
818
+ '__lookupSetter__',
819
+ ]);
688
820
  function readProperty(target, key) {
689
821
  if (typeof key === 'string' && DANGEROUS_KEYS.has(key)) {
690
822
  throw new Error(`Access to dangerous property '${key}' is denied`);
@@ -694,135 +826,89 @@ function readProperty(target, key) {
694
826
  return target[key];
695
827
  }
696
828
 
697
- const boundCache = new WeakMap();
698
- function bindMethod(fn, owner) {
699
- if (owner === null || typeof owner !== 'object') {
700
- return (...args) => Reflect.apply(fn, owner, args);
701
- }
702
- let perOwner = boundCache.get(fn);
703
- if (!perOwner) {
704
- perOwner = new WeakMap();
705
- boundCache.set(fn, perOwner);
706
- }
707
- const cached = perOwner.get(owner);
708
- if (cached)
709
- return cached;
710
- const bound = (...args) => Reflect.apply(fn, owner, args);
711
- perOwner.set(owner, bound);
712
- return bound;
713
- }
714
829
  function handleAccessPropertyRequest(data, ctx, meta) {
830
+ if (ctx.isDestroyed())
831
+ return;
715
832
  const target = ctx.getDelegateTarget();
716
- const channel = ctx.getMessageChannel();
833
+ const { respondError, respondThrown } = createResponder(ctx, meta.senderInstanceId, 'accessPropertyResponse', data.id);
717
834
  if (data.path.length === 0) {
718
- channel.getSender().sendMessage('accessPropertyResponse', {
719
- id: data.id,
720
- error: { message: 'Property path must not be empty' },
721
- }, meta.senderInstanceId);
835
+ respondError('Property path must not be empty');
722
836
  return;
723
837
  }
724
- if (!target) {
725
- channel.getSender().sendMessage('accessPropertyResponse', {
726
- id: data.id,
727
- error: { message: 'No delegate target is configured' },
728
- }, meta.senderInstanceId);
838
+ if (target === undefined || target === null) {
839
+ respondError('No delegate target is configured');
729
840
  return;
730
841
  }
731
842
  try {
732
843
  let current = target;
733
844
  let owner = target;
734
- for (const key of data.path) {
845
+ for (let i = 0; i < data.path.length; i++) {
846
+ const key = data.path[i];
735
847
  if (current === undefined || current === null) {
736
- channel.getSender().sendMessage('accessPropertyResponse', {
737
- id: data.id,
738
- data: serialize(current, ctx.registerFunction),
739
- }, meta.senderInstanceId);
848
+ respondError(`Cannot read property '${String(key)}' of ${current === null ? 'null' : 'undefined'} (at '${data.path
849
+ .slice(0, i)
850
+ .map(String)
851
+ .join('.')}')`);
740
852
  return;
741
853
  }
742
854
  owner = current;
743
855
  current = readProperty(current, key);
744
856
  }
745
- const value = typeof current === 'function' ? bindMethod(current, owner) : current;
746
- channel.getSender().sendMessage('accessPropertyResponse', {
747
- id: data.id,
748
- data: serialize(value, ctx.registerFunction, { persistent: isLikelyListenerPath(data.path) }),
749
- }, meta.senderInstanceId);
750
- }
751
- catch (err) {
752
- channel.getSender().sendMessage('accessPropertyResponse', {
857
+ const value = typeof current === 'function' ? ctx.bindMethod(current, owner) : current;
858
+ ctx
859
+ .getMessageChannel()
860
+ .getSender()
861
+ .sendMessage('accessPropertyResponse', {
753
862
  id: data.id,
754
- error: serializeThrownError(err),
863
+ data: ctx.serializeForRemote(value, meta.senderInstanceId, {
864
+ persistent: isLikelyListenerPath(data.path),
865
+ }),
755
866
  }, meta.senderInstanceId);
756
867
  }
757
- }
758
- function handleAccessPropertyResponse(data, ctx, meta) {
759
- const pending = ctx.getAndRemovePendingPromise(data.id);
760
- if (!pending)
761
- return;
762
- if (data.error) {
763
- const err = new Error(data.error.message);
764
- err.stack = data.error.stack;
765
- pending.reject(err);
766
- return;
767
- }
768
- const scopedRemoteCallback = createScopedRemoteCallback$2(ctx, meta.senderInstanceId);
769
- try {
770
- pending.resolve(deserialize(data.data, (id, args, options) => {
771
- return ctx.invokeFunctionById(id, args, options);
772
- }, scopedRemoteCallback));
773
- }
774
868
  catch (err) {
775
- pending.reject(err instanceof Error ? err : new Error(String(err)));
869
+ respondThrown(err);
776
870
  }
777
871
  }
778
- function createScopedRemoteCallback$2(ctx, remoteInstanceId) {
779
- return (id, invoke, options) => ctx.getRemoteCallback(id, invoke, options, remoteInstanceId);
780
- }
872
+ const handleAccessPropertyResponse = createResponseHandler('handleAccessPropertyResponse', warn);
781
873
 
782
874
  function handleInvokeRequest(data, ctx, meta) {
875
+ if (ctx.isDestroyed())
876
+ return;
783
877
  const target = ctx.getDelegateTarget();
784
- const channel = ctx.getMessageChannel();
785
- const scopedRemoteCallback = createScopedRemoteCallback$1(ctx, meta.senderInstanceId);
786
- if (!target) {
787
- channel.getSender().sendMessage('invokeResponse', {
788
- id: data.id,
789
- error: { message: 'No delegate target is configured' },
790
- }, meta.senderInstanceId);
878
+ const scopedRemoteCallback = createScopedRemoteCallback(ctx, meta.senderInstanceId);
879
+ const generateCallback = createGenerateCallback(ctx, meta.senderInstanceId);
880
+ const { respondError, respondThrown, respondSuccess } = createResponder(ctx, meta.senderInstanceId, 'invokeResponse', data.id);
881
+ if (target === undefined || target === null) {
882
+ respondError('No delegate target is configured');
883
+ return;
884
+ }
885
+ if (data.path.length === 0) {
886
+ respondError('Invocation path must not be empty');
791
887
  return;
792
888
  }
793
889
  let current = target;
794
890
  for (let i = 0; i < data.path.length - 1; i++) {
795
891
  current = readProperty(current, data.path[i]);
796
892
  if (current === undefined || current === null) {
797
- channel.getSender().sendMessage('invokeResponse', {
798
- id: data.id,
799
- error: {
800
- message: `Cannot read property '${String(data.path[i + 1])}' of ${String(data.path[i])}`,
801
- },
802
- }, meta.senderInstanceId);
893
+ respondError(`Cannot read property '${String(data.path[i + 1])}' of ${current === null ? 'null' : 'undefined'} (at '${data.path
894
+ .slice(0, i + 1)
895
+ .map(String)
896
+ .join('.')}')`);
803
897
  return;
804
898
  }
805
899
  }
806
900
  const lastKey = data.path[data.path.length - 1];
807
901
  const fn = readProperty(current, lastKey);
808
902
  if (typeof fn !== 'function') {
809
- channel.getSender().sendMessage('invokeResponse', {
810
- id: data.id,
811
- error: { message: `'${String(lastKey)}' is not a function` },
812
- }, meta.senderInstanceId);
903
+ respondError(`'${String(lastKey)}' is not a function`);
813
904
  return;
814
905
  }
815
906
  let deserializedArgs;
816
907
  try {
817
- deserializedArgs = data.args.map((arg) => deserialize(arg, (id, args, options) => {
818
- return ctx.invokeFunctionById(id, args, options);
819
- }, scopedRemoteCallback));
908
+ deserializedArgs = data.args.map((arg) => deserialize(arg, generateCallback, scopedRemoteCallback));
820
909
  }
821
910
  catch (err) {
822
- channel.getSender().sendMessage('invokeResponse', {
823
- id: data.id,
824
- error: serializeThrownError(err),
825
- }, meta.senderInstanceId);
911
+ respondThrown(err);
826
912
  return;
827
913
  }
828
914
  let result;
@@ -830,165 +916,98 @@ function handleInvokeRequest(data, ctx, meta) {
830
916
  result = Reflect.apply(fn, current, deserializedArgs);
831
917
  }
832
918
  catch (err) {
833
- channel.getSender().sendMessage('invokeResponse', {
834
- id: data.id,
835
- error: serializeThrownError(err),
836
- }, meta.senderInstanceId);
919
+ respondThrown(err);
837
920
  return;
838
921
  }
839
- if (result instanceof Promise) {
840
- result
922
+ if (isPromiseLike(result)) {
923
+ Promise.resolve(result)
841
924
  .then((value) => {
842
- sendInvokeSuccess(data.id, value, ctx, meta.senderInstanceId);
925
+ respondSuccess(value);
843
926
  })
844
927
  .catch((err) => {
845
- channel.getSender().sendMessage('invokeResponse', {
846
- id: data.id,
847
- error: serializeThrownError(err),
848
- }, meta.senderInstanceId);
928
+ respondThrown(err);
849
929
  });
850
- }
851
- else {
852
- sendInvokeSuccess(data.id, result, ctx, meta.senderInstanceId);
853
- }
854
- }
855
- function handleInvokeResponse(data, ctx, meta) {
856
- const pending = ctx.getAndRemovePendingPromise(data.id);
857
- if (!pending)
858
930
  return;
859
- if (data.error) {
860
- const err = new Error(data.error.message);
861
- err.stack = data.error.stack;
862
- pending.reject(err);
863
- }
864
- else {
865
- const scopedRemoteCallback = createScopedRemoteCallback$1(ctx, meta.senderInstanceId);
866
- try {
867
- pending.resolve(deserialize(data.data, (id, args, options) => {
868
- return ctx.invokeFunctionById(id, args, options);
869
- }, scopedRemoteCallback));
870
- }
871
- catch (err) {
872
- pending.reject(err instanceof Error ? err : new Error(String(err)));
873
- }
874
- }
875
- }
876
- function sendInvokeSuccess(id, value, ctx, targetInstanceId) {
877
- const channel = ctx.getMessageChannel();
878
- try {
879
- channel.getSender().sendMessage('invokeResponse', {
880
- id,
881
- data: serialize(value, ctx.registerFunction),
882
- }, targetInstanceId);
883
- }
884
- catch (err) {
885
- channel.getSender().sendMessage('invokeResponse', {
886
- id,
887
- error: serializeThrownError(err),
888
- }, targetInstanceId);
889
931
  }
932
+ respondSuccess(result);
890
933
  }
891
- function createScopedRemoteCallback$1(ctx, remoteInstanceId) {
892
- return (id, invoke, options) => ctx.getRemoteCallback(id, invoke, options, remoteInstanceId);
893
- }
934
+ const handleInvokeResponse = createResponseHandler('handleInvokeResponse', warn);
894
935
 
895
936
  function handleCallbackInvoke(data, ctx, meta) {
896
- const channel = ctx.getMessageChannel();
937
+ if (ctx.isDestroyed())
938
+ return;
939
+ const { respondError, respondThrown, respondSuccess } = createResponder(ctx, meta.senderInstanceId, 'invokeFunctionByIdResponse', data.callId);
897
940
  const persistentEntry = ctx.getPersistentFunctionCache().get(data.id);
898
941
  const entry = persistentEntry ?? ctx.getFunctionCache().get(data.id);
899
942
  if (!entry) {
900
- const error = { message: `Callback '${data.id}' is not available or has expired` };
901
- console.warn(`chrome-in-iframe: ${error.message}`);
902
- channel.getSender().sendMessage('invokeFunctionByIdResponse', {
903
- id: data.callId,
904
- error,
905
- }, meta.senderInstanceId);
943
+ const message = `Callback '${data.id}' is not available or has expired`;
944
+ warn('handleCallbackInvoke', message);
945
+ respondError(message);
906
946
  return;
907
947
  }
908
948
  const scopedRemoteCallback = createScopedRemoteCallback(ctx, meta.senderInstanceId);
949
+ const generateCallback = createGenerateCallback(ctx, meta.senderInstanceId);
909
950
  let deserializedArgs;
910
951
  try {
911
- deserializedArgs = data.args.map((arg) => deserialize(arg, (id, args, options) => {
912
- return ctx.invokeFunctionById(id, args, options);
913
- }, scopedRemoteCallback));
952
+ deserializedArgs = data.args.map((arg) => deserialize(arg, generateCallback, scopedRemoteCallback));
914
953
  }
915
954
  catch (err) {
916
- sendCallbackError(data.callId, err, ctx, meta.senderInstanceId);
955
+ respondThrown(err);
917
956
  return;
918
957
  }
919
958
  try {
920
959
  const result = Reflect.apply(entry.fn, entry.thisArg, deserializedArgs);
921
- if (result instanceof Promise) {
922
- result
960
+ if (isPromiseLike(result)) {
961
+ Promise.resolve(result)
923
962
  .then((value) => {
924
- sendCallbackSuccess(data.callId, value, ctx, meta.senderInstanceId);
963
+ respondSuccess(value);
925
964
  })
926
965
  .catch((err) => {
927
- sendCallbackError(data.callId, err, ctx, meta.senderInstanceId);
966
+ respondThrown(err);
928
967
  });
929
968
  return;
930
969
  }
931
- sendCallbackSuccess(data.callId, result, ctx, meta.senderInstanceId);
970
+ respondSuccess(result);
932
971
  }
933
972
  catch (err) {
934
- sendCallbackError(data.callId, err, ctx, meta.senderInstanceId);
973
+ respondThrown(err);
935
974
  }
936
975
  }
937
- function handleCallbackInvokeResponse(data, ctx, meta) {
938
- const pending = ctx.getAndRemovePendingPromise(data.id);
939
- if (!pending)
940
- return;
941
- if (data.error) {
942
- const err = new Error(data.error.message);
943
- err.stack = data.error.stack;
944
- pending.reject(err);
945
- return;
946
- }
947
- const scopedRemoteCallback = createScopedRemoteCallback(ctx, meta.senderInstanceId);
948
- try {
949
- pending.resolve(deserialize(data.data, (id, args, options) => {
950
- return ctx.invokeFunctionById(id, args, options);
951
- }, scopedRemoteCallback));
952
- }
953
- catch (err) {
954
- pending.reject(err instanceof Error ? err : new Error(String(err)));
955
- }
956
- }
957
- function sendCallbackSuccess(id, value, ctx, targetInstanceId) {
958
- const channel = ctx.getMessageChannel();
959
- try {
960
- channel.getSender().sendMessage('invokeFunctionByIdResponse', {
961
- id,
962
- data: serialize(value, ctx.registerFunction),
963
- }, targetInstanceId);
964
- }
965
- catch (err) {
966
- sendCallbackError(id, err, ctx, targetInstanceId);
967
- }
968
- }
969
- function sendCallbackError(id, err, ctx, targetInstanceId) {
970
- ctx
971
- .getMessageChannel()
972
- .getSender()
973
- .sendMessage('invokeFunctionByIdResponse', {
974
- id,
975
- error: serializeThrownError(err),
976
- }, targetInstanceId);
977
- }
978
- function createScopedRemoteCallback(ctx, remoteInstanceId) {
979
- return (id, invoke, options) => ctx.getRemoteCallback(id, invoke, options, remoteInstanceId);
980
- }
976
+ const handleCallbackInvokeResponse = createResponseHandler('handleCallbackInvokeResponse', warn);
981
977
 
982
- const MAX_RELEASE_BATCH = 10000;
983
- function handleReleaseCallbacks(data, ctx, _meta) {
984
- if (data.ids.length > MAX_RELEASE_BATCH)
985
- return;
986
- for (const id of data.ids) {
987
- ctx.getFunctionCache().delete(id);
988
- ctx.getPersistentFunctionCache().delete(id);
989
- }
978
+ const RELEASE_CHUNK_SIZE = 1000;
979
+ const scheduleNextChunk = typeof queueMicrotask === 'function' ? (cb) => queueMicrotask(cb) : (cb) => setTimeout(cb, 0);
980
+ function handleConnectRequest(_data, ctx, meta) {
981
+ ctx.noteFreshConnect(meta.senderInstanceId);
982
+ ctx.getMessageChannel().getSender().sendMessage('connectResponse', {
983
+ instanceId: ctx.getInstanceId(),
984
+ }, meta.senderInstanceId);
985
+ }
986
+ function handleConnectResponse(_data, ctx, meta) {
987
+ ctx.bindRemote(meta.senderInstanceId);
988
+ }
989
+ function handleReleaseCallbacks(data, ctx, meta) {
990
+ const ids = data.ids;
991
+ const total = ids.length;
992
+ ctx.releaseRemoteCallbacks(meta.senderInstanceId, ids);
993
+ let cursor = 0;
994
+ const sender = meta.senderInstanceId;
995
+ const processChunk = () => {
996
+ if (ctx.isDestroyed())
997
+ return;
998
+ const end = Math.min(cursor + RELEASE_CHUNK_SIZE, total);
999
+ for (let i = cursor; i < end; i++) {
1000
+ ctx.dropLocalCallback(ids[i], sender);
1001
+ }
1002
+ cursor = end;
1003
+ if (cursor < total)
1004
+ scheduleNextChunk(processChunk);
1005
+ };
1006
+ processChunk();
990
1007
  }
991
- function handleDestroyEndpoint(_data, ctx, meta) {
1008
+ function handleDestroyEndpoint(data, ctx, meta) {
1009
+ if (data.instanceId !== meta.senderInstanceId)
1010
+ return;
992
1011
  ctx.handleRemoteDestroy(meta.senderInstanceId);
993
1012
  }
994
1013
 
@@ -1009,24 +1028,130 @@ const DEFAULT_FUNCTION_CACHE_MAX = 100;
1009
1028
  const DEFAULT_FUNCTION_CACHE_TTL = 5 * 60 * 1000;
1010
1029
  const DEFAULT_REMOTE_CALLBACK_CACHE_MAX = 500;
1011
1030
  const DEFAULT_REMOTE_CALLBACK_CACHE_TTL = 5 * 60 * 1000;
1031
+ const RELEASE_CALLBACKS_CHUNK_SIZE = 10000;
1032
+ const BROADCAST_OWNER = Symbol('chrome-in-iframe.broadcast-owner');
1033
+ function resolveTimeout(raw) {
1034
+ if (raw === undefined)
1035
+ return DEFAULT_TIMEOUT;
1036
+ if (typeof raw !== 'number')
1037
+ return DEFAULT_TIMEOUT;
1038
+ if (raw === Infinity)
1039
+ return Infinity;
1040
+ if (!Number.isFinite(raw) || raw <= 0)
1041
+ return DEFAULT_TIMEOUT;
1042
+ return raw;
1043
+ }
1012
1044
  function createClientContext(getMessageChannel, instanceId, options = {}) {
1013
1045
  const remoteCacheMax = options.remoteCallbackCacheMax ?? DEFAULT_REMOTE_CALLBACK_CACHE_MAX;
1014
1046
  const remoteCacheTtl = options.remoteCallbackCacheTtl ?? DEFAULT_REMOTE_CALLBACK_CACHE_TTL;
1047
+ const callTimeout = resolveTimeout(options.timeout);
1015
1048
  const pending = new Map();
1016
- const functionIds = new Map();
1049
+ const functionIds = new WeakMap();
1050
+ const persistentFunctionCache = new Map();
1051
+ const persistentRefcount = new Map();
1017
1052
  const functionCache = new LRUCache({
1018
1053
  max: options.functionCacheMax ?? DEFAULT_FUNCTION_CACHE_MAX,
1019
1054
  ttl: options.functionCacheTtl ?? DEFAULT_FUNCTION_CACHE_TTL,
1020
- dispose: (value, _key, reason) => {
1055
+ dispose: (value, key, reason) => {
1021
1056
  if (reason === 'set' || reason === 'delete')
1022
1057
  return;
1023
- functionIds.delete(value.fn);
1058
+ if (persistentFunctionCache.has(key))
1059
+ return;
1060
+ removeFunctionIdMapping(value.fn, value.thisArg);
1024
1061
  },
1025
1062
  });
1026
- const persistentFunctionCache = new Map();
1027
1063
  const remoteCallbacksByOwner = new Map();
1028
1064
  const persistentRemoteCallbacksByOwner = new Map();
1065
+ const boundMethodCache = new WeakMap();
1066
+ const callbackOwners = new Map();
1067
+ const knownRemotes = new Set();
1068
+ const remoteTargetWaiters = new Set();
1069
+ let boundRemoteInstanceId;
1070
+ let remoteTargetWaitDisabled = false;
1029
1071
  let destroyed = false;
1072
+ const armTimer = (onTimeout) => {
1073
+ if (callTimeout === Infinity)
1074
+ return null;
1075
+ return setTimeout(onTimeout, callTimeout);
1076
+ };
1077
+ const cancelTimer = (timer) => {
1078
+ if (timer !== null)
1079
+ clearTimeout(timer);
1080
+ };
1081
+ const getPreferredRemote = () => {
1082
+ if (boundRemoteInstanceId !== undefined)
1083
+ return boundRemoteInstanceId;
1084
+ if (knownRemotes.size !== 1)
1085
+ return undefined;
1086
+ return knownRemotes.values().next().value;
1087
+ };
1088
+ const resolveRemoteTargetWaiters = (remoteInstanceId) => {
1089
+ if (remoteTargetWaiters.size === 0)
1090
+ return;
1091
+ const waiters = Array.from(remoteTargetWaiters);
1092
+ remoteTargetWaiters.clear();
1093
+ for (const resolve of waiters)
1094
+ resolve(remoteInstanceId);
1095
+ };
1096
+ const waitForRemoteTarget = () => {
1097
+ const preferred = getPreferredRemote();
1098
+ if (preferred !== undefined)
1099
+ return Promise.resolve(preferred);
1100
+ if (destroyed)
1101
+ return Promise.resolve(undefined);
1102
+ return new Promise((resolve) => {
1103
+ remoteTargetWaiters.add(resolve);
1104
+ });
1105
+ };
1106
+ const recordCallbackOwners = (ids, owner) => {
1107
+ if (ids.length === 0)
1108
+ return;
1109
+ if (owner === 'broadcast') {
1110
+ for (const id of ids)
1111
+ callbackOwners.set(id, BROADCAST_OWNER);
1112
+ return;
1113
+ }
1114
+ for (const id of ids) {
1115
+ const cur = callbackOwners.get(id);
1116
+ if (cur === BROADCAST_OWNER)
1117
+ continue;
1118
+ if (cur === undefined) {
1119
+ callbackOwners.set(id, owner);
1120
+ continue;
1121
+ }
1122
+ if (typeof cur === 'string') {
1123
+ if (cur === owner)
1124
+ continue;
1125
+ callbackOwners.set(id, new Set([cur, owner]));
1126
+ continue;
1127
+ }
1128
+ cur.add(owner);
1129
+ }
1130
+ };
1131
+ const bindMethod = (fn, owner) => {
1132
+ if (owner === null || typeof owner !== 'object') {
1133
+ return (...args) => Reflect.apply(fn, owner, args);
1134
+ }
1135
+ let perOwner = boundMethodCache.get(fn);
1136
+ if (!perOwner) {
1137
+ perOwner = new WeakMap();
1138
+ boundMethodCache.set(fn, perOwner);
1139
+ }
1140
+ const cached = perOwner.get(owner);
1141
+ if (cached)
1142
+ return cached;
1143
+ const bound = (...args) => Reflect.apply(fn, owner, args);
1144
+ perOwner.set(owner, bound);
1145
+ return bound;
1146
+ };
1147
+ const removeFunctionIdMapping = (fn, thisArg) => {
1148
+ const perOwner = functionIds.get(fn);
1149
+ if (!perOwner)
1150
+ return;
1151
+ perOwner.delete(thisArg);
1152
+ if (perOwner.size === 0)
1153
+ functionIds.delete(fn);
1154
+ };
1030
1155
  const getOrCreateRemoteLru = (remoteId) => {
1031
1156
  let lru = remoteCallbacksByOwner.get(remoteId);
1032
1157
  if (!lru) {
@@ -1044,21 +1169,88 @@ function createClientContext(getMessageChannel, instanceId, options = {}) {
1044
1169
  return map;
1045
1170
  };
1046
1171
  const registerFunction = (fn, thisArg, callbackOptions = {}) => {
1047
- let id = functionIds.get(fn);
1172
+ let perOwner = functionIds.get(fn);
1173
+ if (!perOwner) {
1174
+ perOwner = new Map();
1175
+ functionIds.set(fn, perOwner);
1176
+ }
1177
+ let id = perOwner.get(thisArg);
1048
1178
  if (!id) {
1049
1179
  id = createId('cb');
1050
- functionIds.set(fn, id);
1180
+ perOwner.set(thisArg, id);
1051
1181
  }
1052
1182
  const entry = { fn, thisArg };
1053
1183
  if (callbackOptions.persistent) {
1054
1184
  persistentFunctionCache.set(id, entry);
1055
1185
  functionCache.delete(id);
1186
+ persistentRefcount.set(id, (persistentRefcount.get(id) ?? 0) + 1);
1056
1187
  }
1057
1188
  else if (!persistentFunctionCache.has(id)) {
1058
1189
  functionCache.set(id, entry);
1059
1190
  }
1060
1191
  return id;
1061
1192
  };
1193
+ const releasePersistentRegistrationById = (id) => {
1194
+ if (!persistentRefcount.has(id))
1195
+ return undefined;
1196
+ const next = (persistentRefcount.get(id) ?? 0) - 1;
1197
+ if (next > 0) {
1198
+ persistentRefcount.set(id, next);
1199
+ return undefined;
1200
+ }
1201
+ persistentRefcount.delete(id);
1202
+ const entry = persistentFunctionCache.get(id);
1203
+ persistentFunctionCache.delete(id);
1204
+ functionCache.delete(id);
1205
+ if (entry)
1206
+ removeFunctionIdMapping(entry.fn, entry.thisArg);
1207
+ return id;
1208
+ };
1209
+ const dropLocalCallback = (id, requesterInstanceId) => {
1210
+ if (requesterInstanceId !== undefined) {
1211
+ const cur = callbackOwners.get(id);
1212
+ if (cur === BROADCAST_OWNER) {
1213
+ const remaining = new Set();
1214
+ for (const r of knownRemotes) {
1215
+ if (r !== requesterInstanceId)
1216
+ remaining.add(r);
1217
+ }
1218
+ if (remaining.size > 0) {
1219
+ callbackOwners.set(id, remaining.size === 1 ? remaining.values().next().value : remaining);
1220
+ return;
1221
+ }
1222
+ }
1223
+ else if (typeof cur === 'string') {
1224
+ if (cur !== requesterInstanceId)
1225
+ return;
1226
+ }
1227
+ else if (cur instanceof Set) {
1228
+ if (!cur.has(requesterInstanceId))
1229
+ return;
1230
+ cur.delete(requesterInstanceId);
1231
+ if (cur.size > 1)
1232
+ return;
1233
+ if (cur.size === 1) {
1234
+ callbackOwners.set(id, cur.values().next().value);
1235
+ return;
1236
+ }
1237
+ }
1238
+ }
1239
+ if (persistentRefcount.has(id)) {
1240
+ const next = (persistentRefcount.get(id) ?? 0) - 1;
1241
+ if (next > 0) {
1242
+ persistentRefcount.set(id, next);
1243
+ return;
1244
+ }
1245
+ }
1246
+ callbackOwners.delete(id);
1247
+ const entry = persistentFunctionCache.get(id) ?? functionCache.get(id);
1248
+ persistentRefcount.delete(id);
1249
+ persistentFunctionCache.delete(id);
1250
+ functionCache.delete(id);
1251
+ if (entry)
1252
+ removeFunctionIdMapping(entry.fn, entry.thisArg);
1253
+ };
1062
1254
  const getRemoteCallback = (id, invoke, callbackOptions, remoteInstanceId) => {
1063
1255
  const opts = callbackOptions ?? {};
1064
1256
  const lru = remoteCallbacksByOwner.get(remoteInstanceId);
@@ -1083,12 +1275,34 @@ function createClientContext(getMessageChannel, instanceId, options = {}) {
1083
1275
  if (ids.length === 0)
1084
1276
  return;
1085
1277
  try {
1086
- getMessageChannel().getSender().sendMessage('releaseCallbacks', { ids });
1278
+ const sender = getMessageChannel().getSender();
1279
+ for (let cursor = 0; cursor < ids.length; cursor += RELEASE_CALLBACKS_CHUNK_SIZE) {
1280
+ sender.sendMessage('releaseCallbacks', { ids: ids.slice(cursor, cursor + RELEASE_CALLBACKS_CHUNK_SIZE) });
1281
+ }
1087
1282
  }
1088
- catch {
1089
- // channel may not be available yet
1283
+ catch (err) {
1284
+ warn('notifyReleaseCallbacks', 'failed to release callbacks', err);
1090
1285
  }
1091
1286
  };
1287
+ const safeOnReject = (callbacks) => {
1288
+ if (!callbacks.onReject)
1289
+ return;
1290
+ try {
1291
+ callbacks.onReject();
1292
+ }
1293
+ catch (err) {
1294
+ warn('rejectPending', 'onReject hook threw', err);
1295
+ }
1296
+ };
1297
+ const rejectPending = (id, error) => {
1298
+ const callbacks = pending.get(id);
1299
+ if (!callbacks)
1300
+ return;
1301
+ cancelTimer(callbacks.timer);
1302
+ pending.delete(id);
1303
+ safeOnReject(callbacks);
1304
+ callbacks.reject(error);
1305
+ };
1092
1306
  return {
1093
1307
  getDelegateTarget() {
1094
1308
  return options.delegateTarget;
@@ -1108,20 +1322,33 @@ function createClientContext(getMessageChannel, instanceId, options = {}) {
1108
1322
  registerFunction(fn, thisArg, callbackOptions) {
1109
1323
  return registerFunction(fn, thisArg, callbackOptions);
1110
1324
  },
1325
+ serializeForRemote(value, target, callbackOptions) {
1326
+ const ids = [];
1327
+ const tracking = (fn, thisArg, opts) => {
1328
+ const id = registerFunction(fn, thisArg, opts);
1329
+ ids.push(id);
1330
+ return id;
1331
+ };
1332
+ const result = serialize(value, tracking, callbackOptions);
1333
+ recordCallbackOwners(ids, target);
1334
+ return result;
1335
+ },
1111
1336
  getRemoteCallback(id, invoke, callbackOptions, remoteInstanceId) {
1112
1337
  return getRemoteCallback(id, invoke, callbackOptions, remoteInstanceId);
1113
1338
  },
1114
- getAndRemovePendingPromise(id) {
1339
+ getAndRemovePendingPromise(id, senderInstanceId) {
1115
1340
  const callbacks = pending.get(id);
1116
- if (callbacks) {
1117
- clearTimeout(callbacks.timer);
1118
- pending.delete(id);
1341
+ if (!callbacks)
1342
+ return undefined;
1343
+ if (callbacks.expectedRemote !== undefined &&
1344
+ senderInstanceId !== undefined &&
1345
+ callbacks.expectedRemote !== senderInstanceId) {
1346
+ return undefined;
1119
1347
  }
1348
+ cancelTimer(callbacks.timer);
1349
+ pending.delete(id);
1120
1350
  return callbacks;
1121
1351
  },
1122
- registerPendingPromise(id, callbacks) {
1123
- pending.set(id, callbacks);
1124
- },
1125
1352
  invoke(path, args) {
1126
1353
  return new Promise((resolve, reject) => {
1127
1354
  if (destroyed) {
@@ -1129,35 +1356,114 @@ function createClientContext(getMessageChannel, instanceId, options = {}) {
1129
1356
  return;
1130
1357
  }
1131
1358
  const id = createId('req');
1132
- const timeout = options.timeout ?? DEFAULT_TIMEOUT;
1133
- const timer = setTimeout(() => {
1134
- pending.delete(id);
1135
- reject(new Error(`Call timed out: ${String(path.join('.'))}`));
1136
- }, timeout);
1137
- pending.set(id, { resolve, reject, timer });
1138
- const channel = getMessageChannel();
1139
1359
  const persistent = isListenerRegistrationPath(path);
1140
- const serializedArgs = args.map((arg) => serialize(arg, registerFunction, { persistent }));
1141
- if (isListenerRemovalPath(path)) {
1142
- const releasedIds = [];
1143
- for (const original of args) {
1144
- if (typeof original !== 'function')
1145
- continue;
1146
- const fnId = functionIds.get(original);
1147
- if (fnId) {
1148
- releasedIds.push(fnId);
1149
- functionCache.delete(fnId);
1150
- persistentFunctionCache.delete(fnId);
1151
- functionIds.delete(original);
1360
+ const registeredIds = [];
1361
+ const collectingRegister = (fn, thisArg, opts) => {
1362
+ const cbId = registerFunction(fn, thisArg, opts);
1363
+ registeredIds.push(cbId);
1364
+ return cbId;
1365
+ };
1366
+ const releaseRegisteredPersistents = () => {
1367
+ const released = [];
1368
+ for (const cbId of registeredIds) {
1369
+ const releasedId = releasePersistentRegistrationById(cbId);
1370
+ if (releasedId)
1371
+ released.push(releasedId);
1372
+ }
1373
+ return released;
1374
+ };
1375
+ const cleanupRegisteredOnFailure = () => {
1376
+ const released = [];
1377
+ for (const cbId of registeredIds) {
1378
+ if (persistentRefcount.has(cbId)) {
1379
+ const releasedId = releasePersistentRegistrationById(cbId);
1380
+ if (releasedId)
1381
+ released.push(releasedId);
1382
+ }
1383
+ else {
1384
+ const entry = functionCache.get(cbId);
1385
+ if (entry) {
1386
+ functionCache.delete(cbId);
1387
+ removeFunctionIdMapping(entry.fn, entry.thisArg);
1388
+ }
1152
1389
  }
1390
+ callbackOwners.delete(cbId);
1153
1391
  }
1154
- notifyReleaseCallbacks(releasedIds);
1392
+ return released;
1393
+ };
1394
+ const timer = armTimer(() => {
1395
+ rejectPending(id, new Error(`Call timed out: ${String(path.join('.'))}`));
1396
+ });
1397
+ let serializedArgs;
1398
+ try {
1399
+ serializePath(path);
1400
+ serializedArgs = args.map((arg) => serialize(arg, collectingRegister, { persistent }));
1401
+ }
1402
+ catch (err) {
1403
+ cancelTimer(timer);
1404
+ const released = cleanupRegisteredOnFailure();
1405
+ if (released.length > 0)
1406
+ notifyReleaseCallbacks(released);
1407
+ reject(err);
1408
+ return;
1155
1409
  }
1156
- channel.getSender().sendMessage('invokeRequest', {
1157
- id,
1158
- path,
1159
- args: serializedArgs,
1410
+ pending.set(id, {
1411
+ resolve,
1412
+ reject,
1413
+ timer,
1414
+ onResolve: () => {
1415
+ if (isListenerRemovalPath(path)) {
1416
+ const released = releaseRegisteredPersistents();
1417
+ if (released.length > 0)
1418
+ notifyReleaseCallbacks(released);
1419
+ }
1420
+ },
1421
+ onReject: () => {
1422
+ if (isListenerRegistrationPath(path)) {
1423
+ const released = releaseRegisteredPersistents();
1424
+ if (released.length > 0)
1425
+ notifyReleaseCallbacks(released);
1426
+ }
1427
+ },
1160
1428
  });
1429
+ const sendRequest = (targetInstanceId, requireTarget = true) => {
1430
+ const callbacks = pending.get(id);
1431
+ if (!callbacks)
1432
+ return;
1433
+ if (targetInstanceId === undefined && requireTarget) {
1434
+ rejectPending(id, new Error('No remote endpoint is available'));
1435
+ return;
1436
+ }
1437
+ if (targetInstanceId !== undefined)
1438
+ callbacks.expectedRemote = targetInstanceId;
1439
+ try {
1440
+ const channel = getMessageChannel();
1441
+ recordCallbackOwners(registeredIds, targetInstanceId ?? 'broadcast');
1442
+ channel.getSender().sendMessage('invokeRequest', {
1443
+ id,
1444
+ path,
1445
+ args: serializedArgs,
1446
+ }, targetInstanceId);
1447
+ }
1448
+ catch (err) {
1449
+ cancelTimer(timer);
1450
+ pending.delete(id);
1451
+ const released = cleanupRegisteredOnFailure();
1452
+ if (released.length > 0)
1453
+ notifyReleaseCallbacks(released);
1454
+ reject(err);
1455
+ }
1456
+ };
1457
+ const targetInstanceId = getPreferredRemote();
1458
+ if (targetInstanceId !== undefined) {
1459
+ sendRequest(targetInstanceId);
1460
+ }
1461
+ else if (remoteTargetWaitDisabled) {
1462
+ sendRequest(undefined, false);
1463
+ }
1464
+ else {
1465
+ void waitForRemoteTarget().then(sendRequest);
1466
+ }
1161
1467
  });
1162
1468
  },
1163
1469
  accessProperty(path) {
@@ -1167,64 +1473,236 @@ function createClientContext(getMessageChannel, instanceId, options = {}) {
1167
1473
  return;
1168
1474
  }
1169
1475
  const id = createId('req');
1170
- const timeout = options.timeout ?? DEFAULT_TIMEOUT;
1171
- const timer = setTimeout(() => {
1172
- pending.delete(id);
1173
- reject(new Error(`Property access timed out: ${String(path.join('.'))}`));
1174
- }, timeout);
1175
- pending.set(id, { resolve, reject, timer });
1176
- getMessageChannel().getSender().sendMessage('accessPropertyRequest', {
1177
- id,
1178
- path,
1476
+ const timer = armTimer(() => {
1477
+ rejectPending(id, new Error(`Property access timed out: ${String(path.join('.'))}`));
1179
1478
  });
1479
+ pending.set(id, { resolve, reject, timer });
1480
+ let pathSerializationError;
1481
+ try {
1482
+ serializePath(path);
1483
+ }
1484
+ catch (err) {
1485
+ pathSerializationError = err;
1486
+ }
1487
+ if (pathSerializationError !== undefined) {
1488
+ cancelTimer(timer);
1489
+ pending.delete(id);
1490
+ reject(pathSerializationError);
1491
+ return;
1492
+ }
1493
+ const sendRequest = (targetInstanceId, requireTarget = true) => {
1494
+ const callbacks = pending.get(id);
1495
+ if (!callbacks)
1496
+ return;
1497
+ if (targetInstanceId === undefined && requireTarget) {
1498
+ rejectPending(id, new Error('No remote endpoint is available'));
1499
+ return;
1500
+ }
1501
+ if (targetInstanceId !== undefined)
1502
+ callbacks.expectedRemote = targetInstanceId;
1503
+ try {
1504
+ getMessageChannel().getSender().sendMessage('accessPropertyRequest', {
1505
+ id,
1506
+ path,
1507
+ }, targetInstanceId);
1508
+ }
1509
+ catch (err) {
1510
+ cancelTimer(timer);
1511
+ pending.delete(id);
1512
+ reject(err);
1513
+ }
1514
+ };
1515
+ const targetInstanceId = getPreferredRemote();
1516
+ if (targetInstanceId !== undefined) {
1517
+ sendRequest(targetInstanceId);
1518
+ }
1519
+ else if (remoteTargetWaitDisabled) {
1520
+ sendRequest(undefined, false);
1521
+ }
1522
+ else {
1523
+ void waitForRemoteTarget().then(sendRequest);
1524
+ }
1180
1525
  });
1181
1526
  },
1182
- invokeFunctionById(callbackId, args, callbackOptions = {}) {
1527
+ invokeFunctionById(callbackId, args, callbackOptions = {}, remoteInstanceId) {
1183
1528
  return new Promise((resolve, reject) => {
1184
1529
  if (destroyed) {
1185
1530
  reject(new Error('Channel has been destroyed'));
1186
1531
  return;
1187
1532
  }
1188
1533
  const callId = createId('cbcall');
1189
- const timeout = options.timeout ?? DEFAULT_TIMEOUT;
1190
- const timer = setTimeout(() => {
1191
- pending.delete(callId);
1192
- reject(new Error(`Callback call timed out: ${callbackId}`));
1193
- }, timeout);
1194
- pending.set(callId, { resolve, reject, timer });
1195
- getMessageChannel()
1196
- .getSender()
1197
- .sendMessage('invokeFunctionByIdRequest', {
1198
- id: callbackId,
1199
- callId,
1200
- args: args.map((arg) => serialize(arg, registerFunction, callbackOptions)),
1534
+ const timer = armTimer(() => {
1535
+ rejectPending(callId, new Error(`Callback call timed out: ${callbackId}`));
1201
1536
  });
1537
+ pending.set(callId, { resolve, reject, timer, expectedRemote: remoteInstanceId });
1538
+ const registeredIds = [];
1539
+ const collectingRegister = (fn, thisArg, opts) => {
1540
+ const cbId = registerFunction(fn, thisArg, opts);
1541
+ registeredIds.push(cbId);
1542
+ return cbId;
1543
+ };
1544
+ try {
1545
+ const serializedArgs = args.map((arg) => serialize(arg, collectingRegister, callbackOptions));
1546
+ const targetInstanceId = remoteInstanceId ?? getPreferredRemote();
1547
+ recordCallbackOwners(registeredIds, targetInstanceId ?? 'broadcast');
1548
+ getMessageChannel().getSender().sendMessage('invokeFunctionByIdRequest', {
1549
+ id: callbackId,
1550
+ callId,
1551
+ args: serializedArgs,
1552
+ }, targetInstanceId);
1553
+ }
1554
+ catch (err) {
1555
+ cancelTimer(timer);
1556
+ pending.delete(callId);
1557
+ for (const cbId of registeredIds) {
1558
+ if (persistentRefcount.has(cbId)) {
1559
+ releasePersistentRegistrationById(cbId);
1560
+ }
1561
+ else {
1562
+ const entry = functionCache.get(cbId);
1563
+ if (entry) {
1564
+ functionCache.delete(cbId);
1565
+ removeFunctionIdMapping(entry.fn, entry.thisArg);
1566
+ }
1567
+ }
1568
+ callbackOwners.delete(cbId);
1569
+ }
1570
+ reject(err);
1571
+ }
1202
1572
  });
1203
1573
  },
1204
1574
  handleRemoteDestroy(remoteInstanceId) {
1205
1575
  remoteCallbacksByOwner.delete(remoteInstanceId);
1206
1576
  persistentRemoteCallbacksByOwner.delete(remoteInstanceId);
1577
+ knownRemotes.delete(remoteInstanceId);
1578
+ if (boundRemoteInstanceId === remoteInstanceId) {
1579
+ boundRemoteInstanceId = undefined;
1580
+ const preferred = getPreferredRemote();
1581
+ if (preferred !== undefined)
1582
+ boundRemoteInstanceId = preferred;
1583
+ }
1584
+ for (const [id, owners] of callbackOwners) {
1585
+ if (owners === remoteInstanceId) {
1586
+ callbackOwners.delete(id);
1587
+ }
1588
+ else if (owners instanceof Set && owners.has(remoteInstanceId)) {
1589
+ owners.delete(remoteInstanceId);
1590
+ if (owners.size === 0)
1591
+ callbackOwners.delete(id);
1592
+ else if (owners.size === 1)
1593
+ callbackOwners.set(id, owners.values().next().value);
1594
+ }
1595
+ }
1596
+ const reason = new Error(`Remote endpoint ${remoteInstanceId} was destroyed`);
1597
+ const noKnownRemotesLeft = knownRemotes.size === 0;
1598
+ for (const [id, callbacks] of pending) {
1599
+ const targeted = callbacks.expectedRemote === remoteInstanceId;
1600
+ const stranded = callbacks.expectedRemote === undefined && noKnownRemotesLeft;
1601
+ if (!targeted && !stranded)
1602
+ continue;
1603
+ cancelTimer(callbacks.timer);
1604
+ pending.delete(id);
1605
+ safeOnReject(callbacks);
1606
+ callbacks.reject(reason);
1607
+ }
1608
+ },
1609
+ bindRemote(remoteInstanceId) {
1610
+ if (!remoteInstanceId || remoteInstanceId === instanceId)
1611
+ return;
1612
+ knownRemotes.add(remoteInstanceId);
1613
+ if (boundRemoteInstanceId === undefined) {
1614
+ boundRemoteInstanceId = remoteInstanceId;
1615
+ resolveRemoteTargetWaiters(remoteInstanceId);
1616
+ }
1617
+ },
1618
+ noteFreshConnect(remoteInstanceId) {
1619
+ if (!remoteInstanceId || remoteInstanceId === instanceId)
1620
+ return;
1621
+ const isNew = !knownRemotes.has(remoteInstanceId);
1622
+ knownRemotes.add(remoteInstanceId);
1623
+ if (boundRemoteInstanceId === remoteInstanceId)
1624
+ return;
1625
+ if (boundRemoteInstanceId === undefined || isNew) {
1626
+ boundRemoteInstanceId = remoteInstanceId;
1627
+ resolveRemoteTargetWaiters(remoteInstanceId);
1628
+ }
1629
+ },
1630
+ disableRemoteTargetWait() {
1631
+ remoteTargetWaitDisabled = true;
1632
+ resolveRemoteTargetWaiters(undefined);
1633
+ },
1634
+ releaseRemoteCallbacks(remoteInstanceId, ids) {
1635
+ const lru = remoteCallbacksByOwner.get(remoteInstanceId);
1636
+ const pmap = persistentRemoteCallbacksByOwner.get(remoteInstanceId);
1637
+ if (!lru && !pmap)
1638
+ return;
1639
+ for (const id of ids) {
1640
+ lru?.delete(id);
1641
+ pmap?.delete(id);
1642
+ }
1643
+ },
1644
+ dropLocalCallback(id, requesterInstanceId) {
1645
+ dropLocalCallback(id, requesterInstanceId);
1646
+ },
1647
+ noteRemoteSeen(remoteInstanceId) {
1648
+ if (remoteInstanceId && remoteInstanceId !== instanceId) {
1649
+ knownRemotes.add(remoteInstanceId);
1650
+ const preferred = getPreferredRemote();
1651
+ if (preferred !== undefined)
1652
+ resolveRemoteTargetWaiters(preferred);
1653
+ }
1654
+ },
1655
+ bindMethod(fn, owner) {
1656
+ return bindMethod(fn, owner);
1657
+ },
1658
+ getRemoteCallbackCounts(remoteInstanceId) {
1659
+ return {
1660
+ lru: remoteCallbacksByOwner.get(remoteInstanceId)?.size ?? 0,
1661
+ persistent: persistentRemoteCallbacksByOwner.get(remoteInstanceId)?.size ?? 0,
1662
+ };
1663
+ },
1664
+ isDestroyed() {
1665
+ return destroyed;
1207
1666
  },
1208
1667
  destroy(notifyRemote = true) {
1209
1668
  if (destroyed)
1210
1669
  return;
1211
1670
  destroyed = true;
1212
1671
  if (notifyRemote) {
1672
+ const heldRemoteIdSet = new Set();
1673
+ for (const lru of remoteCallbacksByOwner.values()) {
1674
+ for (const id of lru.keys())
1675
+ heldRemoteIdSet.add(id);
1676
+ }
1677
+ for (const map of persistentRemoteCallbacksByOwner.values()) {
1678
+ for (const id of map.keys())
1679
+ heldRemoteIdSet.add(id);
1680
+ }
1681
+ if (heldRemoteIdSet.size > 0) {
1682
+ notifyReleaseCallbacks(Array.from(heldRemoteIdSet));
1683
+ }
1213
1684
  try {
1214
1685
  getMessageChannel().getSender().sendMessage('destroyEndpoint', { instanceId });
1215
1686
  }
1216
- catch {
1217
- // channel may already be torn down
1687
+ catch (err) {
1688
+ if (!isTransportDetachedError(err)) {
1689
+ warn('destroy', 'failed to notify remote of destroy', err);
1690
+ }
1218
1691
  }
1219
1692
  }
1220
1693
  for (const [id, callbacks] of pending) {
1221
- clearTimeout(callbacks.timer);
1694
+ cancelTimer(callbacks.timer);
1695
+ safeOnReject(callbacks);
1222
1696
  callbacks.reject(new Error(`Channel has been destroyed before call ${id} completed`));
1223
1697
  }
1224
1698
  pending.clear();
1225
1699
  functionCache.clear();
1226
1700
  persistentFunctionCache.clear();
1227
- functionIds.clear();
1701
+ persistentRefcount.clear();
1702
+ callbackOwners.clear();
1703
+ knownRemotes.clear();
1704
+ boundRemoteInstanceId = undefined;
1705
+ resolveRemoteTargetWaiters(undefined);
1228
1706
  remoteCallbacksByOwner.clear();
1229
1707
  persistentRemoteCallbacksByOwner.clear();
1230
1708
  },
@@ -1296,38 +1774,55 @@ function createClientProxy(invoke, accessProperty) {
1296
1774
  });
1297
1775
  return [...basePath, ...extraPath];
1298
1776
  };
1299
- function createHandler(node) {
1300
- return {
1777
+ function createNode(node) {
1778
+ const stringChildren = new Map();
1779
+ const symbolChildren = new Map();
1780
+ let getCache;
1781
+ const handler = {
1301
1782
  get(_target, prop) {
1302
1783
  if (typeof prop === 'string') {
1303
1784
  if (INTERCEPTED_STRING_PROPS.has(prop))
1304
1785
  return undefined;
1305
1786
  if (prop === '$get') {
1306
- return (...parts) => {
1307
- if (!accessProperty) {
1308
- return Promise.reject(new Error('Property access is not configured'));
1309
- }
1310
- const basePath = node ? resolvePath(node) : [];
1311
- return accessProperty(normalizePropertyPath(basePath, parts));
1312
- };
1787
+ if (!getCache) {
1788
+ getCache = (...parts) => {
1789
+ if (!accessProperty) {
1790
+ return Promise.reject(new Error('Property access is not configured'));
1791
+ }
1792
+ const basePath = node ? resolvePath(node) : [];
1793
+ return accessProperty(normalizePropertyPath(basePath, parts));
1794
+ };
1795
+ }
1796
+ return getCache;
1313
1797
  }
1798
+ const cached = stringChildren.get(prop);
1799
+ if (cached !== undefined)
1800
+ return cached;
1801
+ const childNode = node ? { parent: node, key: prop } : { key: prop };
1802
+ const child = createNode(childNode);
1803
+ stringChildren.set(prop, child);
1804
+ return child;
1314
1805
  }
1315
- else if (prop === Symbol.toPrimitive) {
1806
+ if (prop === Symbol.toPrimitive)
1316
1807
  return toPrimitiveDescriptor();
1317
- }
1318
- else if (INTERCEPTED_NONE_SYMBOLS.has(prop)) {
1808
+ if (INTERCEPTED_NONE_SYMBOLS.has(prop))
1319
1809
  return undefined;
1320
- }
1810
+ const cached = symbolChildren.get(prop);
1811
+ if (cached !== undefined)
1812
+ return cached;
1321
1813
  const childNode = node ? { parent: node, key: prop } : { key: prop };
1322
- return new Proxy(EMPTY_TARGET, createHandler(childNode));
1814
+ const child = createNode(childNode);
1815
+ symbolChildren.set(prop, child);
1816
+ return child;
1323
1817
  },
1324
1818
  apply(_target, _thisArg, argArray) {
1325
1819
  const path = node ? resolvePath(node) : [];
1326
1820
  return invoke(path, argArray);
1327
1821
  },
1328
1822
  };
1823
+ return new Proxy(EMPTY_TARGET, handler);
1329
1824
  }
1330
- return new Proxy(EMPTY_TARGET, createHandler());
1825
+ return createNode();
1331
1826
  }
1332
1827
  function isPathKey(value) {
1333
1828
  return typeof value === 'string' || typeof value === 'symbol';
@@ -1346,6 +1841,8 @@ function createEndpoint(options) {
1346
1841
  const context = createClientContext(getChannel, instanceId, options.contextOptions);
1347
1842
  const channel = createMessageChannel(options.poster, options.key ?? 'default', instanceId, context, processorRegistry);
1348
1843
  channelRef.current = channel;
1844
+ processorRegistry.register('connectRequest', handleConnectRequest);
1845
+ processorRegistry.register('connectResponse', handleConnectResponse);
1349
1846
  processorRegistry.register('invokeRequest', handleInvokeRequest);
1350
1847
  processorRegistry.register('invokeResponse', handleInvokeResponse);
1351
1848
  processorRegistry.register('accessPropertyRequest', handleAccessPropertyRequest);
@@ -1355,61 +1852,64 @@ function createEndpoint(options) {
1355
1852
  processorRegistry.register('releaseCallbacks', handleReleaseCallbacks);
1356
1853
  processorRegistry.register('destroyEndpoint', handleDestroyEndpoint);
1357
1854
  const proxy = createClientProxy((path, args) => context.invoke(path, args), (path) => context.accessProperty(path));
1855
+ try {
1856
+ channel.getSender().sendMessage('connectRequest', { instanceId });
1857
+ }
1858
+ catch {
1859
+ context.disableRemoteTargetWait();
1860
+ }
1861
+ let destroyed = false;
1358
1862
  return {
1359
1863
  proxy,
1360
1864
  getContext: () => context,
1865
+ isDestroyed: () => destroyed,
1361
1866
  destroy() {
1362
- context.destroy(true);
1867
+ if (destroyed)
1868
+ return;
1869
+ destroyed = true;
1363
1870
  channel.destroy();
1871
+ context.destroy(true);
1364
1872
  },
1365
1873
  };
1366
1874
  }
1367
1875
 
1368
1876
  function setupInMainWindow(options) {
1369
- const resolveTargetOrigin = createTargetOriginResolver(() => options.iframe, options.targetOrigin);
1370
- const poster = createWindowPoster({
1877
+ const resolveTargetOrigin = createTargetOriginResolver(() => options.iframe, options.targetOrigin, options.strictOrigin === true);
1878
+ return setupConnection(options, {
1879
+ resolveTargetOrigin,
1371
1880
  postMessage: (message) => {
1372
1881
  const contentWindow = options.iframe.contentWindow;
1373
1882
  if (!contentWindow) {
1374
- throw new Error('chrome-in-iframe: iframe contentWindow is not available');
1883
+ throw createTransportDetachedError('chrome-in-iframe: iframe contentWindow is not available');
1375
1884
  }
1376
1885
  contentWindow.postMessage(message, resolveTargetOrigin());
1377
1886
  },
1378
1887
  getExpectedSource: () => options.iframe.contentWindow,
1379
- allowedOrigin: options.allowedOrigin,
1380
- });
1381
- const endpoint = createEndpoint({
1382
- poster,
1383
- key: options.key,
1384
- contextOptions: {
1385
- delegateTarget: options.delegateTarget,
1386
- timeout: options.timeout,
1387
- functionCacheMax: options.functionCacheMax,
1388
- functionCacheTtl: options.functionCacheTtl,
1389
- remoteCallbackCacheMax: options.remoteCallbackCacheMax,
1390
- remoteCallbackCacheTtl: options.remoteCallbackCacheTtl,
1391
- },
1888
+ delegateTarget: options.delegateTarget,
1392
1889
  });
1393
- const proxy = endpoint.proxy;
1394
- return {
1395
- proxy,
1396
- get: proxy.$get,
1397
- destroy: endpoint.destroy,
1398
- };
1399
1890
  }
1400
1891
  function setupInIframe(options = {}) {
1401
- const resolveTargetOrigin = createParentTargetOriginResolver(options.targetOrigin);
1402
- const poster = createWindowPoster({
1892
+ const resolveTargetOrigin = createParentTargetOriginResolver(options.targetOrigin, options.strictOrigin === true);
1893
+ return setupConnection(options, {
1894
+ resolveTargetOrigin,
1403
1895
  postMessage: (message) => {
1404
1896
  window.parent.postMessage(message, resolveTargetOrigin());
1405
1897
  },
1406
1898
  getExpectedSource: () => window.parent,
1407
- allowedOrigin: options.allowedOrigin,
1899
+ });
1900
+ }
1901
+ function setupConnection(options, sides) {
1902
+ const allowedOrigin = createAllowedOrigin(options.allowedOrigin, sides.resolveTargetOrigin);
1903
+ const poster = createWindowPoster({
1904
+ postMessage: sides.postMessage,
1905
+ getExpectedSource: sides.getExpectedSource,
1906
+ allowedOrigin,
1408
1907
  });
1409
1908
  const endpoint = createEndpoint({
1410
1909
  poster,
1411
1910
  key: options.key,
1412
1911
  contextOptions: {
1912
+ delegateTarget: sides.delegateTarget,
1413
1913
  timeout: options.timeout,
1414
1914
  functionCacheMax: options.functionCacheMax,
1415
1915
  functionCacheTtl: options.functionCacheTtl,
@@ -1422,6 +1922,7 @@ function setupInIframe(options = {}) {
1422
1922
  proxy,
1423
1923
  get: proxy.$get,
1424
1924
  destroy: endpoint.destroy,
1925
+ isDestroyed: endpoint.isDestroyed,
1425
1926
  };
1426
1927
  }
1427
1928
  function exposeChromeInIframe(options) {
@@ -1438,34 +1939,90 @@ function connectChromeInIframe(options = {}) {
1438
1939
  };
1439
1940
  }
1440
1941
  function createWindowPoster(options) {
1441
- const listenerMap = new Map();
1942
+ const subscriptions = new Map();
1442
1943
  return {
1443
1944
  postMessage(message) {
1444
1945
  options.postMessage(message);
1445
1946
  },
1446
1947
  addEventListener(name, callback) {
1447
- const listener = (event) => {
1448
- const messageEvent = event;
1449
- if (messageEvent.source !== options.getExpectedSource())
1450
- return;
1451
- if (!isAllowedOrigin(messageEvent.origin, options.allowedOrigin))
1452
- return;
1453
- callback(messageEvent);
1948
+ const existing = subscriptions.get(callback);
1949
+ if (existing)
1950
+ windowDispatcher.remove(name, existing);
1951
+ const sub = {
1952
+ isExpectedSource: (source) => source === options.getExpectedSource(),
1953
+ isAllowedOrigin: (origin) => isAllowedOrigin(origin, options.allowedOrigin),
1954
+ deliver: callback,
1454
1955
  };
1455
- listenerMap.set(callback, listener);
1456
- window.addEventListener(name, listener);
1956
+ subscriptions.set(callback, sub);
1957
+ windowDispatcher.add(name, sub);
1457
1958
  },
1458
1959
  removeEventListener(name, callback) {
1459
- const listener = listenerMap.get(callback);
1460
- if (!listener)
1960
+ const sub = subscriptions.get(callback);
1961
+ if (!sub)
1962
+ return;
1963
+ windowDispatcher.remove(name, sub);
1964
+ subscriptions.delete(callback);
1965
+ },
1966
+ };
1967
+ }
1968
+ const windowDispatcher = createWindowDispatcher();
1969
+ function createWindowDispatcher() {
1970
+ const perName = new Map();
1971
+ const domListeners = new Map();
1972
+ const ensureDomListener = (name) => {
1973
+ if (domListeners.has(name))
1974
+ return;
1975
+ if (typeof window === 'undefined')
1976
+ return;
1977
+ const handler = (event) => {
1978
+ const messageEvent = event;
1979
+ const subs = perName.get(name);
1980
+ if (!subs || subs.size === 0)
1981
+ return;
1982
+ for (const sub of subs) {
1983
+ if (!sub.isExpectedSource(messageEvent.source))
1984
+ continue;
1985
+ if (!sub.isAllowedOrigin(messageEvent.origin))
1986
+ continue;
1987
+ try {
1988
+ sub.deliver({ data: messageEvent.data });
1989
+ }
1990
+ catch (err) {
1991
+ warn('windowDispatcher', 'subscriber threw while delivering message', err);
1992
+ }
1993
+ }
1994
+ };
1995
+ domListeners.set(name, handler);
1996
+ window.addEventListener(name, handler);
1997
+ };
1998
+ return {
1999
+ add(name, sub) {
2000
+ let subs = perName.get(name);
2001
+ if (!subs) {
2002
+ subs = new Set();
2003
+ perName.set(name, subs);
2004
+ }
2005
+ subs.add(sub);
2006
+ ensureDomListener(name);
2007
+ },
2008
+ remove(name, sub) {
2009
+ const subs = perName.get(name);
2010
+ if (!subs)
2011
+ return;
2012
+ subs.delete(sub);
2013
+ if (subs.size > 0)
1461
2014
  return;
1462
- window.removeEventListener(name, listener);
1463
- listenerMap.delete(callback);
2015
+ perName.delete(name);
2016
+ const handler = domListeners.get(name);
2017
+ if (handler && typeof window !== 'undefined') {
2018
+ window.removeEventListener(name, handler);
2019
+ }
2020
+ domListeners.delete(name);
1464
2021
  },
1465
2022
  };
1466
2023
  }
1467
2024
  function isAllowedOrigin(origin, allowedOrigin) {
1468
- if (!allowedOrigin)
2025
+ if (allowedOrigin === undefined)
1469
2026
  return true;
1470
2027
  if (typeof allowedOrigin === 'function')
1471
2028
  return allowedOrigin(origin);
@@ -1473,24 +2030,51 @@ function isAllowedOrigin(origin, allowedOrigin) {
1473
2030
  return allowedOrigin.includes(origin);
1474
2031
  return allowedOrigin === origin;
1475
2032
  }
2033
+ function createAllowedOrigin(explicit, resolveTargetOrigin) {
2034
+ if (explicit !== undefined)
2035
+ return explicit;
2036
+ return (origin) => {
2037
+ const targetOrigin = resolveTargetOrigin();
2038
+ if (targetOrigin === '*') {
2039
+ if (typeof location !== 'undefined' && location.origin && location.origin !== 'null') {
2040
+ return origin === location.origin;
2041
+ }
2042
+ return false;
2043
+ }
2044
+ return targetOrigin === origin;
2045
+ };
2046
+ }
1476
2047
  function getGlobalChrome() {
1477
2048
  const chromeApi = globalThis.chrome;
1478
2049
  if (!chromeApi) {
1479
- throw new Error('chrome-in-iframe: global chrome API is not available');
2050
+ throw new Error('chrome-in-iframe: global `chrome` API is not available. ' +
2051
+ 'Make sure this runs inside a Chrome extension page, or pass a custom ' +
2052
+ '`chromeApi` option (e.g. for tests / non-extension environments).');
1480
2053
  }
1481
2054
  return chromeApi;
1482
2055
  }
1483
- function createTargetOriginResolver(getIframe, explicit) {
2056
+ function createTargetOriginResolver(getIframe, explicit, strict) {
1484
2057
  if (explicit)
1485
2058
  return () => explicit;
1486
- return () => deriveOriginFromIframe(getIframe());
2059
+ return freezeResolvedOrigin(() => deriveOriginFromIframe(getIframe(), strict === true));
1487
2060
  }
1488
- function createParentTargetOriginResolver(explicit) {
2061
+ function createParentTargetOriginResolver(explicit, strict) {
1489
2062
  if (explicit)
1490
2063
  return () => explicit;
1491
- return () => deriveParentOrigin();
2064
+ return freezeResolvedOrigin(() => deriveParentOrigin(strict === true));
2065
+ }
2066
+ function freezeResolvedOrigin(resolve) {
2067
+ let frozen;
2068
+ return () => {
2069
+ if (frozen !== undefined)
2070
+ return frozen;
2071
+ const resolved = resolve();
2072
+ if (resolved !== '*')
2073
+ frozen = resolved;
2074
+ return resolved;
2075
+ };
1492
2076
  }
1493
- function deriveOriginFromIframe(iframe) {
2077
+ function deriveOriginFromIframe(iframe, strict) {
1494
2078
  const src = iframe.src || iframe.getAttribute('src') || '';
1495
2079
  const origin = parseOrigin(src);
1496
2080
  if (origin)
@@ -1502,16 +2086,17 @@ function deriveOriginFromIframe(iframe) {
1502
2086
  return loc.origin;
1503
2087
  }
1504
2088
  }
1505
- catch {
1506
- // cross-origin contentWindow.location access throws — fall through
2089
+ catch (err) {
2090
+ warn('deriveOriginFromIframe', 'unable to access cross-origin contentWindow.location', err);
1507
2091
  }
1508
- if (typeof console !== 'undefined') {
1509
- console.warn("chrome-in-iframe: unable to derive iframe origin automatically; falling back to '*'. " +
1510
- 'Pass `targetOrigin` explicitly to lock the destination.');
2092
+ if (strict) {
2093
+ throw new Error('chrome-in-iframe: unable to derive iframe origin automatically; pass `targetOrigin` explicitly or set `strictOrigin: false`.');
1511
2094
  }
2095
+ warn('deriveOriginFromIframe', "unable to derive iframe origin automatically; falling back to '*'. " +
2096
+ 'Pass `targetOrigin` explicitly to lock the destination.');
1512
2097
  return '*';
1513
2098
  }
1514
- function deriveParentOrigin() {
2099
+ function deriveParentOrigin(strict) {
1515
2100
  if (typeof document !== 'undefined') {
1516
2101
  const referrer = document.referrer;
1517
2102
  const origin = parseOrigin(referrer);
@@ -1523,10 +2108,11 @@ function deriveParentOrigin() {
1523
2108
  if (first && first !== 'null')
1524
2109
  return first;
1525
2110
  }
1526
- if (typeof console !== 'undefined') {
1527
- console.warn("chrome-in-iframe: unable to derive parent origin automatically; falling back to '*'. " +
1528
- 'Pass `targetOrigin` explicitly to lock the destination.');
2111
+ if (strict) {
2112
+ throw new Error('chrome-in-iframe: unable to derive parent origin automatically; pass `targetOrigin` explicitly or set `strictOrigin: false`.');
1529
2113
  }
2114
+ warn('deriveParentOrigin', "unable to derive parent origin automatically; falling back to '*'. " +
2115
+ 'Pass `targetOrigin` explicitly to lock the destination.');
1530
2116
  return '*';
1531
2117
  }
1532
2118
  function parseOrigin(url) {
@@ -1537,10 +2123,11 @@ function parseOrigin(url) {
1537
2123
  if (parsed.origin && parsed.origin !== 'null')
1538
2124
  return parsed.origin;
1539
2125
  }
1540
- catch {
2126
+ catch (err) {
2127
+ warn('parseOrigin', 'failed to parse URL', url, err);
1541
2128
  return null;
1542
2129
  }
1543
2130
  return null;
1544
2131
  }
1545
2132
 
1546
- export { connectChromeInIframe, exposeChromeInIframe, setupInIframe, setupInMainWindow };
2133
+ export { TRANSPORT_DETACHED, connectChromeInIframe, exposeChromeInIframe, isTransportDetachedError, setLogger, setupInIframe, setupInMainWindow };