UNPKG

@sanity/comlink

Version:

A library for one-to-many cross-origin communication between Window contexts, built on the postMessage API.

1,052 lines (1,051 loc) 36.5 kB
import { v4 } from "uuid"; import { fromEventObservable, setup, sendTo, assign, fromCallback, createActor, enqueueActions, raise, emit, assertEvent, stopChild } from "xstate"; import { defer, fromEvent, map, pipe, filter, bufferCount, concatMap, take, EMPTY, takeUntil } from "rxjs"; const listenInputFromContext = (config) => ({ context }) => { const { count, include, exclude, responseType = "message.received" } = config; return { count, domain: context.domain, from: context.connectTo, include: include ? Array.isArray(include) ? include : [include] : [], exclude: exclude ? Array.isArray(exclude) ? exclude : [exclude] : [], responseType, target: context.target, to: context.name }; }, listenFilter = (input) => (event) => { const { data } = event; return (input.include.length ? input.include.includes(data.type) : !0) && (input.exclude.length ? !input.exclude.includes(data.type) : !0) && data.domain === input.domain && data.from === input.from && data.to === input.to && (!input.target || event.source === input.target); }, eventToMessage = (type) => (event) => ({ type, message: event }), messageEvents$ = defer( () => fromEvent(window, "message") ), createListenLogic = (compatMap) => fromEventObservable(({ input }) => messageEvents$.pipe( compatMap ? map(compatMap) : pipe(), filter(listenFilter(input)), map(eventToMessage(input.responseType)), input.count ? pipe( bufferCount(input.count), concatMap((arr) => arr), take(input.count) ) : pipe() )), DOMAIN = "sanity/comlink", RESPONSE_TIMEOUT_DEFAULT = 3e3, FETCH_TIMEOUT_DEFAULT = 1e4, HEARTBEAT_INTERVAL = 1e3, HANDSHAKE_INTERVAL = 500, MSG_RESPONSE = "comlink/response", MSG_HEARTBEAT = "comlink/heartbeat", MSG_DISCONNECT = "comlink/disconnect", MSG_HANDSHAKE_SYN = "comlink/handshake/syn", MSG_HANDSHAKE_SYN_ACK = "comlink/handshake/syn-ack", MSG_HANDSHAKE_ACK = "comlink/handshake/ack", HANDSHAKE_MSG_TYPES = [ MSG_HANDSHAKE_SYN, MSG_HANDSHAKE_SYN_ACK, MSG_HANDSHAKE_ACK ], INTERNAL_MSG_TYPES = [ MSG_RESPONSE, MSG_DISCONNECT, MSG_HEARTBEAT, ...HANDSHAKE_MSG_TYPES ], throwOnEvent = (message) => (source) => source.pipe( take(1), map(() => { throw new Error(message); }) ), createRequestMachine = () => setup({ types: {}, actors: { listen: fromEventObservable( ({ input }) => { const abortSignal$ = input.signal ? fromEvent(input.signal, "abort").pipe( throwOnEvent(`Request ${input.requestId} aborted`) ) : EMPTY, messageFilter = (event) => event.data?.type === MSG_RESPONSE && event.data?.responseTo === input.requestId && !!event.source && input.sources.has(event.source); return fromEvent(window, "message").pipe( filter(messageFilter), take(input.sources.size), takeUntil(abortSignal$) ); } ) }, actions: { "send message": ({ context }, params) => { const { sources, targetOrigin } = context, { message } = params; sources.forEach((source) => { source.postMessage(message, { targetOrigin }); }); }, "on success": sendTo( ({ context }) => context.parentRef, ({ context, self }) => (context.response && context.resolvable?.resolve(context.response), { type: "request.success", requestId: self.id, response: context.response, responseTo: context.responseTo }) ), "on fail": sendTo( ({ context }) => context.parentRef, ({ context, self }) => (context.suppressWarnings || console.warn( `[@sanity/comlink] Received no response to message '${context.type}' on client '${context.from}' (ID: '${context.id}').` ), context.resolvable?.reject(new Error("No response received")), { type: "request.failed", requestId: self.id }) ), "on abort": sendTo( ({ context }) => context.parentRef, ({ context, self }) => (context.resolvable?.reject(new Error("Request aborted")), { type: "request.aborted", requestId: self.id }) ) }, guards: { expectsResponse: ({ context }) => context.expectResponse }, delays: { initialTimeout: 0, responseTimeout: ({ context }) => context.responseTimeout ?? RESPONSE_TIMEOUT_DEFAULT } }).createMachine({ /** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAOlwgBswBiAD1gBd0GwT0AzFgJ2QNwdzoKAFVyowAewCuDItTRY8hUuSoBtAAwBdRKAAOE2P1wT8ukLUQBGAEwBWEgBYAnK+eOAzB7sB2DzY8rABoQAE9rDQc3V0cNTw8fAA4NHwBfVJCFHAJiElgwfAgCKGpNHSQQAyMBU3NLBDsrDxI7DTaAjQA2OOcNDxDwhHsNJx9Ou0TOq2cJxP9HdMyMbOU8gqL8ErUrcv1DY1qK+sbm1vaPLp6+gcRnGydo9wDGycWQLKVc9AB3dGNN6jiWCwdAwMrmKoHMxHRCJRKOEiJHwuZKBZwXKzBMKIGyYkhtAkXOweTqOHw2RJvD45Ug-P4CAH0JgsNicMA8LhwAz4fKicTSWTyZafWm-f5QcEVSE1aGgepwhFIlF9aYYrGDC4+JzEppjGzOUkeGbpDIgfASCBwczU5QQ-YyuqIAC0nRuCBd+IJXu9KSpwppZEoYDt1RMsosiEcNjdVjiJEeGisiSTHkcVgWpptuXyhWKIahjqGzi1BqRJINnVcdkcbuTLS9VYC8ISfsUAbp4vzDphCHJIyjBvJNlxNmRNexQ3sJGH43GPj8jWJrZWuXYfyoEC7YcLsbrgRsjkcvkmdgNbopVhIPhVfnsh8ClMz-tWsCkmEwcHgUvt257u8v+6Hse4xnhOdZnImVidPqCRNB4JqpEAA */ context: ({ input }) => ({ channelId: input.channelId, data: input.data, domain: input.domain, expectResponse: input.expectResponse ?? !1, from: input.from, id: `msg-${v4()}`, parentRef: input.parentRef, resolvable: input.resolvable, response: null, responseTimeout: input.responseTimeout, responseTo: input.responseTo, signal: input.signal, sources: input.sources instanceof Set ? input.sources : /* @__PURE__ */ new Set([input.sources]), suppressWarnings: input.suppressWarnings, targetOrigin: input.targetOrigin, to: input.to, type: input.type }), initial: "idle", on: { abort: ".aborted" }, states: { idle: { after: { initialTimeout: [ { target: "sending" } ] } }, sending: { entry: { type: "send message", params: ({ context }) => { const { channelId, data, domain, from, id, responseTo, to, type } = context; return { message: { channelId, data, domain, from, id, to, type, responseTo } }; } }, always: [ { guard: "expectsResponse", target: "awaiting" }, "success" ] }, awaiting: { invoke: { id: "listen for response", src: "listen", input: ({ context }) => ({ requestId: context.id, sources: context.sources, signal: context.signal }), onError: "aborted" }, after: { responseTimeout: "failed" }, on: { message: { actions: assign({ response: ({ event }) => event.data.data, responseTo: ({ event }) => event.data.responseTo }), target: "success" } } }, failed: { type: "final", entry: "on fail" }, success: { type: "final", entry: "on success" }, aborted: { type: "final", entry: "on abort" } }, output: ({ context, self }) => ({ requestId: self.id, response: context.response, responseTo: context.responseTo }) }), sendBackAtInterval = fromCallback(({ sendBack, input }) => { const send = () => { sendBack(input.event); }; input.immediate && send(); const interval = setInterval(send, input.interval); return () => { clearInterval(interval); }; }), createConnectionMachine = () => setup({ types: {}, actors: { requestMachine: createRequestMachine(), listen: createListenLogic(), sendBackAtInterval }, actions: { "buffer message": enqueueActions(({ enqueue }) => { enqueue.assign({ buffer: ({ event, context }) => (assertEvent(event, "post"), [...context.buffer, event.data]) }), enqueue.emit(({ event }) => (assertEvent(event, "post"), { type: "buffer.added", message: event.data })); }), "create request": assign({ requests: ({ context, event, self, spawn }) => { assertEvent(event, "request"); const requests = (Array.isArray(event.data) ? event.data : [event.data]).map((request) => { const id = `req-${v4()}`; return spawn("requestMachine", { id, input: { channelId: context.channelId, data: request.data, domain: context.domain, expectResponse: request.expectResponse, from: context.name, parentRef: self, responseTo: request.responseTo, sources: context.target, targetOrigin: context.targetOrigin, to: context.connectTo, type: request.type } }); }); return [...context.requests, ...requests]; } }), "emit received message": enqueueActions(({ enqueue }) => { enqueue.emit(({ event }) => (assertEvent(event, "message.received"), { type: "message", message: event.message.data })); }), "emit status": emit((_, params) => ({ type: "status", status: params.status })), "post message": raise(({ event }) => (assertEvent(event, "post"), { type: "request", data: { data: event.data.data, expectResponse: !0, type: event.data.type } })), "remove request": enqueueActions(({ context, enqueue, event }) => { assertEvent(event, ["request.success", "request.failed", "request.aborted"]), stopChild(event.requestId), enqueue.assign({ requests: context.requests.filter(({ id }) => id !== event.requestId) }); }), respond: raise(({ event }) => (assertEvent(event, "response"), { type: "request", data: { data: event.data, type: MSG_RESPONSE, responseTo: event.respondTo } })), "send handshake ack": raise({ type: "request", data: { type: MSG_HANDSHAKE_ACK } }), "send disconnect": raise(() => ({ type: "request", data: { type: MSG_DISCONNECT } })), "send handshake syn": raise({ type: "request", data: { type: MSG_HANDSHAKE_SYN } }), "send pending messages": enqueueActions(({ enqueue }) => { enqueue.raise(({ context }) => ({ type: "request", data: context.buffer.map(({ data, type }) => ({ data, type })) })), enqueue.emit(({ context }) => ({ type: "buffer.flushed", messages: context.buffer })), enqueue.assign({ buffer: [] }); }), "set target": assign({ target: ({ event }) => (assertEvent(event, "target.set"), event.target) }) }, guards: { "has target": ({ context }) => !!context.target, "should send heartbeats": ({ context }) => context.heartbeat } }).createMachine({ /** @xstate-layout N4IgpgJg5mDOIC5QGMAWBDAdpsAbAxAC7oBOMhAdLGIQNoAMAuoqAA4D2sAloV+5ixAAPRAHZRAJgoAWABz0ArHICMy2QGZZCgJwAaEAE9EE+tIrb6ANgkLl46fTuj1AXxf60WHARJgAjgCucJSwAcjIcLAMzEggHNy8-IIiCKLS2hQS6qb2yurisrL6RgjK9LIyCuqq0g7WstZuHhjYePi+gcEUAGboXLiQ0YLxPHwCsSmiCgoykpayDtqS6trqxYjKEk0gnq24FFwQA-jI-DjIdEzDnKNJExuOZpZ12eq29OrSCuupypYUojUaTKCnm5Wk2123gORzA+HilxibBuiXGoBSGnUAIU4gU9FWamUtR+lmUM1EllBEkslMUEnpkJa0JaEFgGAA1lxMFB8LADJghrERqjkhtshk3mTtNo5OpqpYfqCKhTptoqpY1WUtu4dky8BQWWz0Jzue1-EFYIjrgkxqLSupqRRPpoPqJtLI0hIioZENJJE7NnJ8ZYHVk1YyvPrDRyuTyEYLkTa7uixVlMh81KGFhS1j6EPkZlpVjTphr8mkI3sDVhWTHTQBbSLoGAUXwRLgAN0GVyFKNt91KimUFEKXvKC2s9R+6X+jipnzJeSqEJ1UKjNaNJp5EC4sFOrQuCbifeTwg2cgoym0RPxDtqkj0eaB9Ao8zSolMEivZVcq71+33c5CEgeFOCtXskzRM8EDxKRpmkSw3QJbQsmpH5tHmV8JHSbJpDsakV2aSMALOMALhAjoLXAxNbiglI-SxWw1Vw0QNDw0Qfg9KQ7EJSxHHxApK2hQCyOAiAzVgDhMGoI9hX7FMEHSF8cWkelpHURCbBsb481xAEgT9BQJCmWQsiE-URPI8TG1gWBmzAVsyLATtuyRY9ILtWoKmlL82Kqd0tAVJ91LMHFZDKIkVlkNVZHMkiDzE-Adz3UjDx7GiRQHCKnheD53k+HSSkDDIwpBVTqQwuKKEssSDTAUhCAAI3qyg0DIrd8Fkk86MQUMnVM+RynoegTDJH48hGp0vR-FDRqqKqasgOqGua9AQjATAd1NSiul6fpXOtWi7Wy19cslD4vnG7IX3oVjVDUVYEJQqrksW8SdstLqPKy0wKgG1RhtMWogqKhoMjkWp6XxUyFBe3c3tAz70vco6fq+V8PTkGUFzdQqNnELEM2yClrwwzQ4ZShKQJqr7UYU98AS0W9pT4z5pHG0yXwMkNNTyGk3B1TB2AgOBBDXXBDsyhSFG9EovQqN5i1JeRcKqw4Bkl+ToMx8x0j+EaqQ9XMSkBURMgMkEwQWKro2NWNNdPFJAzN0lJGM4slDxhBEJfXyplBd03wW1KxIdnrBxBh4JAyW75C8rJpmDqmIGWkgmpasPjqUcaHooMLHA0uU1UkJOgKW1B6rT1bWor5At0zgcTAkK7hrz1irB0D8cW0UvRPLyv07WqgNq2qAG+l9SnXUz0UOXD5xuMs3Y4+DVJBX7UiKrV6Q8gcfoJO54rFefLLqfJYX1WKYNLxL4NO1NwgA */ id: "connection", context: ({ input }) => ({ id: input.id || `${input.name}-${v4()}`, buffer: [], channelId: `chn-${v4()}`, connectTo: input.connectTo, domain: input.domain ?? DOMAIN, heartbeat: input.heartbeat ?? !1, name: input.name, requests: [], target: input.target, targetOrigin: input.targetOrigin }), on: { "target.set": { actions: "set target" }, "request.success": { actions: "remove request" }, "request.failed": { actions: "remove request" } }, initial: "idle", states: { idle: { entry: [{ type: "emit status", params: { status: "idle" } }], on: { connect: { target: "handshaking", guard: "has target" }, post: { actions: "buffer message" } } }, handshaking: { id: "handshaking", entry: [{ type: "emit status", params: { status: "handshaking" } }], invoke: [ { id: "send syn", src: "sendBackAtInterval", input: () => ({ event: { type: "syn" }, interval: HANDSHAKE_INTERVAL, immediate: !0 }) }, { id: "listen for handshake", src: "listen", input: (input) => listenInputFromContext({ include: MSG_HANDSHAKE_SYN_ACK, count: 1 })(input) /* Below would maybe be more readable than transitioning to 'connected' on 'message', and 'ack' on exit but having onDone when using passing invocations currently breaks XState Editor */ // onDone: { // target: 'connected', // actions: 'ack', // }, } ], on: { syn: { actions: "send handshake syn" }, request: { actions: "create request" }, post: { actions: "buffer message" }, "message.received": { target: "connected" }, disconnect: { target: "disconnected" } }, exit: "send handshake ack" }, connected: { entry: ["send pending messages", { type: "emit status", params: { status: "connected" } }], invoke: { id: "listen for messages", src: "listen", input: listenInputFromContext({ exclude: [MSG_RESPONSE, MSG_HEARTBEAT] }) }, on: { post: { actions: "post message" }, request: { actions: "create request" }, response: { actions: "respond" }, "message.received": { actions: "emit received message" }, disconnect: { target: "disconnected" } }, initial: "heartbeat", states: { heartbeat: { initial: "checking", states: { checking: { always: { guard: "should send heartbeats", target: "sending" } }, sending: { on: { "request.failed": { target: "#handshaking" } }, invoke: { id: "send heartbeat", src: "sendBackAtInterval", input: () => ({ event: { type: "post", data: { type: MSG_HEARTBEAT, data: void 0 } }, interval: 2e3, immediate: !1 }) } } } } } }, disconnected: { id: "disconnected", entry: ["send disconnect", { type: "emit status", params: { status: "disconnected" } }], on: { request: { actions: "create request" }, post: { actions: "buffer message" }, connect: { target: "handshaking", guard: "has target" } } } } }), createConnection = (input, machine = createConnectionMachine()) => { const id = input.id || `${input.name}-${v4()}`, actor = createActor(machine, { input: { ...input, id } }), eventHandlers = /* @__PURE__ */ new Map(), unhandledMessages = /* @__PURE__ */ new Map(), on = (type, handler, options) => { const handlers = eventHandlers.get(type) || /* @__PURE__ */ new Set(); eventHandlers.has(type) || eventHandlers.set(type, handlers), handlers.add(handler); const unhandledMessagesForType = unhandledMessages.get(type); if (unhandledMessagesForType) { const replayCount = options?.replay ?? 1; Array.from(unhandledMessagesForType).slice(-replayCount).forEach(async ({ data, id: id2 }) => { const response = await handler(data); response && actor.send({ type: "response", respondTo: id2, data: response }); }), unhandledMessages.delete(type); } return () => { handlers.delete(handler); }; }, connect = () => { actor.send({ type: "connect" }); }, disconnect = () => { actor.send({ type: "disconnect" }); }, onStatus = (handler, filter2) => { const { unsubscribe } = actor.on("status", (event) => { filter2 && event.status !== filter2 || handler(event.status); }); return unsubscribe; }, setTarget = (target) => { actor.send({ type: "target.set", target }); }, post = (type, data) => { const _data = { type, data }; actor.send({ type: "post", data: _data }); }; actor.on("message", async ({ message }) => { const handlers = eventHandlers.get(message.type); if (handlers) { handlers.forEach(async (handler) => { const response = await handler(message.data); response && actor.send({ type: "response", respondTo: message.id, data: response }); }); return; } const unhandledMessagesForType = unhandledMessages.get(message.type); unhandledMessagesForType ? unhandledMessagesForType.add(message) : unhandledMessages.set(message.type, /* @__PURE__ */ new Set([message])); }); const stop = () => { actor.stop(); }, start = () => (actor.start(), stop); return { actor, connect, disconnect, id, name: input.name, machine, on, onStatus, post, setTarget, start, stop, get target() { return actor.getSnapshot().context.target; } }; }, cleanupConnection = (connection) => { connection.disconnect(), setTimeout(() => { connection.stop(); }, 0); }, noop = () => { }, createController = (input) => { const { targetOrigin } = input, targets = /* @__PURE__ */ new Set(), channels = /* @__PURE__ */ new Set(); return { addTarget: (target) => { if (targets.has(target)) return noop; if (!targets.size || !channels.size) return targets.add(target), channels.forEach((channel) => { channel.connections.forEach((connection) => { connection.setTarget(target), connection.connect(); }); }), () => { targets.delete(target), channels.forEach((channel) => { channel.connections.forEach((connection) => { connection.target === target && connection.disconnect(); }); }); }; targets.add(target); const targetConnections = /* @__PURE__ */ new Set(); return channels.forEach((channel) => { const connection = createConnection( { ...channel.input, target, targetOrigin }, channel.machine ); targetConnections.add(connection), channel.connections.add(connection), channel.subscribers.forEach(({ type, handler, unsubscribers }) => { unsubscribers.push(connection.on(type, handler)); }), channel.internalEventSubscribers.forEach(({ type, handler, unsubscribers }) => { unsubscribers.push(connection.actor.on(type, handler).unsubscribe); }), channel.statusSubscribers.forEach(({ handler, unsubscribers }) => { unsubscribers.push( connection.onStatus((status) => handler({ connection: connection.id, status })) ); }), connection.start(), connection.connect(); }), () => { targets.delete(target), targetConnections.forEach((connection) => { cleanupConnection(connection), channels.forEach((channel) => { channel.connections.delete(connection); }); }); }; }, createChannel: (input2, machine = createConnectionMachine()) => { const channel = { connections: /* @__PURE__ */ new Set(), input: input2, internalEventSubscribers: /* @__PURE__ */ new Set(), machine, statusSubscribers: /* @__PURE__ */ new Set(), subscribers: /* @__PURE__ */ new Set() }; channels.add(channel); const { connections, internalEventSubscribers, statusSubscribers, subscribers } = channel; if (targets.size) targets.forEach((target) => { const connection = createConnection( { ...input2, target, targetOrigin }, machine ); connections.add(connection); }); else { const connection = createConnection({ ...input2, targetOrigin }, machine); connections.add(connection); } const post = (...params) => { const [type, data] = params; connections.forEach((connection) => { connection.post(type, data); }); }, on = (type, handler) => { const unsubscribers = []; connections.forEach((connection) => { unsubscribers.push(connection.on(type, handler)); }); const subscriber = { type, handler, unsubscribers }; return subscribers.add(subscriber), () => { unsubscribers.forEach((unsub) => unsub()), subscribers.delete(subscriber); }; }, onInternalEvent = (type, handler) => { const unsubscribers = []; connections.forEach((connection) => { unsubscribers.push(connection.actor.on(type, handler).unsubscribe); }); const subscriber = { type, handler, unsubscribers }; return internalEventSubscribers.add(subscriber), () => { unsubscribers.forEach((unsub) => unsub()), internalEventSubscribers.delete(subscriber); }; }, onStatus = (handler) => { const unsubscribers = []; connections.forEach((connection) => { unsubscribers.push( connection.onStatus((status) => handler({ connection: connection.id, status })) ); }); const subscriber = { handler, unsubscribers }; return statusSubscribers.add(subscriber), () => { unsubscribers.forEach((unsub) => unsub()), statusSubscribers.delete(subscriber); }; }, stop = () => { const connections2 = channel.connections; connections2.forEach(cleanupConnection), connections2.clear(), channels.delete(channel); }; return { on, onInternalEvent, onStatus, post, start: () => (connections.forEach((connection) => { connection.start(), connection.connect(); }), stop), stop }; }, destroy: () => { channels.forEach(({ connections }) => { connections.forEach(cleanupConnection), connections.clear(); }), channels.clear(), targets.clear(); } }; }; function createPromiseWithResolvers() { if (typeof Promise.withResolvers == "function") return Promise.withResolvers(); let resolve, reject; return { promise: new Promise((res, rej) => { resolve = res, reject = rej; }), resolve, reject }; } const createNodeMachine = () => setup({ types: {}, actors: { requestMachine: createRequestMachine(), listen: createListenLogic() }, actions: { "buffer handshake": assign({ handshakeBuffer: ({ event, context }) => (assertEvent(event, "message.received"), [...context.handshakeBuffer, event]) }), "buffer message": enqueueActions(({ enqueue }) => { enqueue.assign({ buffer: ({ event, context }) => (assertEvent(event, "post"), [ ...context.buffer, { data: event.data, resolvable: event.resolvable, options: event.options } ]) }), enqueue.emit(({ event }) => (assertEvent(event, "post"), { type: "buffer.added", message: event.data })); }), "create request": assign({ requests: ({ context, event, self, spawn }) => { assertEvent(event, "request"); const requests = (Array.isArray(event.data) ? event.data : [event.data]).map((request) => { const id = `req-${v4()}`; return spawn("requestMachine", { id, input: { channelId: context.channelId, data: request.data, domain: context.domain, expectResponse: request.expectResponse, from: context.name, parentRef: self, resolvable: request.resolvable, responseTimeout: request.options?.responseTimeout, responseTo: request.responseTo, signal: request.options?.signal, sources: context.target, suppressWarnings: request.options?.suppressWarnings, targetOrigin: context.targetOrigin, to: context.connectTo, type: request.type } }); }); return [...context.requests, ...requests]; } }), "emit heartbeat": emit(() => ({ type: "heartbeat" })), "emit received message": enqueueActions(({ enqueue }) => { enqueue.emit(({ event }) => (assertEvent(event, "message.received"), { type: "message", message: event.message.data })); }), "emit status": emit((_, params) => ({ type: "status", status: params.status })), "post message": raise(({ event }) => (assertEvent(event, "post"), { type: "request", data: { data: event.data.data, expectResponse: !!event.resolvable, type: event.data.type, resolvable: event.resolvable, options: event.options } })), "process pending handshakes": enqueueActions(({ context, enqueue }) => { context.handshakeBuffer.forEach((event) => enqueue.raise(event)), enqueue.assign({ handshakeBuffer: [] }); }), "remove request": enqueueActions(({ context, enqueue, event }) => { assertEvent(event, ["request.success", "request.failed", "request.aborted"]), stopChild(event.requestId), enqueue.assign({ requests: context.requests.filter(({ id }) => id !== event.requestId) }); }), "send response": raise(({ event }) => (assertEvent(event, ["message.received", "heartbeat.received"]), { type: "request", data: { type: MSG_RESPONSE, responseTo: event.message.data.id, data: void 0 } })), "send handshake syn ack": raise({ type: "request", data: { type: MSG_HANDSHAKE_SYN_ACK } }), "send pending messages": enqueueActions(({ enqueue }) => { enqueue.raise(({ context }) => ({ type: "request", data: context.buffer.map(({ data, resolvable, options }) => ({ data: data.data, type: data.type, expectResponse: !!resolvable, resolvable, options })) })), enqueue.emit(({ context }) => ({ type: "buffer.flushed", messages: context.buffer.map(({ data }) => data) })), enqueue.assign({ buffer: [] }); }), "set connection config": assign({ channelId: ({ event }) => (assertEvent(event, "handshake.syn"), event.message.data.channelId), target: ({ event }) => (assertEvent(event, "handshake.syn"), event.message.source || void 0), targetOrigin: ({ event }) => (assertEvent(event, "handshake.syn"), event.message.origin) }) }, guards: { hasSource: ({ context }) => context.target !== null } }).createMachine({ /** @xstate-layout N4IgpgJg5mDOIC5QDsD2EwGIBOYCOArnAC4B0sBAxpXLANoAMAuoqAA6qwCWxXqyrEAA9EAVgYAWUgEYJDUQA4JAZmUSJC0coDsAGhABPRNIYLSErdOkBOAGzbx227YUBfV-rQYc+IrDIAZgCGXAA2kIwsSCAc3Lz8giIIoiakqgBMDKbp2tYS0srp+kYI0ununuhgpFwQ4ZgQ-NVcyABuqADW1V7NdWAILe2UQfHIkZGCsTx8AtFJ6aKipAzWOtrpC7Z5BUWGiNoK6aS26RLW2tLaqkqqFSA9NX2YALa0QTCkuDRcrRHMk5xpgk5ogJLZSNZIVDoVCFLZiohbIVSLkXLZRHZDgxbHcHrV6rFiBNolNRolEVJbCsdGUzsoyhiEcllOC1DowelVmVrOUPPcqqQABZBZAQWDCjotKANJo1NqdboC4Wi8VBSXIKADeXDUbjf4kwFkkEILbg8RZMHKOzWKzKJkHJa086Xa4qZS4pUisUSqU+QgkYnsQ0zcnJaRLDbpZwKNQSBYspm2MEyC5KTnaDSSd18h7K71q32EwMxYPA0BJFLKY5yZxIrKSURM0RnFHSBTrQqQ9babQejBCr2q9XSiBcWCUfjIMCUIn6oNxEPGtTWFFR0RUy7iGzt+3Ip0XURXVZKPvVCfIKczyB+vyzqLzoGzcuIG0MGTyCztjRtjaJjbHVMNAUTdu1PUhz0vYhryLOcSwXMthBfK0ZGsLQGBZekCi0Jso1IdI23WG04zOE4wIg6coIgBox3Imdi1JRdnxNOxSHNSQkWtW0mTjMxMQ7fDzgcbNKn7WjKJeN4Pi+MAfj+e84MfUMFHbZZwxOHZNDyO09gQOQjmAhZJCM9IMjIycKOvQUwCCbBiAAI2sshpNkiB6NLJ9EIQBQbWOdJlMhYCUjbJkchXGsFmsJQMVsWl3BzKp4GiHoAXgjykgAWmkZZ6xy3LZF2EobCy6xsQWJQ42kE4FjA-EwBSxTjSRUhDgqkzgO2BxdykU4AvXFQ-KjMC8yHKV6qNJi6WOdcypcZsXGxe0JG0XySKjM5lKsMyLwsiAxsYzylDfONznUEqrmi+1ThkHqXDONbULi1wgA */ id: "node", context: ({ input }) => ({ buffer: [], channelId: null, connectTo: input.connectTo, domain: input.domain ?? DOMAIN, handshakeBuffer: [], name: input.name, requests: [], target: void 0, targetOrigin: null }), // Always listen for handshake syn messages. The channel could have // disconnected without being able to notify the node, and so need to // re-establish the connection. invoke: { id: "listen for handshake syn", src: "listen", input: listenInputFromContext({ include: MSG_HANDSHAKE_SYN, responseType: "handshake.syn" }) }, on: { "request.success": { actions: "remove request" }, "request.failed": { actions: "remove request" }, "request.aborted": { actions: "remove request" }, "handshake.syn": { actions: "set connection config", target: ".handshaking" } }, initial: "idle", states: { idle: { entry: [{ type: "emit status", params: { status: "idle" } }], on: { post: { actions: "buffer message" } } }, handshaking: { guard: "hasSource", entry: ["send handshake syn ack", { type: "emit status", params: { status: "handshaking" } }], invoke: [ { id: "listen for handshake ack", src: "listen", input: listenInputFromContext({ include: MSG_HANDSHAKE_ACK, count: 1, // Override the default `message.received` responseType to prevent // buffering the ack message. We transition to the connected state // using onDone instead of listening to this event using `on` responseType: "handshake.complete" }), onDone: "connected" }, { id: "listen for disconnect", src: "listen", input: listenInputFromContext({ include: MSG_DISCONNECT, count: 1, responseType: "disconnect" }) }, { id: "listen for messages", src: "listen", input: listenInputFromContext({ exclude: [ MSG_DISCONNECT, MSG_HANDSHAKE_SYN, MSG_HANDSHAKE_ACK, MSG_HEARTBEAT, MSG_RESPONSE ] }) } ], on: { request: { actions: "create request" }, post: { actions: "buffer message" }, "message.received": { actions: "buffer handshake" }, disconnect: { target: "idle" } } }, connected: { entry: [ "process pending handshakes", "send pending messages", { type: "emit status", params: { status: "connected" } } ], invoke: [ { id: "listen for messages", src: "listen", input: listenInputFromContext({ exclude: [ MSG_DISCONNECT, MSG_HANDSHAKE_SYN, MSG_HANDSHAKE_ACK, MSG_HEARTBEAT, MSG_RESPONSE ] }) }, { id: "listen for heartbeat", src: "listen", input: listenInputFromContext({ include: MSG_HEARTBEAT, responseType: "heartbeat.received" }) }, { id: "listen for disconnect", src: "listen", input: listenInputFromContext({ include: MSG_DISCONNECT, count: 1, responseType: "disconnect" }) } ], on: { request: { actions: "create request" }, post: { actions: "post message" }, disconnect: { target: "idle" }, "message.received": { actions: ["send response", "emit received message"] }, "heartbeat.received": { actions: ["send response", "emit heartbeat"] } } } } }), createNode = (input, machine = createNodeMachine()) => { const actor = createActor(machine, { input }), eventHandlers = /* @__PURE__ */ new Map(), unhandledMessages = /* @__PURE__ */ new Map(), on = (type, handler, options) => { const handlers = eventHandlers.get(type) || /* @__PURE__ */ new Set(); eventHandlers.has(type) || eventHandlers.set(type, handlers), handlers.add(handler); const unhandledMessagesForType = unhandledMessages.get(type); if (unhandledMessagesForType) { const replayCount = options?.replay ?? 1; Array.from(unhandledMessagesForType).slice(-replayCount).forEach(({ data }) => handler(data)), unhandledMessages.delete(type); } return () => { handlers.delete(handler); }; }; let cachedStatus; const onStatus = (handler, filter2) => { const { unsubscribe } = actor.on( "status", (event) => { cachedStatus = event.status, !(filter2 && event.status !== filter2) && handler(event.status); } ); return cachedStatus && handler(cachedStatus), unsubscribe; }, post = (type, data) => { const _data = { type, data }; actor.send({ type: "post", data: _data }); }, fetch = (type, data, options) => { const { responseTimeout = FETCH_TIMEOUT_DEFAULT, signal, suppressWarnings } = options || {}, resolvable = createPromiseWithResolvers(), _data = { type, data }; return actor.send({ type: "post", data: _data, resolvable, options: { responseTimeout, signal, suppressWarnings } }), resolvable.promise; }; actor.on("message", ({ message }) => { const handlers = eventHandlers.get(message.type); if (handlers) { handlers.forEach((handler) => handler(message.data)); return; } const unhandledMessagesForType = unhandledMessages.get(message.type); unhandledMessagesForType ? unhandledMessagesForType.add(message) : unhandledMessages.set(message.type, /* @__PURE__ */ new Set([message])); }); const stop = () => { actor.stop(); }; return { actor, fetch, machine, on, onStatus, post, start: () => (actor.start(), stop), stop }; }; export { DOMAIN, FETCH_TIMEOUT_DEFAULT, HANDSHAKE_INTERVAL, HANDSHAKE_MSG_TYPES, HEARTBEAT_INTERVAL, INTERNAL_MSG_TYPES, MSG_DISCONNECT, MSG_HANDSHAKE_ACK, MSG_HANDSHAKE_SYN, MSG_HANDSHAKE_SYN_ACK, MSG_HEARTBEAT, MSG_RESPONSE, RESPONSE_TIMEOUT_DEFAULT, createConnection, createConnectionMachine, createController, createListenLogic, createNode, createNodeMachine, createRequestMachine }; //# sourceMappingURL=index.js.map