chrome-in-iframe 2.0.0 → 2.0.1

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,22 @@
1
1
  import { nanoid } from 'nanoid';
2
2
  import { LRUCache } from 'lru-cache';
3
3
 
4
+ const PREFIX = 'chrome-in-iframe';
5
+ const defaultLogger = (scope, ...args) => {
6
+ if (typeof console !== 'undefined' && typeof console.warn === 'function') {
7
+ console.warn(`[${PREFIX}] ${scope}`, ...args);
8
+ }
9
+ };
10
+ let activeLogger = defaultLogger;
11
+ function setLogger(logger) {
12
+ activeLogger = logger;
13
+ }
14
+ function warn(scope, ...args) {
15
+ if (!activeLogger)
16
+ return;
17
+ activeLogger(scope, ...args);
18
+ }
19
+
4
20
  const WELL_KNOWN_SYMBOLS = {
5
21
  asyncIterator: Symbol.asyncIterator,
6
22
  hasInstance: Symbol.hasInstance,
@@ -16,12 +32,9 @@ const WELL_KNOWN_SYMBOLS = {
16
32
  toStringTag: Symbol.toStringTag,
17
33
  unscopables: Symbol.unscopables,
18
34
  };
35
+ const WELL_KNOWN_SYMBOL_NAMES = new Map(Object.entries(WELL_KNOWN_SYMBOLS).map(([name, symbol]) => [symbol, name]));
19
36
  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;
37
+ return WELL_KNOWN_SYMBOL_NAMES.get(value);
25
38
  }
26
39
  function getWellKnownSymbol(name) {
27
40
  return WELL_KNOWN_SYMBOLS[name];
@@ -32,6 +45,11 @@ function serializeThrownError(err) {
32
45
  }
33
46
  return { message: String(err) };
34
47
  }
48
+ function isPromiseLike(value) {
49
+ return (value !== null &&
50
+ (typeof value === 'object' || typeof value === 'function') &&
51
+ typeof value.then === 'function');
52
+ }
35
53
 
36
54
  function serializePath(path) {
37
55
  return path.map((key) => {
@@ -117,15 +135,23 @@ function serializeMessageData(type, data) {
117
135
 
118
136
  function createMessageChannel(poster, key, instanceId, context, processorRegistry) {
119
137
  const sender = createMessageSender(poster, key, instanceId);
138
+ const keyMatch = `"key":${JSON.stringify(key)}`;
120
139
  const listener = (event) => {
140
+ if (typeof event.data !== 'string')
141
+ return;
142
+ if (event.data.length === 0 || event.data.charCodeAt(0) !== 123 /* '{' */)
143
+ return;
144
+ if (event.data.indexOf(keyMatch) === -1)
145
+ return;
121
146
  let body;
122
147
  try {
123
148
  body = JSON.parse(event.data);
124
149
  }
125
- catch {
150
+ catch (err) {
151
+ warn('createMessageChannel', 'failed to parse incoming message as JSON', err);
126
152
  return;
127
153
  }
128
- if (!isMessageBody(body))
154
+ if (!isMessageEnvelope(body))
129
155
  return;
130
156
  if (body.key !== key)
131
157
  return;
@@ -136,6 +162,8 @@ function createMessageChannel(poster, key, instanceId, context, processorRegistr
136
162
  const handler = processorRegistry.get(body.type);
137
163
  if (!handler)
138
164
  return;
165
+ if (!isValidMessagePayload(body))
166
+ return;
139
167
  const meta = { senderInstanceId: body.senderInstanceId };
140
168
  try {
141
169
  handler(deserializeMessageData(body), context, meta);
@@ -149,34 +177,25 @@ function createMessageChannel(poster, key, instanceId, context, processorRegistr
149
177
  getSender() {
150
178
  return sender;
151
179
  },
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
180
  destroy() {
165
181
  poster.removeEventListener('message', listener);
166
182
  },
167
183
  };
168
184
  }
169
- function isMessageBody(value) {
185
+ function isMessageEnvelope(value) {
170
186
  if (!isRecord(value))
171
187
  return false;
172
- if (typeof value.type !== 'string')
188
+ if (typeof value.type !== 'string' || value.type.length === 0)
173
189
  return false;
174
190
  if (typeof value.key !== 'string')
175
191
  return false;
176
- if (typeof value.senderInstanceId !== 'string')
192
+ if (typeof value.senderInstanceId !== 'string' || value.senderInstanceId.length === 0)
177
193
  return false;
178
194
  if ('targetInstanceId' in value && typeof value.targetInstanceId !== 'string' && value.targetInstanceId !== undefined)
179
195
  return false;
196
+ return true;
197
+ }
198
+ function isValidMessagePayload(value) {
180
199
  switch (value.type) {
181
200
  case 'invokeRequest':
182
201
  return isInvokeRequest(value.data);
@@ -265,7 +284,7 @@ function sendProcessorError(body, sender, err) {
265
284
  if (body.type === 'invokeFunctionByIdRequest') {
266
285
  const data = body.data;
267
286
  const error = serializeThrownError(err);
268
- console.warn(`chrome-in-iframe: callback '${data.id}' failed: ${error.message}`);
287
+ warn('sendProcessorError', `callback '${data.id}' failed: ${error.message}`);
269
288
  sender.sendMessage('invokeFunctionByIdResponse', {
270
289
  id: data.callId,
271
290
  error,
@@ -570,8 +589,8 @@ function deserializeError(source, generateCallback, getRemoteCallback) {
570
589
  try {
571
590
  error.name = name;
572
591
  }
573
- catch {
574
- // ignore non-writable name
592
+ catch (err) {
593
+ warn('deserializeError', 'failed to set error.name', name, err);
575
594
  }
576
595
  }
577
596
  if (typeof source.stack === 'string')
@@ -587,10 +606,10 @@ function deserializeError(source, generateCallback, getRemoteCallback) {
587
606
  if (typeof rawKey !== 'string')
588
607
  continue;
589
608
  try {
590
- error[rawKey] = deserialize0(rawValue, generateCallback, getRemoteCallback);
609
+ setOwnProperty(error, rawKey, deserialize0(rawValue, generateCallback, getRemoteCallback));
591
610
  }
592
- catch {
593
- // ignore non-writable property
611
+ catch (err) {
612
+ warn('deserializeError', 'failed to set error property', rawKey, err);
594
613
  }
595
614
  }
596
615
  }
@@ -607,7 +626,7 @@ function deserializeWrappedObject(entries, generateCallback, getRemoteCallback)
607
626
  const value = deserialize0(entry[1], generateCallback, getRemoteCallback);
608
627
  const key = resolveObjectKey(rawKey);
609
628
  if (key === undefined) {
610
- console.warn('chrome-in-iframe: dropping wrapped-object entry with unresolvable key', rawKey);
629
+ warn('deserializeWrappedObject', 'dropping wrapped-object entry with unresolvable key', rawKey);
611
630
  continue;
612
631
  }
613
632
  Object.defineProperty(result, key, {
@@ -628,7 +647,8 @@ function resolveObjectKey(raw) {
628
647
  try {
629
648
  return deserializeSymbol(tagged.v);
630
649
  }
631
- catch {
650
+ catch (err) {
651
+ warn('resolveObjectKey', 'failed to deserialize symbol key', tagged.v, err);
632
652
  return undefined;
633
653
  }
634
654
  }
@@ -673,7 +693,7 @@ function setOwnProperty(target, key, value) {
673
693
 
674
694
  function isListenerRegistrationPath(path) {
675
695
  const last = path[path.length - 1];
676
- return last === 'addListener' || last === 'hasListener';
696
+ return last === 'addListener';
677
697
  }
678
698
  function isListenerRemovalPath(path) {
679
699
  const last = path[path.length - 1];
@@ -694,23 +714,6 @@ function readProperty(target, key) {
694
714
  return target[key];
695
715
  }
696
716
 
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
717
  function handleAccessPropertyRequest(data, ctx, meta) {
715
718
  const target = ctx.getDelegateTarget();
716
719
  const channel = ctx.getMessageChannel();
@@ -721,7 +724,7 @@ function handleAccessPropertyRequest(data, ctx, meta) {
721
724
  }, meta.senderInstanceId);
722
725
  return;
723
726
  }
724
- if (!target) {
727
+ if (target === undefined || target === null) {
725
728
  channel.getSender().sendMessage('accessPropertyResponse', {
726
729
  id: data.id,
727
730
  error: { message: 'No delegate target is configured' },
@@ -731,18 +734,24 @@ function handleAccessPropertyRequest(data, ctx, meta) {
731
734
  try {
732
735
  let current = target;
733
736
  let owner = target;
734
- for (const key of data.path) {
737
+ for (let i = 0; i < data.path.length; i++) {
738
+ const key = data.path[i];
735
739
  if (current === undefined || current === null) {
736
740
  channel.getSender().sendMessage('accessPropertyResponse', {
737
741
  id: data.id,
738
- data: serialize(current, ctx.registerFunction),
742
+ error: {
743
+ message: `Cannot read property '${String(key)}' of ${current === null ? 'null' : 'undefined'} (at '${data.path
744
+ .slice(0, i)
745
+ .map(String)
746
+ .join('.')}')`,
747
+ },
739
748
  }, meta.senderInstanceId);
740
749
  return;
741
750
  }
742
751
  owner = current;
743
752
  current = readProperty(current, key);
744
753
  }
745
- const value = typeof current === 'function' ? bindMethod(current, owner) : current;
754
+ const value = typeof current === 'function' ? ctx.bindMethod(current, owner) : current;
746
755
  channel.getSender().sendMessage('accessPropertyResponse', {
747
756
  id: data.id,
748
757
  data: serialize(value, ctx.registerFunction, { persistent: isLikelyListenerPath(data.path) }),
@@ -761,17 +770,21 @@ function handleAccessPropertyResponse(data, ctx, meta) {
761
770
  return;
762
771
  if (data.error) {
763
772
  const err = new Error(data.error.message);
764
- err.stack = data.error.stack;
773
+ if (data.error.stack)
774
+ err.stack = data.error.stack;
775
+ pending.onReject?.();
765
776
  pending.reject(err);
766
777
  return;
767
778
  }
768
779
  const scopedRemoteCallback = createScopedRemoteCallback$2(ctx, meta.senderInstanceId);
769
780
  try {
781
+ pending.onResolve?.();
770
782
  pending.resolve(deserialize(data.data, (id, args, options) => {
771
783
  return ctx.invokeFunctionById(id, args, options);
772
784
  }, scopedRemoteCallback));
773
785
  }
774
786
  catch (err) {
787
+ pending.onReject?.();
775
788
  pending.reject(err instanceof Error ? err : new Error(String(err)));
776
789
  }
777
790
  }
@@ -783,13 +796,20 @@ function handleInvokeRequest(data, ctx, meta) {
783
796
  const target = ctx.getDelegateTarget();
784
797
  const channel = ctx.getMessageChannel();
785
798
  const scopedRemoteCallback = createScopedRemoteCallback$1(ctx, meta.senderInstanceId);
786
- if (!target) {
799
+ if (target === undefined || target === null) {
787
800
  channel.getSender().sendMessage('invokeResponse', {
788
801
  id: data.id,
789
802
  error: { message: 'No delegate target is configured' },
790
803
  }, meta.senderInstanceId);
791
804
  return;
792
805
  }
806
+ if (data.path.length === 0) {
807
+ channel.getSender().sendMessage('invokeResponse', {
808
+ id: data.id,
809
+ error: { message: 'Invocation path must not be empty' },
810
+ }, meta.senderInstanceId);
811
+ return;
812
+ }
793
813
  let current = target;
794
814
  for (let i = 0; i < data.path.length - 1; i++) {
795
815
  current = readProperty(current, data.path[i]);
@@ -797,7 +817,10 @@ function handleInvokeRequest(data, ctx, meta) {
797
817
  channel.getSender().sendMessage('invokeResponse', {
798
818
  id: data.id,
799
819
  error: {
800
- message: `Cannot read property '${String(data.path[i + 1])}' of ${String(data.path[i])}`,
820
+ message: `Cannot read property '${String(data.path[i + 1])}' of ${current === null ? 'null' : 'undefined'} (at '${data.path
821
+ .slice(0, i + 1)
822
+ .map(String)
823
+ .join('.')}')`,
801
824
  },
802
825
  }, meta.senderInstanceId);
803
826
  return;
@@ -836,8 +859,8 @@ function handleInvokeRequest(data, ctx, meta) {
836
859
  }, meta.senderInstanceId);
837
860
  return;
838
861
  }
839
- if (result instanceof Promise) {
840
- result
862
+ if (isPromiseLike(result)) {
863
+ Promise.resolve(result)
841
864
  .then((value) => {
842
865
  sendInvokeSuccess(data.id, value, ctx, meta.senderInstanceId);
843
866
  })
@@ -847,10 +870,9 @@ function handleInvokeRequest(data, ctx, meta) {
847
870
  error: serializeThrownError(err),
848
871
  }, meta.senderInstanceId);
849
872
  });
873
+ return;
850
874
  }
851
- else {
852
- sendInvokeSuccess(data.id, result, ctx, meta.senderInstanceId);
853
- }
875
+ sendInvokeSuccess(data.id, result, ctx, meta.senderInstanceId);
854
876
  }
855
877
  function handleInvokeResponse(data, ctx, meta) {
856
878
  const pending = ctx.getAndRemovePendingPromise(data.id);
@@ -858,17 +880,21 @@ function handleInvokeResponse(data, ctx, meta) {
858
880
  return;
859
881
  if (data.error) {
860
882
  const err = new Error(data.error.message);
861
- err.stack = data.error.stack;
883
+ if (data.error.stack)
884
+ err.stack = data.error.stack;
885
+ pending.onReject?.();
862
886
  pending.reject(err);
863
887
  }
864
888
  else {
865
889
  const scopedRemoteCallback = createScopedRemoteCallback$1(ctx, meta.senderInstanceId);
866
890
  try {
891
+ pending.onResolve?.();
867
892
  pending.resolve(deserialize(data.data, (id, args, options) => {
868
893
  return ctx.invokeFunctionById(id, args, options);
869
894
  }, scopedRemoteCallback));
870
895
  }
871
896
  catch (err) {
897
+ pending.onReject?.();
872
898
  pending.reject(err instanceof Error ? err : new Error(String(err)));
873
899
  }
874
900
  }
@@ -898,7 +924,7 @@ function handleCallbackInvoke(data, ctx, meta) {
898
924
  const entry = persistentEntry ?? ctx.getFunctionCache().get(data.id);
899
925
  if (!entry) {
900
926
  const error = { message: `Callback '${data.id}' is not available or has expired` };
901
- console.warn(`chrome-in-iframe: ${error.message}`);
927
+ warn('handleCallbackInvoke', error.message);
902
928
  channel.getSender().sendMessage('invokeFunctionByIdResponse', {
903
929
  id: data.callId,
904
930
  error,
@@ -918,8 +944,8 @@ function handleCallbackInvoke(data, ctx, meta) {
918
944
  }
919
945
  try {
920
946
  const result = Reflect.apply(entry.fn, entry.thisArg, deserializedArgs);
921
- if (result instanceof Promise) {
922
- result
947
+ if (isPromiseLike(result)) {
948
+ Promise.resolve(result)
923
949
  .then((value) => {
924
950
  sendCallbackSuccess(data.callId, value, ctx, meta.senderInstanceId);
925
951
  })
@@ -940,17 +966,21 @@ function handleCallbackInvokeResponse(data, ctx, meta) {
940
966
  return;
941
967
  if (data.error) {
942
968
  const err = new Error(data.error.message);
943
- err.stack = data.error.stack;
969
+ if (data.error.stack)
970
+ err.stack = data.error.stack;
971
+ pending.onReject?.();
944
972
  pending.reject(err);
945
973
  return;
946
974
  }
947
975
  const scopedRemoteCallback = createScopedRemoteCallback(ctx, meta.senderInstanceId);
948
976
  try {
977
+ pending.onResolve?.();
949
978
  pending.resolve(deserialize(data.data, (id, args, options) => {
950
979
  return ctx.invokeFunctionById(id, args, options);
951
980
  }, scopedRemoteCallback));
952
981
  }
953
982
  catch (err) {
983
+ pending.onReject?.();
954
984
  pending.reject(err instanceof Error ? err : new Error(String(err)));
955
985
  }
956
986
  }
@@ -979,13 +1009,17 @@ function createScopedRemoteCallback(ctx, remoteInstanceId) {
979
1009
  return (id, invoke, options) => ctx.getRemoteCallback(id, invoke, options, remoteInstanceId);
980
1010
  }
981
1011
 
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);
1012
+ const MAX_RELEASE_IDS = 100000;
1013
+ function handleReleaseCallbacks(data, ctx, meta) {
1014
+ const ids = data.ids;
1015
+ const total = ids.length > MAX_RELEASE_IDS ? MAX_RELEASE_IDS : ids.length;
1016
+ if (ids.length > MAX_RELEASE_IDS) {
1017
+ warn('handleReleaseCallbacks', `releaseCallbacks payload exceeds cap (${ids.length} > ${MAX_RELEASE_IDS}); only the first ${MAX_RELEASE_IDS} ids will be released`);
1018
+ }
1019
+ const effectiveIds = total === ids.length ? ids : ids.slice(0, total);
1020
+ ctx.releaseRemoteCallbacks(meta.senderInstanceId, effectiveIds);
1021
+ for (let i = 0; i < total; i++) {
1022
+ ctx.dropLocalCallback(ids[i]);
989
1023
  }
990
1024
  }
991
1025
  function handleDestroyEndpoint(_data, ctx, meta) {
@@ -1009,24 +1043,60 @@ const DEFAULT_FUNCTION_CACHE_MAX = 100;
1009
1043
  const DEFAULT_FUNCTION_CACHE_TTL = 5 * 60 * 1000;
1010
1044
  const DEFAULT_REMOTE_CALLBACK_CACHE_MAX = 500;
1011
1045
  const DEFAULT_REMOTE_CALLBACK_CACHE_TTL = 5 * 60 * 1000;
1046
+ function resolveTimeout(raw) {
1047
+ if (raw === undefined)
1048
+ return DEFAULT_TIMEOUT;
1049
+ if (typeof raw !== 'number' || !Number.isFinite(raw) || raw <= 0)
1050
+ return DEFAULT_TIMEOUT;
1051
+ return raw;
1052
+ }
1012
1053
  function createClientContext(getMessageChannel, instanceId, options = {}) {
1013
1054
  const remoteCacheMax = options.remoteCallbackCacheMax ?? DEFAULT_REMOTE_CALLBACK_CACHE_MAX;
1014
1055
  const remoteCacheTtl = options.remoteCallbackCacheTtl ?? DEFAULT_REMOTE_CALLBACK_CACHE_TTL;
1056
+ const callTimeout = resolveTimeout(options.timeout);
1015
1057
  const pending = new Map();
1016
1058
  const functionIds = new Map();
1059
+ const persistentFunctionCache = new Map();
1060
+ const persistentRefcount = new Map();
1017
1061
  const functionCache = new LRUCache({
1018
1062
  max: options.functionCacheMax ?? DEFAULT_FUNCTION_CACHE_MAX,
1019
1063
  ttl: options.functionCacheTtl ?? DEFAULT_FUNCTION_CACHE_TTL,
1020
- dispose: (value, _key, reason) => {
1064
+ dispose: (value, key, reason) => {
1021
1065
  if (reason === 'set' || reason === 'delete')
1022
1066
  return;
1023
- functionIds.delete(value.fn);
1067
+ if (persistentFunctionCache.has(key))
1068
+ return;
1069
+ removeFunctionIdMapping(value.fn, value.thisArg);
1024
1070
  },
1025
1071
  });
1026
- const persistentFunctionCache = new Map();
1027
1072
  const remoteCallbacksByOwner = new Map();
1028
1073
  const persistentRemoteCallbacksByOwner = new Map();
1074
+ const boundMethodCache = new WeakMap();
1029
1075
  let destroyed = false;
1076
+ const bindMethod = (fn, owner) => {
1077
+ if (owner === null || typeof owner !== 'object') {
1078
+ return (...args) => Reflect.apply(fn, owner, args);
1079
+ }
1080
+ let perOwner = boundMethodCache.get(fn);
1081
+ if (!perOwner) {
1082
+ perOwner = new WeakMap();
1083
+ boundMethodCache.set(fn, perOwner);
1084
+ }
1085
+ const cached = perOwner.get(owner);
1086
+ if (cached)
1087
+ return cached;
1088
+ const bound = (...args) => Reflect.apply(fn, owner, args);
1089
+ perOwner.set(owner, bound);
1090
+ return bound;
1091
+ };
1092
+ const removeFunctionIdMapping = (fn, thisArg) => {
1093
+ const perOwner = functionIds.get(fn);
1094
+ if (!perOwner)
1095
+ return;
1096
+ perOwner.delete(thisArg);
1097
+ if (perOwner.size === 0)
1098
+ functionIds.delete(fn);
1099
+ };
1030
1100
  const getOrCreateRemoteLru = (remoteId) => {
1031
1101
  let lru = remoteCallbacksByOwner.get(remoteId);
1032
1102
  if (!lru) {
@@ -1044,21 +1114,51 @@ function createClientContext(getMessageChannel, instanceId, options = {}) {
1044
1114
  return map;
1045
1115
  };
1046
1116
  const registerFunction = (fn, thisArg, callbackOptions = {}) => {
1047
- let id = functionIds.get(fn);
1117
+ let perOwner = functionIds.get(fn);
1118
+ if (!perOwner) {
1119
+ perOwner = new Map();
1120
+ functionIds.set(fn, perOwner);
1121
+ }
1122
+ let id = perOwner.get(thisArg);
1048
1123
  if (!id) {
1049
1124
  id = createId('cb');
1050
- functionIds.set(fn, id);
1125
+ perOwner.set(thisArg, id);
1051
1126
  }
1052
1127
  const entry = { fn, thisArg };
1053
1128
  if (callbackOptions.persistent) {
1054
1129
  persistentFunctionCache.set(id, entry);
1055
1130
  functionCache.delete(id);
1131
+ persistentRefcount.set(id, (persistentRefcount.get(id) ?? 0) + 1);
1056
1132
  }
1057
1133
  else if (!persistentFunctionCache.has(id)) {
1058
1134
  functionCache.set(id, entry);
1059
1135
  }
1060
1136
  return id;
1061
1137
  };
1138
+ const releasePersistentRegistrationById = (id) => {
1139
+ if (!persistentRefcount.has(id))
1140
+ return undefined;
1141
+ const next = (persistentRefcount.get(id) ?? 0) - 1;
1142
+ if (next > 0) {
1143
+ persistentRefcount.set(id, next);
1144
+ return undefined;
1145
+ }
1146
+ persistentRefcount.delete(id);
1147
+ const entry = persistentFunctionCache.get(id);
1148
+ persistentFunctionCache.delete(id);
1149
+ functionCache.delete(id);
1150
+ if (entry)
1151
+ removeFunctionIdMapping(entry.fn, entry.thisArg);
1152
+ return id;
1153
+ };
1154
+ const dropLocalCallback = (id) => {
1155
+ const entry = persistentFunctionCache.get(id) ?? functionCache.get(id);
1156
+ persistentRefcount.delete(id);
1157
+ persistentFunctionCache.delete(id);
1158
+ functionCache.delete(id);
1159
+ if (entry)
1160
+ removeFunctionIdMapping(entry.fn, entry.thisArg);
1161
+ };
1062
1162
  const getRemoteCallback = (id, invoke, callbackOptions, remoteInstanceId) => {
1063
1163
  const opts = callbackOptions ?? {};
1064
1164
  const lru = remoteCallbacksByOwner.get(remoteInstanceId);
@@ -1085,10 +1185,19 @@ function createClientContext(getMessageChannel, instanceId, options = {}) {
1085
1185
  try {
1086
1186
  getMessageChannel().getSender().sendMessage('releaseCallbacks', { ids });
1087
1187
  }
1088
- catch {
1089
- // channel may not be available yet
1188
+ catch (err) {
1189
+ warn('notifyReleaseCallbacks', 'failed to release callbacks', err);
1090
1190
  }
1091
1191
  };
1192
+ const rejectPending = (id, error) => {
1193
+ const callbacks = pending.get(id);
1194
+ if (!callbacks)
1195
+ return;
1196
+ clearTimeout(callbacks.timer);
1197
+ pending.delete(id);
1198
+ callbacks.onReject?.();
1199
+ callbacks.reject(error);
1200
+ };
1092
1201
  return {
1093
1202
  getDelegateTarget() {
1094
1203
  return options.delegateTarget;
@@ -1119,9 +1228,6 @@ function createClientContext(getMessageChannel, instanceId, options = {}) {
1119
1228
  }
1120
1229
  return callbacks;
1121
1230
  },
1122
- registerPendingPromise(id, callbacks) {
1123
- pending.set(id, callbacks);
1124
- },
1125
1231
  invoke(path, args) {
1126
1232
  return new Promise((resolve, reject) => {
1127
1233
  if (destroyed) {
@@ -1129,35 +1235,63 @@ function createClientContext(getMessageChannel, instanceId, options = {}) {
1129
1235
  return;
1130
1236
  }
1131
1237
  const id = createId('req');
1132
- const timeout = options.timeout ?? DEFAULT_TIMEOUT;
1238
+ const persistent = isListenerRegistrationPath(path);
1239
+ const registeredIds = [];
1240
+ const collectingRegister = (fn, thisArg, opts) => {
1241
+ const cbId = registerFunction(fn, thisArg, opts);
1242
+ registeredIds.push(cbId);
1243
+ return cbId;
1244
+ };
1245
+ const releaseRegisteredPersistents = () => {
1246
+ const released = [];
1247
+ for (const cbId of registeredIds) {
1248
+ const releasedId = releasePersistentRegistrationById(cbId);
1249
+ if (releasedId)
1250
+ released.push(releasedId);
1251
+ }
1252
+ return released;
1253
+ };
1133
1254
  const timer = setTimeout(() => {
1255
+ rejectPending(id, new Error(`Call timed out: ${String(path.join('.'))}`));
1256
+ }, callTimeout);
1257
+ try {
1258
+ const channel = getMessageChannel();
1259
+ const serializedArgs = args.map((arg) => serialize(arg, collectingRegister, { persistent }));
1260
+ pending.set(id, {
1261
+ resolve,
1262
+ reject,
1263
+ timer,
1264
+ onResolve: () => {
1265
+ if (isListenerRemovalPath(path)) {
1266
+ const released = releaseRegisteredPersistents();
1267
+ if (released.length > 0)
1268
+ notifyReleaseCallbacks(released);
1269
+ }
1270
+ },
1271
+ onReject: () => {
1272
+ if (isListenerRegistrationPath(path)) {
1273
+ const released = releaseRegisteredPersistents();
1274
+ if (released.length > 0)
1275
+ notifyReleaseCallbacks(released);
1276
+ }
1277
+ },
1278
+ });
1279
+ channel.getSender().sendMessage('invokeRequest', {
1280
+ id,
1281
+ path,
1282
+ args: serializedArgs,
1283
+ });
1284
+ }
1285
+ catch (err) {
1286
+ clearTimeout(timer);
1134
1287
  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
- 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);
1152
- }
1288
+ if (isListenerRegistrationPath(path)) {
1289
+ const released = releaseRegisteredPersistents();
1290
+ if (released.length > 0)
1291
+ notifyReleaseCallbacks(released);
1153
1292
  }
1154
- notifyReleaseCallbacks(releasedIds);
1293
+ reject(err);
1155
1294
  }
1156
- channel.getSender().sendMessage('invokeRequest', {
1157
- id,
1158
- path,
1159
- args: serializedArgs,
1160
- });
1161
1295
  });
1162
1296
  },
1163
1297
  accessProperty(path) {
@@ -1167,16 +1301,21 @@ function createClientContext(getMessageChannel, instanceId, options = {}) {
1167
1301
  return;
1168
1302
  }
1169
1303
  const id = createId('req');
1170
- const timeout = options.timeout ?? DEFAULT_TIMEOUT;
1171
1304
  const timer = setTimeout(() => {
1172
- pending.delete(id);
1173
- reject(new Error(`Property access timed out: ${String(path.join('.'))}`));
1174
- }, timeout);
1305
+ rejectPending(id, new Error(`Property access timed out: ${String(path.join('.'))}`));
1306
+ }, callTimeout);
1175
1307
  pending.set(id, { resolve, reject, timer });
1176
- getMessageChannel().getSender().sendMessage('accessPropertyRequest', {
1177
- id,
1178
- path,
1179
- });
1308
+ try {
1309
+ getMessageChannel().getSender().sendMessage('accessPropertyRequest', {
1310
+ id,
1311
+ path,
1312
+ });
1313
+ }
1314
+ catch (err) {
1315
+ clearTimeout(timer);
1316
+ pending.delete(id);
1317
+ reject(err);
1318
+ }
1180
1319
  });
1181
1320
  },
1182
1321
  invokeFunctionById(callbackId, args, callbackOptions = {}) {
@@ -1186,35 +1325,77 @@ function createClientContext(getMessageChannel, instanceId, options = {}) {
1186
1325
  return;
1187
1326
  }
1188
1327
  const callId = createId('cbcall');
1189
- const timeout = options.timeout ?? DEFAULT_TIMEOUT;
1190
1328
  const timer = setTimeout(() => {
1191
- pending.delete(callId);
1192
- reject(new Error(`Callback call timed out: ${callbackId}`));
1193
- }, timeout);
1329
+ rejectPending(callId, new Error(`Callback call timed out: ${callbackId}`));
1330
+ }, callTimeout);
1194
1331
  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)),
1201
- });
1332
+ try {
1333
+ getMessageChannel()
1334
+ .getSender()
1335
+ .sendMessage('invokeFunctionByIdRequest', {
1336
+ id: callbackId,
1337
+ callId,
1338
+ args: args.map((arg) => serialize(arg, registerFunction, callbackOptions)),
1339
+ });
1340
+ }
1341
+ catch (err) {
1342
+ clearTimeout(timer);
1343
+ pending.delete(callId);
1344
+ reject(err);
1345
+ }
1202
1346
  });
1203
1347
  },
1204
1348
  handleRemoteDestroy(remoteInstanceId) {
1205
1349
  remoteCallbacksByOwner.delete(remoteInstanceId);
1206
1350
  persistentRemoteCallbacksByOwner.delete(remoteInstanceId);
1207
1351
  },
1352
+ releaseRemoteCallbacks(remoteInstanceId, ids) {
1353
+ const lru = remoteCallbacksByOwner.get(remoteInstanceId);
1354
+ const pmap = persistentRemoteCallbacksByOwner.get(remoteInstanceId);
1355
+ if (!lru && !pmap)
1356
+ return;
1357
+ for (const id of ids) {
1358
+ lru?.delete(id);
1359
+ pmap?.delete(id);
1360
+ }
1361
+ },
1362
+ dropLocalCallback(id) {
1363
+ dropLocalCallback(id);
1364
+ },
1365
+ bindMethod(fn, owner) {
1366
+ return bindMethod(fn, owner);
1367
+ },
1368
+ getRemoteCallbackCounts(remoteInstanceId) {
1369
+ return {
1370
+ lru: remoteCallbacksByOwner.get(remoteInstanceId)?.size ?? 0,
1371
+ persistent: persistentRemoteCallbacksByOwner.get(remoteInstanceId)?.size ?? 0,
1372
+ };
1373
+ },
1208
1374
  destroy(notifyRemote = true) {
1209
1375
  if (destroyed)
1210
1376
  return;
1211
1377
  destroyed = true;
1212
1378
  if (notifyRemote) {
1379
+ const heldRemoteIds = [];
1380
+ for (const lru of remoteCallbacksByOwner.values()) {
1381
+ for (const id of lru.keys())
1382
+ heldRemoteIds.push(id);
1383
+ }
1384
+ for (const map of persistentRemoteCallbacksByOwner.values()) {
1385
+ for (const id of map.keys())
1386
+ heldRemoteIds.push(id);
1387
+ }
1213
1388
  try {
1214
- getMessageChannel().getSender().sendMessage('destroyEndpoint', { instanceId });
1389
+ const sender = getMessageChannel().getSender();
1390
+ if (heldRemoteIds.length > 0) {
1391
+ sender.sendMessage('releaseCallbacks', { ids: heldRemoteIds });
1392
+ }
1393
+ sender.sendMessage('destroyEndpoint', { instanceId });
1215
1394
  }
1216
- catch {
1217
- // channel may already be torn down
1395
+ catch (err) {
1396
+ if (!isExpectedDestroyNotifyError(err)) {
1397
+ warn('destroy', 'failed to notify remote of destroy', err);
1398
+ }
1218
1399
  }
1219
1400
  }
1220
1401
  for (const [id, callbacks] of pending) {
@@ -1224,6 +1405,7 @@ function createClientContext(getMessageChannel, instanceId, options = {}) {
1224
1405
  pending.clear();
1225
1406
  functionCache.clear();
1226
1407
  persistentFunctionCache.clear();
1408
+ persistentRefcount.clear();
1227
1409
  functionIds.clear();
1228
1410
  remoteCallbacksByOwner.clear();
1229
1411
  persistentRemoteCallbacksByOwner.clear();
@@ -1233,6 +1415,9 @@ function createClientContext(getMessageChannel, instanceId, options = {}) {
1233
1415
  function createId(prefix) {
1234
1416
  return `${prefix}-${nanoid()}`;
1235
1417
  }
1418
+ function isExpectedDestroyNotifyError(err) {
1419
+ return err instanceof Error && err.message.includes('contentWindow is not available');
1420
+ }
1236
1421
 
1237
1422
  const EMPTY_TARGET = () => undefined;
1238
1423
  const INTERCEPTED_STRING_PROPS = new Set([
@@ -1355,10 +1540,14 @@ function createEndpoint(options) {
1355
1540
  processorRegistry.register('releaseCallbacks', handleReleaseCallbacks);
1356
1541
  processorRegistry.register('destroyEndpoint', handleDestroyEndpoint);
1357
1542
  const proxy = createClientProxy((path, args) => context.invoke(path, args), (path) => context.accessProperty(path));
1543
+ let destroyed = false;
1358
1544
  return {
1359
1545
  proxy,
1360
1546
  getContext: () => context,
1361
1547
  destroy() {
1548
+ if (destroyed)
1549
+ return;
1550
+ destroyed = true;
1362
1551
  context.destroy(true);
1363
1552
  channel.destroy();
1364
1553
  },
@@ -1367,6 +1556,7 @@ function createEndpoint(options) {
1367
1556
 
1368
1557
  function setupInMainWindow(options) {
1369
1558
  const resolveTargetOrigin = createTargetOriginResolver(() => options.iframe, options.targetOrigin);
1559
+ const allowedOrigin = createAllowedOrigin(options.allowedOrigin, resolveTargetOrigin);
1370
1560
  const poster = createWindowPoster({
1371
1561
  postMessage: (message) => {
1372
1562
  const contentWindow = options.iframe.contentWindow;
@@ -1376,7 +1566,7 @@ function setupInMainWindow(options) {
1376
1566
  contentWindow.postMessage(message, resolveTargetOrigin());
1377
1567
  },
1378
1568
  getExpectedSource: () => options.iframe.contentWindow,
1379
- allowedOrigin: options.allowedOrigin,
1569
+ allowedOrigin,
1380
1570
  });
1381
1571
  const endpoint = createEndpoint({
1382
1572
  poster,
@@ -1399,12 +1589,13 @@ function setupInMainWindow(options) {
1399
1589
  }
1400
1590
  function setupInIframe(options = {}) {
1401
1591
  const resolveTargetOrigin = createParentTargetOriginResolver(options.targetOrigin);
1592
+ const allowedOrigin = createAllowedOrigin(options.allowedOrigin, resolveTargetOrigin);
1402
1593
  const poster = createWindowPoster({
1403
1594
  postMessage: (message) => {
1404
1595
  window.parent.postMessage(message, resolveTargetOrigin());
1405
1596
  },
1406
1597
  getExpectedSource: () => window.parent,
1407
- allowedOrigin: options.allowedOrigin,
1598
+ allowedOrigin,
1408
1599
  });
1409
1600
  const endpoint = createEndpoint({
1410
1601
  poster,
@@ -1439,11 +1630,23 @@ function connectChromeInIframe(options = {}) {
1439
1630
  }
1440
1631
  function createWindowPoster(options) {
1441
1632
  const listenerMap = new Map();
1633
+ const getOrCreatePerName = (name) => {
1634
+ let perName = listenerMap.get(name);
1635
+ if (!perName) {
1636
+ perName = new Map();
1637
+ listenerMap.set(name, perName);
1638
+ }
1639
+ return perName;
1640
+ };
1442
1641
  return {
1443
1642
  postMessage(message) {
1444
1643
  options.postMessage(message);
1445
1644
  },
1446
1645
  addEventListener(name, callback) {
1646
+ const perName = getOrCreatePerName(name);
1647
+ const existing = perName.get(callback);
1648
+ if (existing)
1649
+ window.removeEventListener(name, existing);
1447
1650
  const listener = (event) => {
1448
1651
  const messageEvent = event;
1449
1652
  if (messageEvent.source !== options.getExpectedSource())
@@ -1452,20 +1655,25 @@ function createWindowPoster(options) {
1452
1655
  return;
1453
1656
  callback(messageEvent);
1454
1657
  };
1455
- listenerMap.set(callback, listener);
1658
+ perName.set(callback, listener);
1456
1659
  window.addEventListener(name, listener);
1457
1660
  },
1458
1661
  removeEventListener(name, callback) {
1459
- const listener = listenerMap.get(callback);
1662
+ const perName = listenerMap.get(name);
1663
+ if (!perName)
1664
+ return;
1665
+ const listener = perName.get(callback);
1460
1666
  if (!listener)
1461
1667
  return;
1462
1668
  window.removeEventListener(name, listener);
1463
- listenerMap.delete(callback);
1669
+ perName.delete(callback);
1670
+ if (perName.size === 0)
1671
+ listenerMap.delete(name);
1464
1672
  },
1465
1673
  };
1466
1674
  }
1467
1675
  function isAllowedOrigin(origin, allowedOrigin) {
1468
- if (!allowedOrigin)
1676
+ if (allowedOrigin === undefined)
1469
1677
  return true;
1470
1678
  if (typeof allowedOrigin === 'function')
1471
1679
  return allowedOrigin(origin);
@@ -1473,6 +1681,14 @@ function isAllowedOrigin(origin, allowedOrigin) {
1473
1681
  return allowedOrigin.includes(origin);
1474
1682
  return allowedOrigin === origin;
1475
1683
  }
1684
+ function createAllowedOrigin(explicit, resolveTargetOrigin) {
1685
+ if (explicit !== undefined)
1686
+ return explicit;
1687
+ return (origin) => {
1688
+ const targetOrigin = resolveTargetOrigin();
1689
+ return targetOrigin === '*' || targetOrigin === origin;
1690
+ };
1691
+ }
1476
1692
  function getGlobalChrome() {
1477
1693
  const chromeApi = globalThis.chrome;
1478
1694
  if (!chromeApi) {
@@ -1502,13 +1718,11 @@ function deriveOriginFromIframe(iframe) {
1502
1718
  return loc.origin;
1503
1719
  }
1504
1720
  }
1505
- catch {
1506
- // cross-origin contentWindow.location access throws — fall through
1507
- }
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.');
1721
+ catch (err) {
1722
+ warn('deriveOriginFromIframe', 'unable to access cross-origin contentWindow.location', err);
1511
1723
  }
1724
+ warn('deriveOriginFromIframe', "unable to derive iframe origin automatically; falling back to '*'. " +
1725
+ 'Pass `targetOrigin` explicitly to lock the destination.');
1512
1726
  return '*';
1513
1727
  }
1514
1728
  function deriveParentOrigin() {
@@ -1523,10 +1737,8 @@ function deriveParentOrigin() {
1523
1737
  if (first && first !== 'null')
1524
1738
  return first;
1525
1739
  }
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.');
1529
- }
1740
+ warn('deriveParentOrigin', "unable to derive parent origin automatically; falling back to '*'. " +
1741
+ 'Pass `targetOrigin` explicitly to lock the destination.');
1530
1742
  return '*';
1531
1743
  }
1532
1744
  function parseOrigin(url) {
@@ -1537,10 +1749,11 @@ function parseOrigin(url) {
1537
1749
  if (parsed.origin && parsed.origin !== 'null')
1538
1750
  return parsed.origin;
1539
1751
  }
1540
- catch {
1752
+ catch (err) {
1753
+ warn('parseOrigin', 'failed to parse URL', url, err);
1541
1754
  return null;
1542
1755
  }
1543
1756
  return null;
1544
1757
  }
1545
1758
 
1546
- export { connectChromeInIframe, exposeChromeInIframe, setupInIframe, setupInMainWindow };
1759
+ export { connectChromeInIframe, exposeChromeInIframe, setLogger, setupInIframe, setupInMainWindow };