From 730f2af7cbe652c12c9abed1630ea425d61a6310 Mon Sep 17 00:00:00 2001 From: Marton Lederer Date: Mon, 10 Apr 2023 13:44:32 +0200 Subject: [PATCH 1/6] feat: chunk types --- packages/messaging/src/types.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/messaging/src/types.ts b/packages/messaging/src/types.ts index 26827156f..842eb2ab8 100644 --- a/packages/messaging/src/types.ts +++ b/packages/messaging/src/types.ts @@ -82,3 +82,23 @@ export type OriginContext = | "sandbox-page" | "content-script" | "window" + +interface Chunk { + type: "init" | "end" | "data"; + index: number; + chunkCollectionId: number; + data: number[]; +} + +export interface InitChunk extends Chunk { + type: "init"; + totalChunks: number; +} + +export interface DataChunk extends Chunk { + type: "data"; +} + +export interface EndChunk extends Chunk { + type: "end"; +} From bda3d495eb9a95375d67af08b76acc709b637c1c Mon Sep 17 00:00:00 2001 From: Marton Lederer Date: Thu, 13 Apr 2023 15:59:55 +0200 Subject: [PATCH 2/6] feat: finish up chunk builder --- packages/messaging/src/chunks.ts | 70 ++++++++++++++++++++++++++++++++ packages/messaging/src/types.ts | 3 +- 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 packages/messaging/src/chunks.ts diff --git a/packages/messaging/src/chunks.ts b/packages/messaging/src/chunks.ts new file mode 100644 index 000000000..83f79c800 --- /dev/null +++ b/packages/messaging/src/chunks.ts @@ -0,0 +1,70 @@ +import { Chunk, InitChunk } from "./types"; + +const maxChunkSize = 5_000_000; + +export function createChunksFromData(data: unknown): Chunk[] { + // serialize data to buffer + const jsonObj = JSON.stringify(data); + const serialized = new TextEncoder().encode(jsonObj) + + // split serialized data + const bytes: number[][] = [] + + for (let i = 0; i < serialized.length; i++) { + const chunk = Math.floor(i / maxChunkSize) + + if (!bytes[chunk]) bytes[chunk] = [] + + bytes[chunk].push(serialized[i]) + } + + // create a chunk collection ID + const collectionID = Math.floor(Math.random() * 100) + + // create chunks + const chunks: Chunk[] = bytes.map((byteGroup, i) => ({ + type: i === byteGroup.length - 1 ? "end" : (i === 0 ? "init" : "data"), + index: i, + chunkCollectionId: collectionID, + data: byteGroup + })) + + // add total chunk length + const initChunk = chunks.find((chunk) => chunk.type === "init") as InitChunk + + initChunk.totalChunks = chunks.length + initChunk.dataLength = serialized.length; + + return chunks +} + +export function buildDataFromChunks(chunks: Chunk[]): T { + // find the init chunk + const initChunk = chunks.find((chunk) => chunk.type === "init") as InitChunk + + // validate init chunk and check if + // the chunks are complete + if (!initChunk || initChunk.totalChunks !== chunks.length || typeof initChunk.dataLength === "undefined") { + throw new Error("Failed to validate init chunk: incomplete chunk array / no data length / no init chunk") + } + + // initialize the encoded data + const encoded = new Uint8Array(initChunk.dataLength) + + // sort chunks by their index + // this is to make sure we are + // setting the encoded bytes in + // the correct order + chunks.sort((a, b) => a.index - b.index) + + // set bytes + for (let i = 0; i < chunks.length; i++) { + encoded.set(chunks[i].data, chunks[i - 1]?.data?.length || 0) + } + + // decode the data + const serialized = new TextDecoder().decode(encoded) + const obj: T = JSON.parse(serialized) + + return obj +} diff --git a/packages/messaging/src/types.ts b/packages/messaging/src/types.ts index 842eb2ab8..4ed17765a 100644 --- a/packages/messaging/src/types.ts +++ b/packages/messaging/src/types.ts @@ -83,7 +83,7 @@ export type OriginContext = | "content-script" | "window" -interface Chunk { +export interface Chunk { type: "init" | "end" | "data"; index: number; chunkCollectionId: number; @@ -92,6 +92,7 @@ interface Chunk { export interface InitChunk extends Chunk { type: "init"; + dataLength: number; totalChunks: number; } From 5855c6519eac8d727b4ea23de3fa1a90e5e064df Mon Sep 17 00:00:00 2001 From: Marton Lederer Date: Thu, 13 Apr 2023 22:17:02 +0200 Subject: [PATCH 3/6] feat: advanced plasmo port --- packages/messaging/src/chunks.ts | 8 +++ packages/messaging/src/port.ts | 91 +++++++++++++++++++++++++++++++- packages/messaging/src/types.ts | 7 ++- 3 files changed, 104 insertions(+), 2 deletions(-) diff --git a/packages/messaging/src/chunks.ts b/packages/messaging/src/chunks.ts index 83f79c800..fa644ae21 100644 --- a/packages/messaging/src/chunks.ts +++ b/packages/messaging/src/chunks.ts @@ -2,6 +2,10 @@ import { Chunk, InitChunk } from "./types"; const maxChunkSize = 5_000_000; +/** + * Split large data into multiple chunks to + * bypass the browser's limit on runtime messages. + */ export function createChunksFromData(data: unknown): Chunk[] { // serialize data to buffer const jsonObj = JSON.stringify(data); @@ -23,6 +27,7 @@ export function createChunksFromData(data: unknown): Chunk[] { // create chunks const chunks: Chunk[] = bytes.map((byteGroup, i) => ({ + name: "_PLASMO_MESSAGIN_CHUNK", type: i === byteGroup.length - 1 ? "end" : (i === 0 ? "init" : "data"), index: i, chunkCollectionId: collectionID, @@ -38,6 +43,9 @@ export function createChunksFromData(data: unknown): Chunk[] { return chunks } +/** + * Reconstruct split data from "createChunksFromData()" + */ export function buildDataFromChunks(chunks: Chunk[]): T { // find the init chunk const initChunk = chunks.find((chunk) => chunk.type === "init") as InitChunk diff --git a/packages/messaging/src/port.ts b/packages/messaging/src/port.ts index 90d772545..038edf888 100644 --- a/packages/messaging/src/port.ts +++ b/packages/messaging/src/port.ts @@ -1,13 +1,102 @@ +import type { Chunk, ChunkCollectionID, InitChunk, MessageEventCallback } from "./types" +import { buildDataFromChunks, createChunksFromData } from "./chunks" import type { PortName } from "./index" const portMap = new Map() +/** + * Plasmo advanced port extending the default + * chrome.runtime.Port + */ +const PlasmoPort = (name: PortName) => { + // connect to the port + const port = chrome.runtime.connect({ name }) + + // chunk map + const chunkMap = new Map() + + // intercepted event listeners map + // Map format: key - original handler, value - interceptor + const listenerMap = new Map() + + // setup interceptor + const interceptor: chrome.runtime.Port = { + ...port, + postMessage(message: unknown) { + // split chunks + const chunks = createChunksFromData(message) + + // get if chunks are needed + // if not, just send the message + if (chunks.length >= 1) { + return port.postMessage(message) + } + + // send chunks + for (let i = 0; i < chunks.length; i++) { + port.postMessage(chunks[i]) + } + }, + onMessage: { + ...port.onMessage, + addListener(callback: MessageEventCallback) { + // interceptor for the chunks + const interceptor: MessageEventCallback = (message: Chunk, port) => { + // only handle chunks + if (message?.name !== "_PLASMO_MESSAGIN_CHUNK") { + return callback(message, port) + } + + // check if a group exists for this + // chunk in the chunkMap + let group = chunkMap.get(message.chunkCollectionId) + + // if the group exists, add chunk to it + // otherwise create the group + if (!!group) group.push(message) + else chunkMap.set(message.chunkCollectionId, [message]) + + // update group (in case it was undefined before) + group = chunkMap.get(message.chunkCollectionId) + + // check if all chunks have been received + const initChunk = group.find((chunk) => chunk.type === "init") as InitChunk + + if (group.length !== initChunk.totalChunks) return + + // build message data + const data = buildDataFromChunks(group) + + // call original listener to handle + // the reconstructed message + return callback(data, port) + } + + // add listener + port.onMessage.addListener(interceptor) + + // map listener + listenerMap.set(callback, interceptor) + }, + removeListener(callback: MessageEventCallback) { + // remove listener from the original port + port.onMessage.removeListener(listenerMap.get(callback)) + + // remove listener from listener map + listenerMap.delete(callback) + } + } + } + + return interceptor +} + export const getPort = (name: PortName) => { const port = portMap.get(name) if (!!port) { return port } - const newPort = chrome.runtime.connect({ name }) + const newPort = PlasmoPort(name) portMap.set(name, newPort) return newPort } diff --git a/packages/messaging/src/types.ts b/packages/messaging/src/types.ts index 4ed17765a..1644eeb36 100644 --- a/packages/messaging/src/types.ts +++ b/packages/messaging/src/types.ts @@ -83,10 +83,15 @@ export type OriginContext = | "content-script" | "window" +export type ChunkCollectionID = number; + +export type MessageEventCallback = (message: unknown, port: chrome.runtime.Port) => void; + export interface Chunk { + name: "_PLASMO_MESSAGIN_CHUNK" type: "init" | "end" | "data"; index: number; - chunkCollectionId: number; + chunkCollectionId: ChunkCollectionID; data: number[]; } From 780422289ada4dc21a4af912aac9a0dd03a5629c Mon Sep 17 00:00:00 2001 From: Marton Lederer Date: Thu, 13 Apr 2023 22:49:35 +0200 Subject: [PATCH 4/6] fix: check listener map onmessage --- packages/messaging/src/port.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/messaging/src/port.ts b/packages/messaging/src/port.ts index 038edf888..9ba7ab61d 100644 --- a/packages/messaging/src/port.ts +++ b/packages/messaging/src/port.ts @@ -64,6 +64,9 @@ const PlasmoPort = (name: PortName) => { if (group.length !== initChunk.totalChunks) return + // check if the listener is present + if (!listenerMap.get(callback)) return + // build message data const data = buildDataFromChunks(group) From 149bfd1f61fa64475b7e0914be74c3563765557b Mon Sep 17 00:00:00 2001 From: Marton Lederer Date: Fri, 14 Apr 2023 09:36:52 +0200 Subject: [PATCH 5/6] fix: chunk msg name --- packages/messaging/src/chunks.ts | 2 +- packages/messaging/src/port.ts | 2 +- packages/messaging/src/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/messaging/src/chunks.ts b/packages/messaging/src/chunks.ts index fa644ae21..7928a2204 100644 --- a/packages/messaging/src/chunks.ts +++ b/packages/messaging/src/chunks.ts @@ -27,7 +27,7 @@ export function createChunksFromData(data: unknown): Chunk[] { // create chunks const chunks: Chunk[] = bytes.map((byteGroup, i) => ({ - name: "_PLASMO_MESSAGIN_CHUNK", + name: "__PLASMO_MESSAGING_CHUNK__", type: i === byteGroup.length - 1 ? "end" : (i === 0 ? "init" : "data"), index: i, chunkCollectionId: collectionID, diff --git a/packages/messaging/src/port.ts b/packages/messaging/src/port.ts index 9ba7ab61d..a1ff9c133 100644 --- a/packages/messaging/src/port.ts +++ b/packages/messaging/src/port.ts @@ -43,7 +43,7 @@ const PlasmoPort = (name: PortName) => { // interceptor for the chunks const interceptor: MessageEventCallback = (message: Chunk, port) => { // only handle chunks - if (message?.name !== "_PLASMO_MESSAGIN_CHUNK") { + if (message?.name !== "__PLASMO_MESSAGING_CHUNK__") { return callback(message, port) } diff --git a/packages/messaging/src/types.ts b/packages/messaging/src/types.ts index 1644eeb36..75197e175 100644 --- a/packages/messaging/src/types.ts +++ b/packages/messaging/src/types.ts @@ -88,7 +88,7 @@ export type ChunkCollectionID = number; export type MessageEventCallback = (message: unknown, port: chrome.runtime.Port) => void; export interface Chunk { - name: "_PLASMO_MESSAGIN_CHUNK" + name: "__PLASMO_MESSAGING_CHUNK__" type: "init" | "end" | "data"; index: number; chunkCollectionId: ChunkCollectionID; From fa5b8f40eac3d00713fb6a37d3c6ce9309197d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=E2=9D=A4=EF=B8=8F=20=E2=98=AE=EF=B8=8F=20=E2=9C=8B?= <6723574+louisgv@users.noreply.github.com> Date: Wed, 28 Jun 2023 21:33:38 -0700 Subject: [PATCH 6/6] fix up port streaming as optional behavior --- api/messaging/src/chunked-stream.ts | 183 ++++++++++++++++++++++++++++ api/messaging/src/chunks.ts | 84 ------------- api/messaging/src/hook.ts | 12 +- api/messaging/src/index.ts | 3 +- api/messaging/src/port.ts | 123 +++---------------- api/messaging/src/types.ts | 37 ++++-- 6 files changed, 232 insertions(+), 210 deletions(-) create mode 100644 api/messaging/src/chunked-stream.ts delete mode 100644 api/messaging/src/chunks.ts diff --git a/api/messaging/src/chunked-stream.ts b/api/messaging/src/chunked-stream.ts new file mode 100644 index 000000000..31237381e --- /dev/null +++ b/api/messaging/src/chunked-stream.ts @@ -0,0 +1,183 @@ +import type { + Chunk, + ChunkCollectionID, + InitChunk, + MessageEventCallback, + PortName +} from "./types" +import { getExtRuntime } from "./utils" + +const maxChunkSize = 4_200_000 + +/** + * Split large data into multiple chunks to + * bypass the browser's limit on runtime messages. + */ +export function createChunksFromData(data: unknown): Chunk[] { + // serialize data to buffer + const jsonObj = JSON.stringify(data) + const serialized = new TextEncoder().encode(jsonObj) + + // split serialized data + const bytes: number[][] = [] + + for (let i = 0; i < serialized.length; i++) { + const chunk = Math.floor(i / maxChunkSize) + + if (!bytes[chunk]) bytes[chunk] = [] + + bytes[chunk].push(serialized[i]) + } + + // create a chunk collection ID + const collectionID = Math.floor(Math.random() * 100) + + // create chunks + const chunks: Chunk[] = bytes.map((byteGroup, i) => ({ + name: "__PLASMO_MESSAGING_CHUNK__", + type: i === byteGroup.length - 1 ? "end" : i === 0 ? "init" : "data", + index: i, + chunkCollectionId: collectionID, + data: byteGroup + })) + + // add total chunk length + const initChunk = chunks.find((chunk) => chunk.type === "init") as InitChunk + + initChunk.totalChunks = chunks.length + initChunk.dataLength = serialized.length + + return chunks +} + +/** + * Reconstruct split data from "createChunksFromData()" + */ +export function buildDataFromChunks(chunks: Chunk[]): T { + // find the init chunk + const initChunk = chunks.find((chunk) => chunk.type === "init") as InitChunk + + // validate init chunk and check if + // the chunks are complete + if ( + !initChunk || + initChunk.totalChunks !== chunks.length || + typeof initChunk.dataLength === "undefined" + ) { + throw new Error( + "Failed to validate init chunk: incomplete chunk array / no data length / no init chunk" + ) + } + + // initialize the encoded data + const encoded = new Uint8Array(initChunk.dataLength) + + // sort chunks by their index + // this is to make sure we are + // setting the encoded bytes in + // the correct order + chunks.sort((a, b) => a.index - b.index) + + // set bytes + for (let i = 0; i < chunks.length; i++) { + encoded.set(chunks[i].data, chunks[i - 1]?.data?.length || 0) + } + + // decode the data + const serialized = new TextDecoder().decode(encoded) + const obj: T = JSON.parse(serialized) + + return obj +} + +/** + * Advanced chunked streaming port extending the default + * chrome.runtime.Port + */ +export const createChunkedStreamPort = ( + name: PortName +): chrome.runtime.Port => { + // connect to the port + const port = getExtRuntime().connect({ name }) + + // chunk map + const chunkMap = new Map() + + // intercepted event listeners map + // Map format: key - original handler, value - interceptor + const listenerMap = new Map() + + // setup interceptor + return { + ...port, + postMessage(message: unknown) { + // split chunks + const chunks = createChunksFromData(message) + + // get if chunks are needed + // if not, just send the message + if (chunks.length >= 1) { + return port.postMessage(message) + } + + // send chunks + for (let i = 0; i < chunks.length; i++) { + port.postMessage(chunks[i]) + } + }, + onMessage: { + ...port.onMessage, + addListener(callback: MessageEventCallback) { + // interceptor for the chunks + const interceptor: MessageEventCallback = (message: Chunk, port) => { + // only handle chunks + if (message?.name !== "__PLASMO_MESSAGING_CHUNK__") { + return callback(message, port) + } + + // check if a group exists for this + // chunk in the chunkMap + let group = chunkMap.get(message.chunkCollectionId) + + // if the group exists, add chunk to it + // otherwise create the group + if (!!group) group.push(message) + else chunkMap.set(message.chunkCollectionId, [message]) + + // update group (in case it was undefined before) + group = chunkMap.get(message.chunkCollectionId) + + // check if all chunks have been received + const initChunk = group.find( + (chunk) => chunk.type === "init" + ) as InitChunk + + if (group.length !== initChunk.totalChunks) return + + // check if the listener is present + if (!listenerMap.get(callback)) return + + // build message data + const data = buildDataFromChunks(group) + + // call original listener to handle + // the reconstructed message + return callback(data, port) + } + + // add listener + port.onMessage.addListener(interceptor) + + // map listener + listenerMap.set(callback, interceptor) + }, + removeListener(callback: MessageEventCallback) { + // remove listener from the original port + port.onMessage.removeListener(listenerMap.get(callback)) + + // remove listener from listener map + listenerMap.delete(callback) + } + } + } +} diff --git a/api/messaging/src/chunks.ts b/api/messaging/src/chunks.ts deleted file mode 100644 index 75920e0c7..000000000 --- a/api/messaging/src/chunks.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { Chunk, InitChunk } from "./types" - -const maxChunkSize = 5_000_000 - -/** - * Split large data into multiple chunks to - * bypass the browser's limit on runtime messages. - */ -export function createChunksFromData(data: unknown): Chunk[] { - // serialize data to buffer - const jsonObj = JSON.stringify(data) - const serialized = new TextEncoder().encode(jsonObj) - - // split serialized data - const bytes: number[][] = [] - - for (let i = 0; i < serialized.length; i++) { - const chunk = Math.floor(i / maxChunkSize) - - if (!bytes[chunk]) bytes[chunk] = [] - - bytes[chunk].push(serialized[i]) - } - - // create a chunk collection ID - const collectionID = Math.floor(Math.random() * 100) - - // create chunks - const chunks: Chunk[] = bytes.map((byteGroup, i) => ({ - name: "__PLASMO_MESSAGING_CHUNK__", - type: i === byteGroup.length - 1 ? "end" : i === 0 ? "init" : "data", - index: i, - chunkCollectionId: collectionID, - data: byteGroup - })) - - // add total chunk length - const initChunk = chunks.find((chunk) => chunk.type === "init") as InitChunk - - initChunk.totalChunks = chunks.length - initChunk.dataLength = serialized.length - - return chunks -} - -/** - * Reconstruct split data from "createChunksFromData()" - */ -export function buildDataFromChunks(chunks: Chunk[]): T { - // find the init chunk - const initChunk = chunks.find((chunk) => chunk.type === "init") as InitChunk - - // validate init chunk and check if - // the chunks are complete - if ( - !initChunk || - initChunk.totalChunks !== chunks.length || - typeof initChunk.dataLength === "undefined" - ) { - throw new Error( - "Failed to validate init chunk: incomplete chunk array / no data length / no init chunk" - ) - } - - // initialize the encoded data - const encoded = new Uint8Array(initChunk.dataLength) - - // sort chunks by their index - // this is to make sure we are - // setting the encoded bytes in - // the correct order - chunks.sort((a, b) => a.index - b.index) - - // set bytes - for (let i = 0; i < chunks.length; i++) { - encoded.set(chunks[i].data, chunks[i - 1]?.data?.length || 0) - } - - // decode the data - const serialized = new TextDecoder().decode(encoded) - const obj: T = JSON.parse(serialized) - - return obj -} diff --git a/api/messaging/src/hook.ts b/api/messaging/src/hook.ts index 86d5b2ab6..df3b88489 100644 --- a/api/messaging/src/hook.ts +++ b/api/messaging/src/hook.ts @@ -27,18 +27,18 @@ export const useMessage = ( } } -export const usePort: PlasmoMessaging.PortHook = (name) => { +export const usePort: PlasmoMessaging.PortHook = (portKey) => { const portRef = useRef() const reconnectRef = useRef(0) const [data, setData] = useState() useEffect(() => { - if (!name) { + if (!portKey) { return null } const { port, disconnect } = portListen( - name, + portKey, (msg) => { setData(msg) }, @@ -50,7 +50,7 @@ export const usePort: PlasmoMessaging.PortHook = (name) => { portRef.current = port return disconnect }, [ - name, + portKey, reconnectRef.current // This is needed to force a new port ref ]) @@ -58,11 +58,11 @@ export const usePort: PlasmoMessaging.PortHook = (name) => { data, send: (body) => { portRef.current.postMessage({ - name, + name: portKey, body }) }, - listen: (handler) => portListen(name, handler) + listen: (handler) => portListen(portKey, handler) } } diff --git a/api/messaging/src/index.ts b/api/messaging/src/index.ts index 257075c4d..c515ba6cc 100644 --- a/api/messaging/src/index.ts +++ b/api/messaging/src/index.ts @@ -8,7 +8,8 @@ export type { PortName, PortsMetadata, MessagesMetadata, - OriginContext + OriginContext, + PortKey } from "./types" /** diff --git a/api/messaging/src/port.ts b/api/messaging/src/port.ts index 0608e5ce2..0bdab96fe 100644 --- a/api/messaging/src/port.ts +++ b/api/messaging/src/port.ts @@ -1,130 +1,39 @@ -import { buildDataFromChunks, createChunksFromData } from "./chunks" -import type { PortName } from "./index" -import type { - Chunk, - ChunkCollectionID, - InitChunk, - MessageEventCallback -} from "./types" +import { createChunkedStreamPort } from "./chunked-stream" +import type { PortKey, PortName } from "./index" import { getExtRuntime } from "./utils" const portMap = new Map() -/** - * Plasmo advanced port extending the default - * chrome.runtime.Port - */ -const createStreamingPort = (name: PortName) => { - // connect to the port - const port = getExtRuntime().connect({ name }) +export const getPort = (portKey: PortKey) => { + const portName = typeof portKey === "string" ? portKey : portKey.name + const isChunked = typeof portKey !== "string" && portKey.isChunked - // chunk map - const chunkMap = new Map() + const port = portMap.get(portName) - // intercepted event listeners map - // Map format: key - original handler, value - interceptor - const listenerMap = new Map() - - // setup interceptor - const interceptor: chrome.runtime.Port = { - ...port, - postMessage(message: unknown) { - // split chunks - const chunks = createChunksFromData(message) - - // get if chunks are needed - // if not, just send the message - if (chunks.length >= 1) { - return port.postMessage(message) - } - - // send chunks - for (let i = 0; i < chunks.length; i++) { - port.postMessage(chunks[i]) - } - }, - onMessage: { - ...port.onMessage, - addListener(callback: MessageEventCallback) { - // interceptor for the chunks - const interceptor: MessageEventCallback = (message: Chunk, port) => { - // only handle chunks - if (message?.name !== "__PLASMO_MESSAGING_CHUNK__") { - return callback(message, port) - } - - // check if a group exists for this - // chunk in the chunkMap - let group = chunkMap.get(message.chunkCollectionId) - - // if the group exists, add chunk to it - // otherwise create the group - if (!!group) group.push(message) - else chunkMap.set(message.chunkCollectionId, [message]) - - // update group (in case it was undefined before) - group = chunkMap.get(message.chunkCollectionId) - - // check if all chunks have been received - const initChunk = group.find( - (chunk) => chunk.type === "init" - ) as InitChunk - - if (group.length !== initChunk.totalChunks) return - - // check if the listener is present - if (!listenerMap.get(callback)) return - - // build message data - const data = buildDataFromChunks(group) - - // call original listener to handle - // the reconstructed message - return callback(data, port) - } - - // add listener - port.onMessage.addListener(interceptor) - - // map listener - listenerMap.set(callback, interceptor) - }, - removeListener(callback: MessageEventCallback) { - // remove listener from the original port - port.onMessage.removeListener(listenerMap.get(callback)) - - // remove listener from listener map - listenerMap.delete(callback) - } - } - } - - return interceptor -} - -export const getPort = (name: PortName) => { - const port = portMap.get(name) if (!!port) { return port } - const newPort = createStreamingPort(name) - portMap.set(name, newPort) + const newPort = isChunked + ? createChunkedStreamPort(portName) + : getExtRuntime().connect({ name: portName }) + + portMap.set(portName, newPort) return newPort } -export const removePort = (name: PortName) => { - portMap.delete(name) +export const removePort = (portKey: PortKey) => { + portMap.delete(typeof portKey === "string" ? portKey : portKey.name) } export const listen = ( - name: PortName, + portKey: PortKey, handler: (msg: ResponseBody) => Promise | void, onReconnect?: () => void ) => { - const port = getPort(name) + const port = getPort(portKey) function reconnectHandler() { - removePort(name) + removePort(portKey) onReconnect?.() } diff --git a/api/messaging/src/types.ts b/api/messaging/src/types.ts index 9ecb911d8..d984793cb 100644 --- a/api/messaging/src/types.ts +++ b/api/messaging/src/types.ts @@ -92,7 +92,9 @@ export namespace PlasmoMessaging { } export interface PortHook { - , TResponseBody = any>(name: PortName): { + , TResponseBody = any>( + portKey: PortKey + ): { data?: TResponseBody send: (payload: TRequestBody) => void listen: ( @@ -112,28 +114,39 @@ export type OriginContext = | "content-script" | "window" -export type ChunkCollectionID = number; +export type PortKey = + | PortName + | { + name: PortName + // Enable chunking of port data stream. This split the data into smaller chunk and stream them through the port, overcoming the port bandwidth limitation. + isChunked?: boolean + } + +export type ChunkCollectionID = number -export type MessageEventCallback = (message: unknown, port: chrome.runtime.Port) => void; +export type MessageEventCallback = ( + message: unknown, + port: chrome.runtime.Port +) => void export interface Chunk { name: "__PLASMO_MESSAGING_CHUNK__" - type: "init" | "end" | "data"; - index: number; - chunkCollectionId: ChunkCollectionID; - data: number[]; + type: "init" | "end" | "data" + index: number + chunkCollectionId: ChunkCollectionID + data: number[] } export interface InitChunk extends Chunk { - type: "init"; - dataLength: number; - totalChunks: number; + type: "init" + dataLength: number + totalChunks: number } export interface DataChunk extends Chunk { - type: "data"; + type: "data" } export interface EndChunk extends Chunk { - type: "end"; + type: "end" }