@electric-sql/pglite
Version:
PGlite is a WASM Postgres build packaged into a TypeScript client library that enables you to run Postgres in the browser, Node.js and Bun, with no need to install any other dependencies. It is only 3.7mb gzipped.
1 lines • 135 kB
Source Map (JSON)
{"version":3,"sources":["../../src/worker/index.ts","../../../../node_modules/.pnpm/tsup@8.3.0_@microsoft+api-extractor@7.47.7_@types+node@20.16.11__postcss@8.4.47_tsx@4.19.1_typescript@5.6.3/node_modules/tsup/assets/cjs_shims.js","../../src/templating.ts","../../src/types.ts","../../src/parse.ts","../../../pg-protocol/src/string-utils.ts","../../../pg-protocol/src/buffer-writer.ts","../../../pg-protocol/src/serializer.ts","../../../pg-protocol/src/messages.ts","../../../pg-protocol/src/buffer-reader.ts","../../../pg-protocol/src/parser.ts","../../src/errors.ts","../../src/base.ts","../../src/utils.ts"],"sourcesContent":["import type {\n DataTransferContainer,\n DebugLevel,\n ExecProtocolResult,\n Extensions,\n PGliteInterface,\n PGliteInterfaceExtensions,\n PGliteOptions,\n Transaction,\n} from '../interface.js'\nimport type { PGlite } from '../pglite.js'\nimport { BasePGlite } from '../base.js'\nimport { toPostgresName, uuid } from '../utils.js'\nimport { DumpTarCompressionOptions } from '../fs/tarUtils.js'\n\nexport type PGliteWorkerOptions<E extends Extensions = Extensions> =\n PGliteOptions<E> & {\n meta?: any\n id?: string\n }\n\nexport class PGliteWorker\n extends BasePGlite\n implements PGliteInterface, AsyncDisposable\n{\n #initPromise: Promise<void>\n #debug: DebugLevel = 0\n\n #ready = false\n #closed = false\n #isLeader = false\n\n #eventTarget = new EventTarget()\n\n #tabId: string\n\n #connected = false\n\n #workerProcess: Worker\n #workerID?: string\n #workerHerePromise?: Promise<void>\n #workerReadyPromise?: Promise<void>\n\n #broadcastChannel?: BroadcastChannel\n #tabChannel?: BroadcastChannel\n #releaseTabCloseLock?: () => void\n\n #notifyListeners = new Map<string, Set<(payload: string) => void>>()\n #globalNotifyListeners = new Set<(channel: string, payload: string) => void>()\n\n #extensions: Extensions\n #extensionsClose: Array<() => Promise<void>> = []\n\n constructor(worker: Worker, options?: PGliteWorkerOptions) {\n super()\n this.#workerProcess = worker\n this.#tabId = uuid()\n this.#extensions = options?.extensions ?? {}\n\n this.#workerHerePromise = new Promise<void>((resolve) => {\n this.#workerProcess.addEventListener(\n 'message',\n (event) => {\n if (event.data.type === 'here') {\n resolve()\n } else {\n throw new Error('Invalid message')\n }\n },\n { once: true },\n )\n })\n\n this.#workerReadyPromise = new Promise<void>((resolve) => {\n const callback = (event: MessageEvent<any>) => {\n if (event.data.type === 'ready') {\n this.#workerID = event.data.id\n this.#workerProcess.removeEventListener('message', callback)\n resolve()\n }\n }\n this.#workerProcess.addEventListener('message', callback)\n })\n\n this.#initPromise = this.#init(options)\n }\n\n /**\n * Create a new PGlite instance with extensions on the Typescript interface\n * This also awaits the instance to be ready before resolving\n * (The main constructor does enable extensions, however due to the limitations\n * of Typescript, the extensions are not available on the instance interface)\n * @param worker The worker to use\n * @param options Optional options\n * @returns A promise that resolves to the PGlite instance when it's ready.\n */\n static async create<O extends PGliteWorkerOptions>(\n worker: Worker,\n options?: O,\n ): Promise<PGliteWorker & PGliteInterfaceExtensions<O['extensions']>> {\n const pg = new PGliteWorker(worker, options)\n await pg.#initPromise\n return pg as PGliteWorker & PGliteInterfaceExtensions<O['extensions']>\n }\n\n async #init(options: PGliteWorkerOptions = {}) {\n // Setup the extensions\n for (const [extName, ext] of Object.entries(this.#extensions)) {\n if (ext instanceof URL) {\n throw new Error(\n 'URL extensions are not supported on the client side of a worker',\n )\n } else {\n const extRet = await ext.setup(this, {}, true)\n if (extRet.emscriptenOpts) {\n console.warn(\n `PGlite extension ${extName} returned emscriptenOpts, these are not supported on the client side of a worker`,\n )\n }\n if (extRet.namespaceObj) {\n const instance = this as any\n instance[extName] = extRet.namespaceObj\n }\n if (extRet.bundlePath) {\n console.warn(\n `PGlite extension ${extName} returned bundlePath, this is not supported on the client side of a worker`,\n )\n }\n if (extRet.init) {\n await extRet.init()\n }\n if (extRet.close) {\n this.#extensionsClose.push(extRet.close)\n }\n }\n }\n\n // Wait for the worker let us know it's here\n await this.#workerHerePromise\n\n // Send the worker the options\n const { extensions: _, ...workerOptions } = options\n this.#workerProcess.postMessage({\n type: 'init',\n options: workerOptions,\n })\n\n // Wait for the worker let us know it's ready\n await this.#workerReadyPromise\n\n // Acquire the tab close lock, this is released then the tab, or this\n // PGliteWorker instance, is closed\n const tabCloseLockId = `pglite-tab-close:${this.#tabId}`\n this.#releaseTabCloseLock = await acquireLock(tabCloseLockId)\n\n // Start the broadcast channel used to communicate with tabs and leader election\n const broadcastChannelId = `pglite-broadcast:${this.#workerID}`\n this.#broadcastChannel = new BroadcastChannel(broadcastChannelId)\n\n // Start the tab channel used to communicate with the leader directly\n const tabChannelId = `pglite-tab:${this.#tabId}`\n this.#tabChannel = new BroadcastChannel(tabChannelId)\n\n this.#broadcastChannel.addEventListener('message', async (event) => {\n if (event.data.type === 'leader-here') {\n this.#connected = false\n this.#eventTarget.dispatchEvent(new Event('leader-change'))\n this.#leaderNotifyLoop()\n } else if (event.data.type === 'notify') {\n this.#receiveNotification(event.data.channel, event.data.payload)\n }\n })\n\n this.#tabChannel.addEventListener('message', async (event) => {\n if (event.data.type === 'connected') {\n this.#connected = true\n this.#eventTarget.dispatchEvent(new Event('connected'))\n this.#debug = await this.#rpc('getDebugLevel')\n this.#ready = true\n }\n })\n\n this.#workerProcess.addEventListener('message', async (event) => {\n if (event.data.type === 'leader-now') {\n this.#isLeader = true\n this.#eventTarget.dispatchEvent(new Event('leader-change'))\n }\n })\n\n this.#leaderNotifyLoop()\n\n // Init array types\n // We don't await this as it will result in a deadlock\n // It immediately takes out the transaction lock as so another query\n this._initArrayTypes()\n }\n\n async #leaderNotifyLoop() {\n if (!this.#connected) {\n this.#broadcastChannel!.postMessage({\n type: 'tab-here',\n id: this.#tabId,\n })\n setTimeout(() => this.#leaderNotifyLoop(), 16)\n }\n }\n\n async #rpc<Method extends WorkerRpcMethod>(\n method: Method,\n ...args: Parameters<WorkerApi[Method]>\n ): Promise<ReturnType<WorkerApi[Method]>> {\n const callId = uuid()\n const message: WorkerRpcCall<Method> = {\n type: 'rpc-call',\n callId,\n method,\n args,\n }\n this.#tabChannel!.postMessage(message)\n return await new Promise<ReturnType<WorkerApi[Method]>>(\n (resolve, reject) => {\n const listener = (event: MessageEvent) => {\n if (event.data.callId !== callId) return\n cleanup()\n const message: WorkerRpcResponse<Method> = event.data\n if (message.type === 'rpc-return') {\n resolve(message.result)\n } else if (message.type === 'rpc-error') {\n const error = new Error(message.error.message)\n Object.assign(error, message.error)\n reject(error)\n } else {\n reject(new Error('Invalid message'))\n }\n }\n const leaderChangeListener = () => {\n // If the leader changes, throw an error to reject the promise\n cleanup()\n reject(new LeaderChangedError())\n }\n const cleanup = () => {\n this.#tabChannel!.removeEventListener('message', listener)\n this.#eventTarget.removeEventListener(\n 'leader-change',\n leaderChangeListener,\n )\n }\n this.#eventTarget.addEventListener(\n 'leader-change',\n leaderChangeListener,\n )\n this.#tabChannel!.addEventListener('message', listener)\n },\n )\n }\n\n get waitReady() {\n return new Promise<void>((resolve) => {\n this.#initPromise.then(() => {\n if (!this.#connected) {\n resolve(\n new Promise<void>((resolve) => {\n this.#eventTarget.addEventListener('connected', () => {\n resolve()\n })\n }),\n )\n } else {\n resolve()\n }\n })\n })\n }\n\n get debug() {\n return this.#debug\n }\n\n /**\n * The ready state of the database\n */\n get ready() {\n return this.#ready\n }\n\n /**\n * The closed state of the database\n */\n get closed() {\n return this.#closed\n }\n\n /**\n * The leader state of this tab\n */\n get isLeader() {\n return this.#isLeader\n }\n\n /**\n * Close the database\n * @returns Promise that resolves when the connection to shared PGlite is closed\n */\n async close() {\n if (this.#closed) {\n return\n }\n this.#closed = true\n this.#broadcastChannel?.close()\n this.#tabChannel?.close()\n this.#releaseTabCloseLock?.()\n this.#workerProcess.terminate()\n }\n\n /**\n * Close the database when the object exits scope\n * Stage 3 ECMAScript Explicit Resource Management\n * https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#using-declarations-and-explicit-resource-management\n */\n async [Symbol.asyncDispose]() {\n await this.close()\n }\n\n /**\n * Execute a postgres wire protocol message directly without wrapping the response.\n * Only use if `execProtocol()` doesn't suite your needs.\n *\n * **Warning:** This bypasses PGlite's protocol wrappers that manage error/notice messages,\n * transactions, and notification listeners. Only use if you need to bypass these wrappers and\n * don't intend to use the above features.\n *\n * @param message The postgres wire protocol message to execute\n * @returns The direct message data response produced by Postgres\n */\n async execProtocolRaw(message: Uint8Array): Promise<Uint8Array> {\n return (await this.#rpc('execProtocolRaw', message)) as Uint8Array\n }\n\n /**\n * Execute a postgres wire protocol message\n * @param message The postgres wire protocol message to execute\n * @returns The result of the query\n */\n async execProtocol(message: Uint8Array): Promise<ExecProtocolResult> {\n return await this.#rpc('execProtocol', message)\n }\n\n /**\n * Sync the database to the filesystem\n * @returns Promise that resolves when the database is synced to the filesystem\n */\n async syncToFs() {\n await this.#rpc('syncToFs')\n }\n\n /**\n * Listen for a notification\n * @param channel The channel to listen on\n * @param callback The callback to call when a notification is received\n */\n async listen(\n channel: string,\n callback: (payload: string) => void,\n tx?: Transaction,\n ): Promise<() => Promise<void>> {\n const pgChannel = toPostgresName(channel)\n const pg = tx ?? this\n if (!this.#notifyListeners.has(pgChannel)) {\n this.#notifyListeners.set(pgChannel, new Set())\n }\n this.#notifyListeners.get(pgChannel)!.add(callback)\n await pg.exec(`LISTEN ${channel}`)\n return async (tx?: Transaction) => {\n await this.unlisten(pgChannel, callback, tx)\n }\n }\n\n /**\n * Stop listening for a notification\n * @param channel The channel to stop listening on\n * @param callback The callback to remove\n */\n async unlisten(\n channel: string,\n callback?: (payload: string) => void,\n tx?: Transaction,\n ): Promise<void> {\n await this.waitReady\n const pg = tx ?? this\n if (callback) {\n this.#notifyListeners.get(channel)?.delete(callback)\n } else {\n this.#notifyListeners.delete(channel)\n }\n if (this.#notifyListeners.get(channel)?.size === 0) {\n // As we currently have a dedicated worker we can just unlisten\n await pg.exec(`UNLISTEN ${channel}`)\n }\n }\n\n /**\n * Listen to notifications\n * @param callback The callback to call when a notification is received\n */\n onNotification(callback: (channel: string, payload: string) => void) {\n this.#globalNotifyListeners.add(callback)\n return () => {\n this.#globalNotifyListeners.delete(callback)\n }\n }\n\n /**\n * Stop listening to notifications\n * @param callback The callback to remove\n */\n offNotification(callback: (channel: string, payload: string) => void) {\n this.#globalNotifyListeners.delete(callback)\n }\n\n #receiveNotification(channel: string, payload: string) {\n const listeners = this.#notifyListeners.get(channel)\n if (listeners) {\n for (const listener of listeners) {\n queueMicrotask(() => listener(payload))\n }\n }\n for (const listener of this.#globalNotifyListeners) {\n queueMicrotask(() => listener(channel, payload))\n }\n }\n\n async dumpDataDir(\n compression?: DumpTarCompressionOptions,\n ): Promise<File | Blob> {\n return (await this.#rpc('dumpDataDir', compression)) as File | Blob\n }\n\n onLeaderChange(callback: () => void) {\n this.#eventTarget.addEventListener('leader-change', callback)\n return () => {\n this.#eventTarget.removeEventListener('leader-change', callback)\n }\n }\n\n offLeaderChange(callback: () => void) {\n this.#eventTarget.removeEventListener('leader-change', callback)\n }\n\n async _handleBlob(blob?: File | Blob): Promise<void> {\n await this.#rpc('_handleBlob', blob)\n }\n\n async _getWrittenBlob(): Promise<File | Blob | undefined> {\n return await this.#rpc('_getWrittenBlob')\n }\n\n async _cleanupBlob(): Promise<void> {\n await this.#rpc('_cleanupBlob')\n }\n\n async _checkReady() {\n await this.waitReady\n }\n\n async _runExclusiveQuery<T>(fn: () => Promise<T>): Promise<T> {\n await this.#rpc('_acquireQueryLock')\n try {\n return await fn()\n } finally {\n await this.#rpc('_releaseQueryLock')\n }\n }\n\n async _runExclusiveTransaction<T>(fn: () => Promise<T>): Promise<T> {\n await this.#rpc('_acquireTransactionLock')\n try {\n return await fn()\n } finally {\n await this.#rpc('_releaseTransactionLock')\n }\n }\n}\n\nexport interface WorkerOptions {\n init: (options: Exclude<PGliteWorkerOptions, 'extensions'>) => Promise<PGlite>\n}\n\nexport async function worker({ init }: WorkerOptions) {\n // Send a message to the main thread to let it know we are here\n postMessage({ type: 'here' })\n\n // Await the main thread to send us the options\n const options = await new Promise<Exclude<PGliteWorkerOptions, 'extensions'>>(\n (resolve) => {\n addEventListener(\n 'message',\n (event) => {\n if (event.data.type === 'init') {\n resolve(event.data.options)\n }\n },\n { once: true },\n )\n },\n )\n\n // ID for this multi-tab worker - this is used to identify the group of workers\n // that are trying to elect a leader for a shared PGlite instance.\n // It defaults to the URL of the worker, and the dataDir if provided\n // but can be overridden by the options.\n const id = options.id ?? `${import.meta.url}:${options.dataDir ?? ''}`\n\n // Let the main thread know we are ready\n postMessage({ type: 'ready', id })\n\n const electionLockId = `pglite-election-lock:${id}`\n const broadcastChannelId = `pglite-broadcast:${id}`\n const broadcastChannel = new BroadcastChannel(broadcastChannelId)\n const connectedTabs = new Set<string>()\n\n // Await the main lock which is used to elect the leader\n // We don't release this lock, its automatically released when the worker or\n // tab is closed\n await acquireLock(electionLockId)\n\n // Now we are the leader, start the worker\n const dbPromise = init(options)\n\n // Start listening for messages from tabs\n broadcastChannel.onmessage = async (event) => {\n const msg = event.data\n switch (msg.type) {\n case 'tab-here':\n // A new tab has joined,\n connectTab(msg.id, await dbPromise, connectedTabs)\n break\n }\n }\n\n // Notify the other tabs that we are the leader\n broadcastChannel.postMessage({ type: 'leader-here', id })\n\n // Let the main thread know we are the leader\n postMessage({ type: 'leader-now' })\n\n const db = await dbPromise\n\n // Listen for notifications and broadcast them to all tabs\n db.onNotification((channel, payload) => {\n broadcastChannel.postMessage({ type: 'notify', channel, payload })\n })\n}\n\nfunction connectTab(tabId: string, pg: PGlite, connectedTabs: Set<string>) {\n if (connectedTabs.has(tabId)) {\n return\n }\n connectedTabs.add(tabId)\n const tabChannelId = `pglite-tab:${tabId}`\n const tabCloseLockId = `pglite-tab-close:${tabId}`\n const tabChannel = new BroadcastChannel(tabChannelId)\n\n // Use a tab close lock to unsubscribe the tab\n navigator.locks.request(tabCloseLockId, () => {\n return new Promise<void>((resolve) => {\n // The tab has been closed, unsubscribe the tab broadcast channel\n tabChannel.close()\n connectedTabs.delete(tabId)\n resolve()\n })\n })\n\n const api = makeWorkerApi(tabId, pg)\n\n tabChannel.addEventListener('message', async (event) => {\n const msg = event.data\n switch (msg.type) {\n case 'rpc-call': {\n await pg.waitReady\n const { callId, method, args } = msg as WorkerRpcCall<WorkerRpcMethod>\n try {\n // @ts-ignore no apparent reason why it fails\n const result = (await api[method](...args)) as WorkerRpcResult<\n typeof method\n >['result']\n tabChannel.postMessage({\n type: 'rpc-return',\n callId,\n result,\n } satisfies WorkerRpcResult<typeof method>)\n } catch (error) {\n console.error(error)\n tabChannel.postMessage({\n type: 'rpc-error',\n callId,\n error: { message: (error as Error).message },\n } satisfies WorkerRpcError)\n }\n break\n }\n }\n })\n\n // Send a message to the tab to let it know it's connected\n tabChannel.postMessage({ type: 'connected' })\n}\n\nfunction makeWorkerApi(tabId: string, db: PGlite) {\n let queryLockRelease: (() => void) | null = null\n let transactionLockRelease: (() => void) | null = null\n\n // If the tab is closed and it is holding a lock, release the the locks\n // and rollback any pending transactions\n const tabCloseLockId = `pglite-tab-close:${tabId}`\n acquireLock(tabCloseLockId).then(() => {\n if (transactionLockRelease) {\n // rollback any pending transactions\n db.exec('ROLLBACK')\n }\n queryLockRelease?.()\n transactionLockRelease?.()\n })\n\n return {\n async getDebugLevel() {\n return db.debug\n },\n async close() {\n await db.close()\n },\n async execProtocol(message: Uint8Array) {\n const { messages, data } = await db.execProtocol(message)\n if (data.byteLength !== data.buffer.byteLength) {\n const buffer = new ArrayBuffer(data.byteLength)\n const dataCopy = new Uint8Array(buffer)\n dataCopy.set(data)\n return { messages, data: dataCopy }\n } else {\n return { messages, data }\n }\n },\n async execProtocolRaw(\n message: Uint8Array,\n options: { dataTransferContainer?: DataTransferContainer } = {},\n ) {\n const result = await db.execProtocolRaw(message, options)\n if (result.byteLength !== result.buffer.byteLength) {\n // The data is a slice of a larger buffer, this is potentially the whole\n // memory of the WASM module. We copy it to a new Uint8Array and return that.\n const buffer = new ArrayBuffer(result.byteLength)\n const resultCopy = new Uint8Array(buffer)\n resultCopy.set(result)\n return resultCopy\n } else {\n return result\n }\n },\n async dumpDataDir(compression?: DumpTarCompressionOptions) {\n return await db.dumpDataDir(compression)\n },\n async syncToFs() {\n return await db.syncToFs()\n },\n async _handleBlob(blob?: File | Blob) {\n return await db._handleBlob(blob)\n },\n async _getWrittenBlob() {\n return await db._getWrittenBlob()\n },\n async _cleanupBlob() {\n return await db._cleanupBlob()\n },\n async _checkReady() {\n return await db._checkReady()\n },\n async _acquireQueryLock() {\n return new Promise<void>((resolve) => {\n db._runExclusiveQuery(() => {\n return new Promise<void>((release) => {\n queryLockRelease = release\n resolve()\n })\n })\n })\n },\n async _releaseQueryLock() {\n queryLockRelease?.()\n queryLockRelease = null\n },\n async _acquireTransactionLock() {\n return new Promise<void>((resolve) => {\n db._runExclusiveTransaction(() => {\n return new Promise<void>((release) => {\n transactionLockRelease = release\n resolve()\n })\n })\n })\n },\n async _releaseTransactionLock() {\n transactionLockRelease?.()\n transactionLockRelease = null\n },\n }\n}\n\nexport class LeaderChangedError extends Error {\n constructor() {\n super('Leader changed, pending operation in indeterminate state')\n }\n}\n\nasync function acquireLock(lockId: string) {\n let release\n await new Promise<void>((resolve) => {\n navigator.locks.request(lockId, () => {\n return new Promise<void>((releaseCallback) => {\n release = releaseCallback\n resolve()\n })\n })\n })\n return release\n}\n\ntype WorkerApi = ReturnType<typeof makeWorkerApi>\n\ntype WorkerRpcMethod = keyof WorkerApi\n\ntype WorkerRpcCall<Method extends WorkerRpcMethod> = {\n type: 'rpc-call'\n callId: string\n method: Method\n args: Parameters<WorkerApi[Method]>\n}\n\ntype WorkerRpcResult<Method extends WorkerRpcMethod> = {\n type: 'rpc-return'\n callId: string\n result: ReturnType<WorkerApi[Method]>\n}\n\ntype WorkerRpcError = {\n type: 'rpc-error'\n callId: string\n error: any\n}\n\ntype WorkerRpcResponse<Method extends WorkerRpcMethod> =\n | WorkerRpcResult<Method>\n | WorkerRpcError\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () =>\n typeof document === 'undefined'\n ? new URL(`file:${__filename}`).href\n : (document.currentScript && document.currentScript.src) ||\n new URL('main.js', document.baseURI).href\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","const TemplateType = {\n part: 'part',\n container: 'container',\n} as const\n\ninterface TemplatePart {\n _templateType: typeof TemplateType.part\n str: string\n}\n\ninterface TemplateContainer {\n _templateType: typeof TemplateType.container\n strings: TemplateStringsArray\n values: any[]\n}\n\ninterface TemplatedQuery {\n query: string\n params: any[]\n}\n\nfunction addToLastAndPushWithSuffix(\n arr: string[],\n suffix: string,\n ...values: string[]\n) {\n const lastArrIdx = arr.length - 1\n const lastValIdx = values.length - 1\n\n // no-op\n if (lastValIdx === -1) return\n\n // overwrite last element\n if (lastValIdx === 0) {\n arr[lastArrIdx] = arr[lastArrIdx] + values[0] + suffix\n return\n }\n\n // sandwich values between array and suffix\n arr[lastArrIdx] = arr[lastArrIdx] + values[0]\n arr.push(...values.slice(1, lastValIdx))\n arr.push(values[lastValIdx] + suffix)\n}\n\n/**\n * Templating utility that allows nesting multiple SQL strings without\n * losing the automatic parametrization capabilities of {@link query}.\n *\n * @example\n * ```ts\n * query`SELECT * FROM tale ${withFilter ? sql`WHERE foo = ${fooVar}` : sql``}`\n * // > { query: 'SELECT * FROM tale WHERE foo = $1', params: [fooVar] }\n * // or\n * // > { query: 'SELECT * FROM tale', params: [] }\n * ```\n */\nexport function sql(\n strings: TemplateStringsArray,\n ...values: any[]\n): TemplateContainer {\n const parsedStrings = [strings[0]] as string[] & {\n raw: string[]\n }\n parsedStrings.raw = [strings.raw[0]]\n\n const parsedValues: any[] = []\n for (let i = 0; i < values.length; i++) {\n const value = values[i]\n const nextStringIdx = i + 1\n\n // if value is a template tag, collapse into last string\n if (value?._templateType === TemplateType.part) {\n addToLastAndPushWithSuffix(\n parsedStrings,\n strings[nextStringIdx],\n value.str,\n )\n addToLastAndPushWithSuffix(\n parsedStrings.raw,\n strings.raw[nextStringIdx],\n value.str,\n )\n continue\n }\n\n // if value is an output of this method, append in place\n if (value?._templateType === TemplateType.container) {\n addToLastAndPushWithSuffix(\n parsedStrings,\n strings[nextStringIdx],\n ...value.strings,\n )\n addToLastAndPushWithSuffix(\n parsedStrings.raw,\n strings.raw[nextStringIdx],\n ...value.strings.raw,\n )\n parsedValues.push(...value.values)\n continue\n }\n\n // otherwise keep reconstructing\n parsedStrings.push(strings[nextStringIdx])\n parsedStrings.raw.push(strings.raw[nextStringIdx])\n parsedValues.push(value)\n }\n\n return {\n _templateType: 'container',\n strings: parsedStrings,\n values: parsedValues,\n }\n}\n\n/**\n * Allows adding identifiers into a query template string without\n * parametrizing them. This method will automatically escape identifiers.\n *\n * @example\n * ```ts\n * query`SELECT * FROM ${identifier`foo`} WHERE ${identifier`id`} = ${id}`\n * // > { query: 'SELECT * FROM \"foo\" WHERE \"id\" = $1', params: [id] }\n * ```\n */\nexport function identifier(\n strings: TemplateStringsArray,\n ...values: any[]\n): TemplatePart {\n return {\n _templateType: 'part',\n str: `\"${String.raw(strings, ...values)}\"`,\n }\n}\n\n/**\n * Allows adding raw strings into a query template string without\n * parametrizing or modifying them in any way.\n *\n * @example\n * ```ts\n * query`SELECT * FROM foo ${raw`WHERE id = ${2+3}`}`\n * // > { query: 'SELECT * FROM foo WHERE id = 5', params: [] }\n * ```\n */\n\nexport function raw(\n strings: TemplateStringsArray,\n ...values: any[]\n): TemplatePart {\n return {\n _templateType: 'part',\n str: String.raw(strings, ...values),\n }\n}\n\n/**\n * Generates a parametrized query from a templated query string, assigning\n * the provided values to the appropriate named parameters.\n *\n * You can use templating helpers like {@link identifier} and {@link raw} to\n * add identifiers and raw strings to the query without making them parameters,\n * and you can use {@link sql} to nest multiple queries and create utilities.\n *\n * @example\n * ```ts\n * query`SELECT * FROM ${identifier`foo`} WHERE id = ${id} and name = ${name}`\n * // > { query: 'SELECT * FROM \"foo\" WHERE id = $1 and name = $2', params: [id, name] }\n * ```\n */\nexport function query(\n strings: TemplateStringsArray,\n ...values: any[]\n): TemplatedQuery {\n const { strings: queryStringParts, values: params } = sql(strings, ...values)\n return {\n query: [\n queryStringParts[0],\n ...params.flatMap((_, idx) => [`$${idx + 1}`, queryStringParts[idx + 1]]),\n ].join(''),\n params: params,\n }\n}\n","/*\nBased on postgres.js types.js\nhttps://github.com/porsager/postgres/blob/master/src/types.js\nPublished under the Unlicense:\nhttps://github.com/porsager/postgres/blob/master/UNLICENSE \n*/\n\nimport type { ParserOptions } from './interface.js'\n\nconst JSON_parse = globalThis.JSON.parse\nconst JSON_stringify = globalThis.JSON.stringify\n\nexport const BOOL = 16,\n BYTEA = 17,\n CHAR = 18,\n INT8 = 20,\n INT2 = 21,\n INT4 = 23,\n REGPROC = 24,\n TEXT = 25,\n OID = 26,\n TID = 27,\n XID = 28,\n CID = 29,\n JSON = 114,\n XML = 142,\n PG_NODE_TREE = 194,\n SMGR = 210,\n PATH = 602,\n POLYGON = 604,\n CIDR = 650,\n FLOAT4 = 700,\n FLOAT8 = 701,\n ABSTIME = 702,\n RELTIME = 703,\n TINTERVAL = 704,\n CIRCLE = 718,\n MACADDR8 = 774,\n MONEY = 790,\n MACADDR = 829,\n INET = 869,\n ACLITEM = 1033,\n BPCHAR = 1042,\n VARCHAR = 1043,\n DATE = 1082,\n TIME = 1083,\n TIMESTAMP = 1114,\n TIMESTAMPTZ = 1184,\n INTERVAL = 1186,\n TIMETZ = 1266,\n BIT = 1560,\n VARBIT = 1562,\n NUMERIC = 1700,\n REFCURSOR = 1790,\n REGPROCEDURE = 2202,\n REGOPER = 2203,\n REGOPERATOR = 2204,\n REGCLASS = 2205,\n REGTYPE = 2206,\n UUID = 2950,\n TXID_SNAPSHOT = 2970,\n PG_LSN = 3220,\n PG_NDISTINCT = 3361,\n PG_DEPENDENCIES = 3402,\n TSVECTOR = 3614,\n TSQUERY = 3615,\n GTSVECTOR = 3642,\n REGCONFIG = 3734,\n REGDICTIONARY = 3769,\n JSONB = 3802,\n REGNAMESPACE = 4089,\n REGROLE = 4096\n\nexport const types = {\n string: {\n to: TEXT,\n from: [TEXT, VARCHAR, BPCHAR],\n serialize: (x: string | number) => {\n if (typeof x === 'string') {\n return x\n } else if (typeof x === 'number') {\n return x.toString()\n } else {\n throw new Error('Invalid input for string type')\n }\n },\n parse: (x: string) => x,\n },\n number: {\n to: 0,\n from: [INT2, INT4, OID, FLOAT4, FLOAT8],\n serialize: (x: number) => x.toString(),\n parse: (x: string) => +x,\n },\n bigint: {\n to: INT8,\n from: [INT8],\n serialize: (x: bigint) => x.toString(),\n parse: (x: string) => {\n const n = BigInt(x)\n if (n < Number.MIN_SAFE_INTEGER || n > Number.MAX_SAFE_INTEGER) {\n return n // return BigInt\n } else {\n return Number(n) // in range of standard JS numbers so return number\n }\n },\n },\n json: {\n to: JSON,\n from: [JSON, JSONB],\n serialize: (x: any) => {\n if (typeof x === 'string') {\n return x\n } else {\n return JSON_stringify(x)\n }\n },\n parse: (x: string) => JSON_parse(x),\n },\n boolean: {\n to: BOOL,\n from: [BOOL],\n serialize: (x: boolean) => {\n if (typeof x !== 'boolean') {\n throw new Error('Invalid input for boolean type')\n }\n return x ? 't' : 'f'\n },\n parse: (x: string) => x === 't',\n },\n date: {\n to: TIMESTAMPTZ,\n from: [DATE, TIMESTAMP, TIMESTAMPTZ],\n serialize: (x: Date | string | number) => {\n if (typeof x === 'string') {\n return x\n } else if (typeof x === 'number') {\n return new Date(x).toISOString()\n } else if (x instanceof Date) {\n return x.toISOString()\n } else {\n throw new Error('Invalid input for date type')\n }\n },\n parse: (x: string | number) => new Date(x),\n },\n bytea: {\n to: BYTEA,\n from: [BYTEA],\n serialize: (x: Uint8Array) => {\n if (!(x instanceof Uint8Array)) {\n throw new Error('Invalid input for bytea type')\n }\n return (\n '\\\\x' +\n Array.from(x)\n .map((byte) => byte.toString(16).padStart(2, '0'))\n .join('')\n )\n },\n parse: (x: string): Uint8Array => {\n const hexString = x.slice(2)\n return Uint8Array.from({ length: hexString.length / 2 }, (_, idx) =>\n parseInt(hexString.substring(idx * 2, (idx + 1) * 2), 16),\n )\n },\n },\n} satisfies TypeHandlers\n\nexport type Parser = (x: string, typeId?: number) => any\nexport type Serializer = (x: any) => string\n\nexport type TypeHandler = {\n to: number\n from: number | number[]\n serialize: Serializer\n parse: Parser\n}\n\nexport type TypeHandlers = {\n [key: string]: TypeHandler\n}\n\nconst defaultHandlers = typeHandlers(types)\n\nexport const parsers = defaultHandlers.parsers\nexport const serializers = defaultHandlers.serializers\n\nexport function parseType(\n x: string | null,\n type: number,\n parsers?: ParserOptions,\n): any {\n if (x === null) {\n return null\n }\n const handler = parsers?.[type] ?? defaultHandlers.parsers[type]\n if (handler) {\n return handler(x, type)\n } else {\n return x\n }\n}\n\nfunction typeHandlers(types: TypeHandlers) {\n return Object.keys(types).reduce(\n ({ parsers, serializers }, k) => {\n const { to, from, serialize, parse } = types[k]\n serializers[to] = serialize\n serializers[k] = serialize\n parsers[k] = parse\n if (Array.isArray(from)) {\n from.forEach((f) => {\n parsers[f] = parse\n serializers[f] = serialize\n })\n } else {\n parsers[from] = parse\n serializers[from] = serialize\n }\n return { parsers, serializers }\n },\n {\n parsers: {} as {\n [key: number | string]: (x: string, typeId?: number) => any\n },\n serializers: {} as {\n [key: number | string]: Serializer\n },\n },\n )\n}\n\nconst escapeBackslash = /\\\\/g\nconst escapeQuote = /\"/g\n\nfunction arrayEscape(x: string) {\n return x.replace(escapeBackslash, '\\\\\\\\').replace(escapeQuote, '\\\\\"')\n}\n\nexport function arraySerializer(\n xs: any,\n serializer: Serializer | undefined,\n typarray: number,\n): string {\n if (Array.isArray(xs) === false) return xs\n\n if (!xs.length) return '{}'\n\n const first = xs[0]\n // Only _box (1020) has the ';' delimiter for arrays, all other types use the ',' delimiter\n const delimiter = typarray === 1020 ? ';' : ','\n\n if (Array.isArray(first)) {\n return `{${xs.map((x) => arraySerializer(x, serializer, typarray)).join(delimiter)}}`\n } else {\n return `{${xs\n .map((x) => {\n if (x === undefined) {\n x = null\n // TODO: Add an option to specify how to handle undefined values\n }\n return x === null\n ? 'null'\n : '\"' + arrayEscape(serializer ? serializer(x) : x.toString()) + '\"'\n })\n .join(delimiter)}}`\n }\n}\n\nconst arrayParserState = {\n i: 0,\n char: null as string | null,\n str: '',\n quoted: false,\n last: 0,\n p: null as string | null,\n}\n\nexport function arrayParser(x: string, parser: Parser, typarray: number) {\n arrayParserState.i = arrayParserState.last = 0\n return arrayParserLoop(arrayParserState, x, parser, typarray)[0]\n}\n\nfunction arrayParserLoop(\n s: typeof arrayParserState,\n x: string,\n parser: Parser | undefined,\n typarray: number,\n): any[] {\n const xs = []\n // Only _box (1020) has the ';' delimiter for arrays, all other types use the ',' delimiter\n const delimiter = typarray === 1020 ? ';' : ','\n for (; s.i < x.length; s.i++) {\n s.char = x[s.i]\n if (s.quoted) {\n if (s.char === '\\\\') {\n s.str += x[++s.i]\n } else if (s.char === '\"') {\n xs.push(parser ? parser(s.str) : s.str)\n s.str = ''\n s.quoted = x[s.i + 1] === '\"'\n s.last = s.i + 2\n } else {\n s.str += s.char\n }\n } else if (s.char === '\"') {\n s.quoted = true\n } else if (s.char === '{') {\n s.last = ++s.i\n xs.push(arrayParserLoop(s, x, parser, typarray))\n } else if (s.char === '}') {\n s.quoted = false\n s.last < s.i &&\n xs.push(parser ? parser(x.slice(s.last, s.i)) : x.slice(s.last, s.i))\n s.last = s.i + 1\n break\n } else if (s.char === delimiter && s.p !== '}' && s.p !== '\"') {\n xs.push(parser ? parser(x.slice(s.last, s.i)) : x.slice(s.last, s.i))\n s.last = s.i + 1\n }\n s.p = s.char\n }\n s.last < s.i &&\n xs.push(\n parser ? parser(x.slice(s.last, s.i + 1)) : x.slice(s.last, s.i + 1),\n )\n return xs\n}\n","import {\n BackendMessage,\n RowDescriptionMessage,\n DataRowMessage,\n CommandCompleteMessage,\n ParameterDescriptionMessage,\n} from '@electric-sql/pg-protocol/messages'\nimport type { Results, QueryOptions } from './interface.js'\nimport { parseType, type Parser } from './types.js'\n\n/**\n * This function is used to parse the results of either a simple or extended query.\n * https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-SIMPLE-QUERY\n */\nexport function parseResults(\n messages: Array<BackendMessage>,\n defaultParsers: Record<number | string, Parser>,\n options?: QueryOptions,\n blob?: Blob,\n): Array<Results> {\n const resultSets: Results[] = []\n let currentResultSet: Results = { rows: [], fields: [] }\n let affectedRows = 0\n const parsers = { ...defaultParsers, ...options?.parsers }\n\n messages.forEach((message) => {\n switch (message.name) {\n case 'rowDescription': {\n const msg = message as RowDescriptionMessage\n currentResultSet.fields = msg.fields.map((field) => ({\n name: field.name,\n dataTypeID: field.dataTypeID,\n }))\n break\n }\n case 'dataRow': {\n if (!currentResultSet) break\n const msg = message as DataRowMessage\n if (options?.rowMode === 'array') {\n currentResultSet.rows.push(\n msg.fields.map((field, i) =>\n parseType(field, currentResultSet!.fields[i].dataTypeID, parsers),\n ),\n )\n } else {\n // rowMode === \"object\"\n currentResultSet.rows.push(\n Object.fromEntries(\n msg.fields.map((field, i) => [\n currentResultSet!.fields[i].name,\n parseType(\n field,\n currentResultSet!.fields[i].dataTypeID,\n parsers,\n ),\n ]),\n ),\n )\n }\n break\n }\n case 'commandComplete': {\n const msg = message as CommandCompleteMessage\n affectedRows += retrieveRowCount(msg)\n\n resultSets.push({\n ...currentResultSet,\n affectedRows,\n ...(blob ? { blob } : {}),\n })\n\n currentResultSet = { rows: [], fields: [] }\n break\n }\n }\n })\n\n if (resultSets.length === 0) {\n resultSets.push({\n affectedRows: 0,\n rows: [],\n fields: [],\n })\n }\n\n return resultSets\n}\n\nfunction retrieveRowCount(msg: CommandCompleteMessage): number {\n const parts = msg.text.split(' ')\n switch (parts[0]) {\n case 'INSERT':\n return parseInt(parts[2], 10)\n case 'UPDATE':\n case 'DELETE':\n case 'COPY':\n case 'MERGE':\n return parseInt(parts[1], 10)\n default:\n return 0\n }\n}\n\n/** Get the dataTypeIDs from a list of messages, if it's available. */\nexport function parseDescribeStatementResults(\n messages: Array<BackendMessage>,\n): number[] {\n const message = messages.find(\n (msg): msg is ParameterDescriptionMessage =>\n msg.name === 'parameterDescription',\n )\n\n if (message) {\n return message.dataTypeIDs\n }\n\n return []\n}\n","/**\n * Calculates the byte length of a UTF-8 encoded string\n * Adapted from https://stackoverflow.com/a/23329386\n * @param str - UTF-8 encoded string\n * @returns byte length of string\n */\nfunction byteLengthUtf8(str: string): number {\n let byteLength = str.length\n for (let i = str.length - 1; i >= 0; i--) {\n const code = str.charCodeAt(i)\n if (code > 0x7f && code <= 0x7ff) byteLength++\n else if (code > 0x7ff && code <= 0xffff) byteLength += 2\n if (code >= 0xdc00 && code <= 0xdfff) i-- // trail surrogate\n }\n return byteLength\n}\n\nexport { byteLengthUtf8 }\n","import { byteLengthUtf8 } from './string-utils'\n\nexport class Writer {\n #bufferView: DataView\n #offset: number = 5\n\n readonly #littleEndian = false as const\n readonly #encoder = new TextEncoder()\n readonly #headerPosition: number = 0\n constructor(private size = 256) {\n this.#bufferView = this.#allocateBuffer(size)\n }\n\n #allocateBuffer(size: number): DataView {\n return new DataView(new ArrayBuffer(size))\n }\n\n #ensure(size: number): void {\n const remaining = this.#bufferView.byteLength - this.#offset\n if (remaining < size) {\n const oldBuffer = this.#bufferView.buffer\n // exponential growth factor of around ~ 1.5\n // https://stackoverflow.com/questions/2269063/buffer-growth-strategy\n const newSize = oldBuffer.byteLength + (oldBuffer.byteLength >> 1) + size\n this.#bufferView = this.#allocateBuffer(newSize)\n new Uint8Array(this.#bufferView.buffer).set(new Uint8Array(oldBuffer))\n }\n }\n\n public addInt32(num: number): Writer {\n this.#ensure(4)\n this.#bufferView.setInt32(this.#offset, num, this.#littleEndian)\n this.#offset += 4\n return this\n }\n\n public addInt16(num: number): Writer {\n this.#ensure(2)\n this.#bufferView.setInt16(this.#offset, num, this.#littleEndian)\n this.#offset += 2\n return this\n }\n\n public addCString(string: string): Writer {\n if (string) {\n // TODO(msfstef): might be faster to extract `addString` code and\n // ensure length + 1 once rather than length and then +1?\n this.addString(string)\n }\n\n // set null terminator\n this.#ensure(1)\n this.#bufferView.setUint8(this.#offset, 0)\n this.#offset++\n return this\n }\n\n public addString(string: string = ''): Writer {\n const length = byteLengthUtf8(string)\n this.#ensure(length)\n this.#encoder.encodeInto(\n string,\n new Uint8Array(this.#bufferView.buffer, this.#offset),\n )\n this.#offset += length\n return this\n }\n\n public add(otherBuffer: ArrayBuffer): Writer {\n this.#ensure(otherBuffer.byteLength)\n new Uint8Array(this.#bufferView.buffer).set(\n new Uint8Array(otherBuffer),\n this.#offset,\n )\n\n this.#offset += otherBuffer.byteLength\n return this\n }\n\n #join(code?: number): ArrayBuffer {\n if (code) {\n this.#bufferView.setUint8(this.#headerPosition, code)\n // length is everything in this packet minus the code\n const length = this.#offset - (this.#headerPosition + 1)\n this.#bufferView.setInt32(\n this.#headerPosition + 1,\n length,\n this.#littleEndian,\n )\n }\n return this.#bufferView.buffer.slice(code ? 0 : 5, this.#offset)\n }\n\n public flush(code?: number): Uint8Array {\n const result = this.#join(code)\n this.#offset = 5\n this.#bufferView = this.#allocateBuffer(this.size)\n return new Uint8Array(result)\n }\n}\n","import { Writer } from './buffer-writer'\nimport { byteLengthUtf8 } from './string-utils'\n\nconst enum code {\n startup = 0x70,\n query = 0x51,\n parse = 0x50,\n bind = 0x42,\n execute = 0x45,\n flush = 0x48,\n sync = 0x53,\n end = 0x58,\n close = 0x43,\n describe = 0x44,\n copyFromChunk = 0x64,\n copyDone = 0x63,\n copyFail = 0x66,\n}\n\ntype LegalValue = string | ArrayBuffer | ArrayBufferView | null\n\nconst writer = new Writer()\n\nconst startup = (opts: Record<string, string>): Uint8Array => {\n // protocol version\n writer.addInt16(3).addInt16(0)\n for (const key of Object.keys(opts)) {\n writer.addCString(key).addCString(opts[key])\n }\n\n writer.addCString('client_encoding').addCString('UTF8')\n\n const bodyBuffer = writer.addCString('').flush()\n // this message is sent without a code\n\n const length = bodyBuffer.byteLength + 4\n\n return new Writer().addInt32(length).add(bodyBuffer).flush()\n}\n\nconst requestSsl = (): Uint8Array => {\n const bufferView = new DataView(new ArrayBuffer(8))\n bufferView.setInt32(0, 8, false)\n bufferView.setInt32(4, 80877103, false)\n return new Uint8Array(bufferView.buffer)\n}\n\nconst password = (password: string): Uint8Array => {\n return writer.addCString(password).flush(code.startup)\n}\n\nconst sendSASLInitialResponseMessage = (\n mechanism: string,\n initialResponse: string,\n): Uint8Array => {\n // 0x70 = 'p'\n writer\n .addCString(mechanism)\n .addInt32(byteLengthUtf8(initialResponse))\n .addString(initialResponse)\n\n return writer.flush(code.startup)\n}\n\nconst sendSCRAMClientFinalMessage = (additionalData: string): Uint8Array => {\n return writer.addString(additionalData).flush(code.startup)\n}\n\nconst query = (text: string): Uint8Array => {\n return writer.addCString(text).flush(code.query)\n}\n\ntype ParseOpts = {\n name?: string\n types?: number[]\n text: string\n}\n\nconst emptyValueArray: LegalValue[] = []\n\nconst parse = (query: ParseOpts): Uint8Array => {\n // expect something like this:\n // { name: 'queryName',\n // text: 'select * from blah',\n // types: ['int8', 'bool'] }\n\n // normalize missing query names to allow for null\n const name = query.name ?? ''\n if (name.length > 63) {\n /* eslint-disable no-console */\n console.error(\n 'Warning! Postgres only supports 63 characters for query names.',\n )\n console.error('You supplied %s (%s)', name, name.length)\n console.error(\n 'This can cause conflicts and silent errors executing queries',\n )\n /* eslint-enable no-console */\n }\n\n const buffer = writer\n .addCString(name) // name of query\n .addCString(query.text) // actual query text\n .addInt16(query.types?.length ?? 0)\n\n query.types?.forEach((type) => buffer.addInt32(type))\n\n return writer.flush(code.parse)\n}\n\ntype ValueMapper = (param: unknown, index: number) => LegalValue\n\ntype BindOpts = {\n portal?: string\n binary?: boolean\n statement?: string\n values?: LegalValue[]\n // optional map from JS value to postgres value per parameter\n valueMapper?: ValueMapper\n}\n\nconst paramWriter = new Writer()\n\n// make this a const enum so typescript will inline the value\nconst enum ParamType {\n STRING = 0,\n BINARY = 1,\n}\n\nconst writeValues = (values: LegalValue[], valueMapper?: ValueMapper): void => {\n for (let i = 0; i < values.length; i++) {\n const mappedVal = valueMapper ? valueMapper(values[i], i) : values[i]\n if (mappedVal === null) {\n // add the param type (string) to the writer\n writer.addInt16(ParamType.STRING)\n // write -1 to the param writer to indicate null\n paramWriter.addInt32(-1)\n } else if (\n mappedVal instanceof ArrayBuffer ||\n ArrayBuffer.isView(mappedVal)\n ) {\n const buffer = ArrayBuffer.isView(mappedVal)\n ? mappedVal.buffer.slice(\n mappedVal.byteOffset,\n mappedVal.byteOffset + mappedVal.byteLength,\n )\n : mappedVal\n // add the param type (binary) to the writer\n writer.addInt16(ParamType.BINARY)\n // add the buffer to the param writer\n paramWriter.addInt32(buffer.byteLength)\n paramWriter.add(buffer)\n } else {\n // add the param type (string) to the writer\n writer.addInt16(ParamType.STRING)\n paramWriter.addInt32(byteLengthUtf8(mappedVal))\n paramWriter.addString(mappedVal)\n }\n }\n}\n\nconst bind = (config: BindOpts = {}): Uint8Array => {\n // normalize config\n const portal = config.portal ?? ''\n const statement = config.statement ?? ''\n const binary = config.binary ?? false\n const values = config.values ?? emptyValueArray\n const len = values.length\n\n writer.addCString(portal).addCString(statement)\n writer.addInt16(len)\n\n writeValues(values, config.valueMapper)\n\n writer.addInt16(len)\n writer.add(paramWriter.flush())\n\n // format code\n writer.addInt16(binary ? ParamType.BINARY : ParamType.STRING)\n return writer.flush(code.bind)\n}\n\ntype ExecOpts = {\n portal?: string\n rows?: number\n}\n\nconst emptyExecute = new Uint8Array([\n code.execute,\n 0x00,\n 0x00,\n 0x00,\n 0x09,\n 0x00,\n 0x00,\n 0x00,\n 0x00,\n 0x00,\n])\n\nconst execute = (config?: ExecOpts): Uint8Array => {\n // this is the happy path for most queries\n if (!config || (!config.portal && !config.rows)) {\n return emptyExecute\n }\n\n const portal = config.portal ?? ''\n const rows = config.rows ?? 0\n\n const portalLength = byteLengthUtf8(portal)\n const len = 4 + portalLength + 1 + 4\n // one extra bit for code\n const bufferView = new DataView(new ArrayBuffer(1 + len))\n bufferView.setUint8(0, code.execute)\n bufferView.setInt32(1, len, false)\n new TextEncoder().encodeInto(portal, new Uint8Array(bufferView.buffer, 5))\n bufferView.setUint8(portalLength + 5, 0) // null terminate portal cString\n bufferView.setUint32(bufferView.byteLength - 4, rows, false)\n return new Uint8Array(bufferView.buffer)\n}\n\nconst cancel = (processID: number, secretKey: number): Uint8Array => {\n const bufferView = new DataView(new ArrayBuffer(16))\n bufferView.setInt32(0, 16, false)\n bufferView.setInt16(4, 1234, false)\n bufferView.setInt16(6, 5678, false)\n bufferView.setInt32(8, processID, false)\n bufferView.setInt32(12