UNPKG

@triplit/client

Version:
287 lines 12.1 kB
import * as ComLink from 'comlink'; import { createUpdateProxyAndTrackChanges, EntityNotFoundError, InvalidCollectionNameError, normalizeSessionVars, queryBuilder, } from '@triplit/db'; import { clientLogHandler } from '../client-logger.js'; import { decodeToken } from '../token.js'; export function getTriplitWorkerEndpoint(workerUrl) { const url = workerUrl ?? new URL('worker-client-operator.js', import.meta.url); const options = { type: 'module', name: 'triplit-client' }; const isSharedWorker = typeof SharedWorker !== 'undefined'; return isSharedWorker ? new SharedWorker(url, options).port : new Worker(url, options); // } export function getTriplitSharedWorkerPort(workerUrl) { const url = workerUrl ?? new URL('worker-client-operator.js', import.meta.url); const options = { type: 'module', name: 'triplit-client' }; return new SharedWorker(url, options).port; } export class WorkerClient { initialized; clientWorker; //ComLink.Remote<Client<M>>; _connectionStatus; _vars; constructor(options, workerEndpoint, sharedWorkerPort) { workerEndpoint = workerEndpoint ?? sharedWorkerPort ?? getTriplitWorkerEndpoint(options?.workerUrl); // @ts-expect-error this.clientWorker = ComLink.wrap(workerEndpoint); const { onSessionError, token, refreshOptions, autoConnect, ...remainingOptions } = options || {}; const connectOnInitialization = autoConnect ?? true; if (token) { this.startSession(token, connectOnInitialization, refreshOptions && ComLink.proxy(refreshOptions)); } if (onSessionError) { this.onSessionError(onSessionError); } // @ts-expect-error this.initialized = this.clientWorker.init(remainingOptions, ComLink.proxy(clientLogHandler())); this._connectionStatus = 'UNINITIALIZED'; this.onConnectionStatusChange((status) => { this._connectionStatus = status; }, true); const decoded = decodeToken(token); const sessionVars = decoded ? normalizeSessionVars(decoded) : {}; this._vars = { $global: remainingOptions?.variables ?? {}, $session: sessionVars, $token: sessionVars, }; this.clientWorker.onVariablesChange(ComLink.proxy((vars) => { this._vars = vars; })); } get connectionStatus() { return this._connectionStatus; } query(collectionName) { return queryBuilder(collectionName); } async fetch(query, options) { await this.initialized; return this.clientWorker.fetch(query, options); } async transact(callback, options = {}) { await this.initialized; const client = this; const wrappedTxCallback = async (tx) => { // create a proxy wrapper around TX that intercepts calls to tx.update that // normally takes a callback so instead we wrap with ComLink.proxy const proxiedTx = new Proxy(tx, { get(target, prop) { if (prop === 'update') { return async (collectionName, id, update) => { const changes = await client.getChangesFromUpdatePayload(collectionName, id, update, tx.fetchById.bind(tx)); return await tx.update(collectionName, id, changes); }; } // @ts-expect-error return target[prop]; }, }); return await callback(proxiedTx); }; return this.clientWorker.transact(ComLink.proxy(wrappedTxCallback), options); } // this takes all the requisite info to mock // a proxy on the client side that can be used // to update an entity and then pass the changes // to the worker async getChangesFromUpdatePayload(collectionName, id, update, fetchById = this.fetchById.bind(this)) { if (!collectionName) { throw new InvalidCollectionNameError(collectionName); } let changes = undefined; const collectionSchema = (await this.getSchema())?.collections[collectionName].schema; if (typeof update === 'function') { const existingEntity = structuredClone(await fetchById(collectionName, id)); if (!existingEntity) { throw new EntityNotFoundError(id, collectionName); } changes = {}; await update(createUpdateProxyAndTrackChanges(existingEntity, changes, collectionSchema)); } else { changes = update; } return changes; } async fetchById(collectionName, id, options) { await this.initialized; return this.clientWorker.fetchById(collectionName, id, options); } async fetchOne(query, options) { await this.initialized; return this.clientWorker.fetchOne(query, options); } async insert(collectionName, object) { await this.initialized; return this.clientWorker.insert(collectionName, object); } async update(collectionName, entityId, data) { await this.initialized; const changes = await this.getChangesFromUpdatePayload(collectionName, entityId, data); return await this.clientWorker.update(collectionName, entityId, changes); } async delete(collectionName, entityId) { await this.initialized; return this.clientWorker.delete(collectionName, entityId); } subscribe(query, onResults, onError, options) { const unsubPromise = (async () => { await this.initialized; return this.clientWorker.subscribe(query, ComLink.proxy(onResults), onError && ComLink.proxy(onError), // CURRENTLY ONLY SUPPORTS onRemoteFulfilled // Comlink is having trouble either just proxying the callback // inside options or proxying the whole options object options && ComLink.proxy(options)); })(); return () => { unsubPromise.then((unsub) => unsub()); }; } subscribeWithStatus(query, callback, options) { const unsubPromise = (async () => { await this.initialized; return this.clientWorker.subscribeWithStatus(query, ComLink.proxy(callback), options && ComLink.proxy(options)); })(); return () => { unsubPromise.then((unsub) => unsub()); }; } subscribeBackground(query, options = {}) { const unsubPromise = (async () => { await this.initialized; return this.clientWorker.subscribeBackground(query, ComLink.proxy(options)); })(); return () => { unsubPromise.then((unsub) => unsub()); }; } /** * Subscribe to a query with helpers for pagination * This query will "oversubscribe" by 1 on either side of the current page to determine if there are "next" or "previous" pages * The window generally looks like [buffer, ...page..., buffer] * Depending on the current paging direction, the query may have its original order reversed * * The pagination will also do its best to always return full pages */ subscribeWithPagination(query, onResults, onError, options) { let unsubscribed = false; // @ts-expect-error const onRes = async (results, info) => { if (unsubscribed) return; onResults(results, info); }; const subscriptionPromise = this.initialized.then(() => this.clientWorker.subscribeWithPagination(query, ComLink.proxy(onRes), onError && ComLink.proxy(onError), options && ComLink.proxy(options))); const unsubscribe = () => { unsubscribed = true; subscriptionPromise.then((sub) => { sub.unsubscribe(); }); }; const nextPage = () => { subscriptionPromise.then((sub) => sub.nextPage()); }; const prevPage = () => { subscriptionPromise.then((sub) => sub.prevPage()); }; return { unsubscribe, nextPage, prevPage }; } subscribeWithExpand(query, onResults, onError, options) { const subscriptionPromise = this.initialized.then(() => this.clientWorker.subscribeWithExpand(query, ComLink.proxy(onResults), onError && ComLink.proxy(onError), options && ComLink.proxy(options))); const unsubscribe = () => { subscriptionPromise.then((sub) => sub.unsubscribe()); }; const loadMore = (pageSize) => { subscriptionPromise.then((sub) => sub.loadMore(pageSize)); }; return { loadMore, unsubscribe }; } async getSchema() { await this.initialized; return this.clientWorker.getSchema(); } async updateServerUrl(serverUrl) { await this.initialized; return this.clientWorker.updateServerUrl(serverUrl); } async startSession(...args) { await this.initialized; if (args[2]) args[2] = ComLink.proxy(args[2]); return this.clientWorker.startSession(...args); } async endSession(...args) { await this.initialized; return this.clientWorker.endSession(...args); } // @ts-expect-error TODO async onSessionError(...args) { await this.initialized; return this.clientWorker.onSessionError(ComLink.proxy(args[0])); } async updateSessionToken(...args) { await this.initialized; return this.clientWorker.updateSessionToken(...args); } async updateGlobalVariables(vars) { await this.initialized; return this.clientWorker.updateGlobalVariables(vars); } async isFirstTimeFetchingQuery(query) { await this.initialized; return this.clientWorker.isFirstTimeFetchingQuery(query); } onSyncMessageReceived(...args) { const unSubPromise = this.initialized.then(() => this.clientWorker.onSyncMessageReceived(ComLink.proxy(args[0]))); return () => unSubPromise.then((unsub) => unsub()); } onSyncMessageSent(...args) { const unSubPromise = this.initialized.then(() => this.clientWorker.onSyncMessageSent(ComLink.proxy(args[0]))); return () => unSubPromise.then((unsub) => unsub()); } onEntitySyncSuccess(...args) { const unSubPromise = this.initialized.then(() => this.clientWorker.onEntitySyncSuccess(args[0], args[1], ComLink.proxy(args[2]))); return () => unSubPromise.then((unsub) => unsub()); } onEntitySyncError(...args) { const unSubPromise = this.initialized.then(() => this.clientWorker.onEntitySyncError(args[0], args[1], ComLink.proxy(args[2]))); return () => unSubPromise.then((unsub) => unsub()); } onFailureToSyncWrites(callback) { const unSubPromise = this.initialized.then(() => this.clientWorker.onFailureToSyncWrites(ComLink.proxy(callback))); return () => unSubPromise.then((unsub) => unsub()); } onConnectionStatusChange(callback, runImmediately) { const unSubPromise = this.initialized.then(() => this.clientWorker.onConnectionStatusChange(ComLink.proxy(callback), runImmediately)); return () => unSubPromise.then((unsub) => unsub()); } get vars() { return this._vars; } async connect() { await this.initialized; return this.clientWorker.connect(); } async disconnect() { await this.initialized; return this.clientWorker.disconnect(); } async syncWrites() { await this.initialized; return this.clientWorker.syncWrites(); } async clear(options = {}) { await this.initialized; return this.clientWorker.clear(options); } async reset(options = {}) { await this.initialized; return this.clientWorker.reset(options); } } //# sourceMappingURL=worker-client.js.map