@triplit/client
Version:
287 lines • 12.1 kB
JavaScript
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