xdrpc.js 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. /**
  2. * This module provides a simple RPC mechanism for cross-document
  3. * (iframe / window.postMessage) communication.
  4. */
  5. // Since `Symbol` is not clonable, we use a UUID to identify RPCs.
  6. const $SCOPE = '9a9c83a4-7897-43a0-93b9-53217b84fde6';
  7. /**
  8. * The CallbackManager is used to manage callbacks for RPCs.
  9. * It is used by the dehydrator and hydrator to store and retrieve
  10. * the functions that are being called remotely.
  11. */
  12. export class CallbackManager {
  13. #messageId = 0;
  14. constructor () {
  15. this.callbacks = new Map();
  16. }
  17. register_callback (callback) {
  18. const id = this.#messageId++;
  19. this.callbacks.set(id, callback);
  20. return id;
  21. }
  22. attach_to_source (source) {
  23. source.addEventListener('message', event => {
  24. const { data } = event;
  25. console.log(
  26. 'test-app got message from window',
  27. data,
  28. );
  29. debugger;
  30. if (data && typeof data === 'object' && data.$SCOPE === $SCOPE) {
  31. const { id, args } = data;
  32. const callback = this.callbacks.get(id);
  33. if (callback) {
  34. callback(...args);
  35. }
  36. }
  37. });
  38. }
  39. }
  40. /**
  41. * The dehydrator replaces functions in an object with identifiers,
  42. * so that hydrate() can be called on the other side of the frame
  43. * to bind RPC stubs. The original functions are stored in a map
  44. * so that they can be called when the RPC is invoked.
  45. */
  46. export class Dehydrator {
  47. constructor ({ callbackManager }) {
  48. this.callbackManager = callbackManager;
  49. }
  50. dehydrate (value) {
  51. return this.dehydrate_value_(value);
  52. }
  53. dehydrate_value_ (value) {
  54. if (typeof value === 'function') {
  55. const id = this.callbackManager.register_callback(value);
  56. return { $SCOPE, id };
  57. } else if (Array.isArray(value)) {
  58. return value.map(this.dehydrate_value_.bind(this));
  59. } else if (typeof value === 'object' && value !== null) {
  60. const result = {};
  61. for (const key in value) {
  62. result[key] = this.dehydrate_value_(value[key]);
  63. }
  64. return result;
  65. } else {
  66. return value;
  67. }
  68. }
  69. }
  70. /**
  71. * The hydrator binds RPC stubs to the functions that were
  72. * previously dehydrated. This allows the RPC to be invoked
  73. * on the other side of the frame.
  74. */
  75. export class Hydrator {
  76. constructor ({ target }) {
  77. this.target = target;
  78. }
  79. hydrate (value) {
  80. return this.hydrate_value_(value);
  81. }
  82. hydrate_value_ (value) {
  83. if (
  84. value && typeof value === 'object' &&
  85. value.$SCOPE === $SCOPE
  86. ) {
  87. const { id } = value;
  88. return (...args) => {
  89. console.log('sending message', { $SCOPE, id, args });
  90. console.log('target', this.target);
  91. this.target.postMessage({ $SCOPE, id, args }, '*');
  92. };
  93. } else if (Array.isArray(value)) {
  94. return value.map(this.hydrate_value_.bind(this));
  95. } else if (typeof value === 'object' && value !== null) {
  96. const result = {};
  97. for (const key in value) {
  98. result[key] = this.hydrate_value_(value[key]);
  99. }
  100. return result;
  101. }
  102. return value;
  103. }
  104. }