UNPKG

@liveblocks/core

Version:

Private internals for Liveblocks. DO NOT import directly from this package!

1,820 lines (1,798 loc) 331 kB
var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/version.ts var PKG_NAME = "@liveblocks/core"; var PKG_VERSION = "3.4.0"; var PKG_FORMAT = "esm"; // src/dupe-detection.ts var g = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : {}; var crossLinkedDocs = "https://liveblocks.io/docs/errors/cross-linked"; var dupesDocs = "https://liveblocks.io/docs/errors/dupes"; var SPACE = " "; function error(msg) { if (process.env.NODE_ENV === "production") { console.error(msg); } else { throw new Error(msg); } } function detectDupes(pkgName, pkgVersion, pkgFormat) { const pkgId = Symbol.for(pkgName); const pkgBuildInfo = pkgFormat ? `${pkgVersion || "dev"} (${pkgFormat})` : pkgVersion || "dev"; if (!g[pkgId]) { g[pkgId] = pkgBuildInfo; } else if (g[pkgId] === pkgBuildInfo) { } else { const msg = [ `Multiple copies of Liveblocks are being loaded in your project. This will cause issues! See ${dupesDocs + SPACE}`, "", "Conflicts:", `- ${pkgName} ${g[pkgId]} (already loaded)`, `- ${pkgName} ${pkgBuildInfo} (trying to load this now)` ].join("\n"); error(msg); } if (pkgVersion && PKG_VERSION && pkgVersion !== PKG_VERSION) { error( [ `Cross-linked versions of Liveblocks found, which will cause issues! See ${crossLinkedDocs + SPACE}`, "", "Conflicts:", `- ${PKG_NAME} is at ${PKG_VERSION}`, `- ${pkgName} is at ${pkgVersion}`, "", "Always upgrade all Liveblocks packages to the same version number." ].join("\n") ); } } // src/lib/EventSource.ts function makeEventSource() { const _observers = /* @__PURE__ */ new Set(); function subscribe(callback) { _observers.add(callback); return () => _observers.delete(callback); } function subscribeOnce(callback) { const unsub = subscribe((event) => { unsub(); return callback(event); }); return unsub; } async function waitUntil(predicate) { let unsub; return new Promise((res) => { unsub = subscribe((event) => { if (predicate === void 0 || predicate(event)) { res(event); } }); }).finally(() => unsub?.()); } function notify(event) { let called = false; for (const callback of _observers) { callback(event); called = true; } return called; } function count() { return _observers.size; } return { // Private/internal control over event emission notify, subscribe, subscribeOnce, count, waitUntil, dispose() { _observers.clear(); }, // Publicly exposable subscription API observable: { subscribe, subscribeOnce, waitUntil } }; } function makeBufferableEventSource() { const eventSource2 = makeEventSource(); let _buffer = null; function pause() { _buffer = []; } function unpause() { if (_buffer === null) { return; } for (const event of _buffer) { eventSource2.notify(event); } _buffer = null; } function notifyOrBuffer(event) { if (_buffer !== null) { _buffer.push(event); return false; } else { return eventSource2.notify(event); } } return { ...eventSource2, notify: notifyOrBuffer, pause, unpause, dispose() { eventSource2.dispose(); if (_buffer !== null) { _buffer.length = 0; } } }; } // src/lib/freeze.ts var freeze = process.env.NODE_ENV === "production" ? ( /* istanbul ignore next */ (x) => x ) : Object.freeze; // src/lib/utils.ts function raise(msg) { throw new Error(msg); } function entries(obj) { return Object.entries(obj); } function keys(obj) { return Object.keys(obj); } function values(obj) { return Object.values(obj); } function create(obj, descriptors) { if (typeof descriptors !== "undefined") { return Object.create(obj, descriptors); } return Object.create(obj); } function mapValues(obj, mapFn) { const result = {}; for (const pair of Object.entries(obj)) { const key = pair[0]; if (key === "__proto__") { continue; } const value = pair[1]; result[key] = mapFn(value, key); } return result; } function tryParseJson(rawMessage) { try { return JSON.parse(rawMessage); } catch (e) { return void 0; } } function deepClone(value) { return JSON.parse(JSON.stringify(value)); } function b64decode(b64value) { try { const formattedValue = b64value.replace(/-/g, "+").replace(/_/g, "/"); const decodedValue = decodeURIComponent( atob(formattedValue).split("").map(function(c) { return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); }).join("") ); return decodedValue; } catch (err) { return atob(b64value); } } function compact(items) { return items.filter( (item) => item !== null && item !== void 0 ); } function compactObject(obj) { const newObj = { ...obj }; Object.keys(obj).forEach((k) => { const key = k; if (newObj[key] === void 0) { delete newObj[key]; } }); return newObj; } function wait(millis) { return new Promise((res) => setTimeout(res, millis)); } async function withTimeout(promise, millis, errmsg) { let timerID; const timer$ = new Promise((_, reject) => { timerID = setTimeout(() => { reject(new Error(errmsg)); }, millis); }); return Promise.race([promise, timer$]).finally(() => clearTimeout(timerID)); } function memoizeOnSuccess(factoryFn) { let cached = null; return () => { if (cached === null) { cached = factoryFn().catch((err) => { setTimeout(() => { cached = null; }, 5e3); throw err; }); } return cached; }; } function findLastIndex(arr, predicate) { for (let i = arr.length - 1; i >= 0; i--) { if (predicate(arr[i], i, arr)) { return i; } } return -1; } // src/lib/signals.ts var kSinks = Symbol("kSinks"); var kTrigger = Symbol("kTrigger"); var signalsToTrigger = null; var trackedReads = null; function batch(callback) { if (signalsToTrigger !== null) { callback(); return; } signalsToTrigger = /* @__PURE__ */ new Set(); try { callback(); } finally { for (const signal of signalsToTrigger) { signal[kTrigger](); } signalsToTrigger = null; } } function enqueueTrigger(signal) { if (!signalsToTrigger) raise("Expected to be in an active batch"); signalsToTrigger.add(signal); } function merge(target, patch) { let updated = false; const newValue = { ...target }; Object.keys(patch).forEach((k) => { const key = k; const val = patch[key]; if (newValue[key] !== val) { if (val === void 0) { delete newValue[key]; } else { newValue[key] = val; } updated = true; } }); return updated ? newValue : target; } var AbstractSignal = class { /** @internal */ equals; #eventSource; /** @internal */ [kSinks]; constructor(equals) { this.equals = equals ?? Object.is; this.#eventSource = makeEventSource(); this[kSinks] = /* @__PURE__ */ new Set(); this.get = this.get.bind(this); this.subscribe = this.subscribe.bind(this); this.subscribeOnce = this.subscribeOnce.bind(this); } dispose() { this.#eventSource.dispose(); this.#eventSource = "(disposed)"; this.equals = "(disposed)"; } get hasWatchers() { if (this.#eventSource.count() > 0) return true; for (const sink of this[kSinks]) { if (sink.hasWatchers) { return true; } } return false; } [kTrigger]() { this.#eventSource.notify(); for (const sink of this[kSinks]) { enqueueTrigger(sink); } } subscribe(callback) { if (this.#eventSource.count() === 0) { this.get(); } return this.#eventSource.subscribe(callback); } subscribeOnce(callback) { const unsub = this.subscribe(() => { unsub(); return callback(); }); return unsub; } waitUntil() { throw new Error("waitUntil not supported on Signals"); } markSinksDirty() { for (const sink of this[kSinks]) { sink.markDirty(); } } addSink(sink) { this[kSinks].add(sink); } removeSink(sink) { this[kSinks].delete(sink); } asReadonly() { return this; } }; var Signal = class extends AbstractSignal { #value; constructor(value, equals) { super(equals); this.#value = freeze(value); } dispose() { super.dispose(); this.#value = "(disposed)"; } get() { trackedReads?.add(this); return this.#value; } set(newValue) { batch(() => { if (typeof newValue === "function") { newValue = newValue(this.#value); } if (!this.equals(this.#value, newValue)) { this.#value = freeze(newValue); this.markSinksDirty(); enqueueTrigger(this); } }); } }; var PatchableSignal = class extends Signal { constructor(data) { super(freeze(compactObject(data))); } set() { throw new Error("Don't call .set() directly, use .patch()"); } /** * Patches the current object. */ patch(patch) { super.set((old) => merge(old, patch)); } }; var INITIAL = Symbol(); var DerivedSignal = class _DerivedSignal extends AbstractSignal { #prevValue; #dirty; // When true, the value in #value may not be up-to-date and needs re-checking #sources; #deps; #transform; // prettier-ignore static from(...args) { const last = args.pop(); if (typeof last !== "function") raise("Invalid .from() call, last argument expected to be a function"); if (typeof args[args.length - 1] === "function") { const equals = last; const transform = args.pop(); return new _DerivedSignal(args, transform, equals); } else { const transform = last; return new _DerivedSignal(args, transform); } } constructor(deps, transform, equals) { super(equals); this.#dirty = true; this.#prevValue = INITIAL; this.#deps = deps; this.#sources = /* @__PURE__ */ new Set(); this.#transform = transform; } dispose() { for (const src of this.#sources) { src.removeSink(this); } this.#prevValue = "(disposed)"; this.#sources = "(disposed)"; this.#deps = "(disposed)"; this.#transform = "(disposed)"; } get isDirty() { return this.#dirty; } #recompute() { const oldTrackedReads = trackedReads; let derived; trackedReads = /* @__PURE__ */ new Set(); try { derived = this.#transform(...this.#deps.map((p) => p.get())); } finally { const oldSources = this.#sources; this.#sources = /* @__PURE__ */ new Set(); for (const sig of trackedReads) { this.#sources.add(sig); oldSources.delete(sig); } for (const oldSource of oldSources) { oldSource.removeSink(this); } for (const newSource of this.#sources) { newSource.addSink(this); } trackedReads = oldTrackedReads; } this.#dirty = false; if (!this.equals(this.#prevValue, derived)) { this.#prevValue = derived; return true; } return false; } markDirty() { if (!this.#dirty) { this.#dirty = true; this.markSinksDirty(); } } get() { if (this.#dirty) { this.#recompute(); } trackedReads?.add(this); return this.#prevValue; } /** * Called by the Signal system if one or more of the dependent signals have * changed. In the case of a DerivedSignal, we'll only want to re-evaluate * the actual value if it's being watched, or any of their sinks are being * watched actively. */ [kTrigger]() { if (!this.hasWatchers) { return; } const updated = this.#recompute(); if (updated) { super[kTrigger](); } } }; var MutableSignal = class extends AbstractSignal { #state; constructor(initialState) { super(); this.#state = initialState; } dispose() { super.dispose(); this.#state = "(disposed)"; } get() { trackedReads?.add(this); return this.#state; } /** * Invokes a callback function that is allowed to mutate the given state * value. Do not change the value outside of the callback. * * If the callback explicitly returns `false`, it's assumed that the state * was not changed. */ mutate(callback) { batch(() => { const result = callback ? callback(this.#state) : true; if (result !== null && typeof result === "object" && "then" in result) { raise("MutableSignal.mutate() does not support async callbacks"); } if (result !== false) { this.markSinksDirty(); enqueueTrigger(this); } }); } }; // src/lib/SortedList.ts function bisectRight(arr, x, lt) { let lo = 0; let hi = arr.length; while (lo < hi) { const mid = lo + (hi - lo >> 1); if (lt(x, arr[mid])) { hi = mid; } else { lo = mid + 1; } } return lo; } var SortedList = class _SortedList { #data; #lt; constructor(alreadySortedList, lt) { this.#lt = lt; this.#data = alreadySortedList; } static with(lt) { return _SortedList.fromAlreadySorted([], lt); } static from(arr, lt) { const sorted = new _SortedList([], lt); for (const item of arr) { sorted.add(item); } return sorted; } static fromAlreadySorted(alreadySorted, lt) { return new _SortedList(alreadySorted, lt); } /** * Clones the sorted list to a new instance. */ clone() { return new _SortedList(this.#data.slice(), this.#lt); } /** * Adds a new item to the sorted list, such that it remains sorted. */ add(value) { const idx = bisectRight(this.#data, value, this.#lt); this.#data.splice(idx, 0, value); } /** * Removes all values from the sorted list, making it empty again. * Returns whether the list was mutated or not. */ clear() { const hadData = this.#data.length > 0; this.#data.length = 0; return hadData; } /** * Removes the first value matching the predicate. * Returns whether the list was mutated or not. */ removeBy(predicate, limit = Number.POSITIVE_INFINITY) { let deleted = 0; for (let i = 0; i < this.#data.length; i++) { if (predicate(this.#data[i])) { this.#data.splice(i, 1); deleted++; if (deleted >= limit) { break; } else { i--; } } } return deleted > 0; } /** * Removes the given value from the sorted list, if it exists. The given * value must be `===` to one of the list items. Only the first entry will be * removed if the element exists in the sorted list multiple times. * * Returns whether the list was mutated or not. */ remove(value) { const idx = this.#data.indexOf(value); if (idx >= 0) { this.#data.splice(idx, 1); return true; } return false; } at(index) { return this.#data[index]; } get length() { return this.#data.length; } *filter(predicate) { for (const item of this.#data) { if (predicate(item)) { yield item; } } } // XXXX If we keep this, add unit tests. Or remove it. *findAllRight(predicate) { for (let i = this.#data.length - 1; i >= 0; i--) { const item = this.#data[i]; if (predicate(item, i)) { yield item; } } } [Symbol.iterator]() { return this.#data[Symbol.iterator](); } *iterReversed() { for (let i = this.#data.length - 1; i >= 0; i--) { yield this.#data[i]; } } /** Finds the leftmost item that matches the predicate. */ find(predicate, start) { const idx = this.findIndex(predicate, start); return idx > -1 ? this.#data.at(idx) : void 0; } /** Finds the leftmost index that matches the predicate. */ findIndex(predicate, start = 0) { for (let i = Math.max(0, start); i < this.#data.length; i++) { if (predicate(this.#data[i], i)) { return i; } } return -1; } /** Finds the rightmost item that matches the predicate. */ findRight(predicate, start) { const idx = this.findIndexRight(predicate, start); return idx > -1 ? this.#data.at(idx) : void 0; } /** Finds the rightmost index that matches the predicate. */ findIndexRight(predicate, start = this.#data.length - 1) { for (let i = Math.min(start, this.#data.length - 1); i >= 0; i--) { if (predicate(this.#data[i], i)) { return i; } } return -1; } get rawArray() { return this.#data; } }; // src/AiChatDB.ts var AiChatDB = class { #byId; // A map of chat id to chat details #chats; // Sorted list of non-deleted chats, most recent first signal; constructor() { this.#byId = /* @__PURE__ */ new Map(); this.#chats = SortedList.from([], (c1, c2) => { const d2 = c2.lastMessageAt ?? c2.createdAt; const d1 = c1.lastMessageAt ?? c1.createdAt; return d2 < d1 ? true : d2 === d1 ? c2.id < c1.id : false; }); this.signal = new MutableSignal(this); } getEvenIfDeleted(chatId) { return this.#byId.get(chatId); } markDeleted(chatId) { const chat = this.#byId.get(chatId); if (chat === void 0 || chat.deletedAt !== void 0) return; this.upsert({ ...chat, deletedAt: (/* @__PURE__ */ new Date()).toISOString() }); } upsert(chat) { this.signal.mutate(() => { const existingThread = this.#byId.get(chat.id); if (existingThread !== void 0) { if (existingThread.deletedAt !== void 0) return false; this.#chats.remove(existingThread); this.#byId.delete(existingThread.id); } if (chat.deletedAt === void 0) { this.#chats.add(chat); } this.#byId.set(chat.id, chat); return true; }); } findMany(query) { return Array.from( this.#chats.filter((chat) => { if (query.metadata === void 0) return true; for (const [key, value] of Object.entries(query.metadata)) { if (value === null) { if (key in chat.metadata) return false; } else if (typeof value === "string") { if (chat.metadata[key] !== value) return false; } else { const chatValue = chat.metadata[key]; if (!Array.isArray(chatValue) || !value.every((v) => chatValue.includes(v))) { return false; } } } return true; }) ); } }; // src/convert-plain-data.ts function convertToCommentData(data) { const editedAt = data.editedAt ? new Date(data.editedAt) : void 0; const createdAt = new Date(data.createdAt); const reactions = data.reactions.map((reaction) => ({ ...reaction, createdAt: new Date(reaction.createdAt) })); if (data.body) { return { ...data, reactions, createdAt, editedAt }; } else { const deletedAt = new Date(data.deletedAt); return { ...data, reactions, createdAt, editedAt, deletedAt }; } } function convertToThreadData(data) { const createdAt = new Date(data.createdAt); const updatedAt = new Date(data.updatedAt); const comments = data.comments.map( (comment) => convertToCommentData(comment) ); return { ...data, createdAt, updatedAt, comments }; } function convertToCommentUserReaction(data) { return { ...data, createdAt: new Date(data.createdAt) }; } function convertToInboxNotificationData(data) { const notifiedAt = new Date(data.notifiedAt); const readAt = data.readAt ? new Date(data.readAt) : null; if ("activities" in data) { const activities = data.activities.map((activity) => ({ ...activity, createdAt: new Date(activity.createdAt) })); return { ...data, notifiedAt, readAt, activities }; } return { ...data, notifiedAt, readAt }; } function convertToSubscriptionData(data) { const createdAt = new Date(data.createdAt); return { ...data, createdAt }; } function convertToUserSubscriptionData(data) { const createdAt = new Date(data.createdAt); return { ...data, createdAt }; } function convertToThreadDeleteInfo(data) { const deletedAt = new Date(data.deletedAt); return { ...data, deletedAt }; } function convertToInboxNotificationDeleteInfo(data) { const deletedAt = new Date(data.deletedAt); return { ...data, deletedAt }; } function convertToSubscriptionDeleteInfo(data) { const deletedAt = new Date(data.deletedAt); return { ...data, deletedAt }; } // src/lib/fancy-console.ts var fancy_console_exports = {}; __export(fancy_console_exports, { error: () => error2, errorWithTitle: () => errorWithTitle, warn: () => warn, warnWithTitle: () => warnWithTitle }); var badge = "background:#0e0d12;border-radius:9999px;color:#fff;padding:3px 7px;font-family:sans-serif;font-weight:600;"; var bold = "font-weight:600"; function wrap(method) { return typeof window === "undefined" || process.env.NODE_ENV === "test" ? console[method] : ( /* istanbul ignore next */ (message, ...args) => console[method]("%cLiveblocks", badge, message, ...args) ); } var warn = wrap("warn"); var error2 = wrap("error"); function wrapWithTitle(method) { return typeof window === "undefined" || process.env.NODE_ENV === "test" ? console[method] : ( /* istanbul ignore next */ (title, message, ...args) => console[method]( `%cLiveblocks%c ${title}`, badge, bold, message, ...args ) ); } var warnWithTitle = wrapWithTitle("warn"); var errorWithTitle = wrapWithTitle("error"); // src/lib/guards.ts function isDefined(value) { return value !== null && value !== void 0; } function isPlainObject(blob) { return blob !== null && typeof blob === "object" && Object.prototype.toString.call(blob) === "[object Object]"; } function isStartsWithOperator(blob) { return isPlainObject(blob) && typeof blob.startsWith === "string"; } // src/lib/autoRetry.ts var HttpError = class _HttpError extends Error { response; details; constructor(message, response, details) { super(message); this.name = "HttpError"; this.response = response; this.details = details; } static async fromResponse(response) { let bodyAsText; try { bodyAsText = await response.text(); } catch { } const bodyAsJson = bodyAsText ? tryParseJson(bodyAsText) : void 0; let bodyAsJsonObject; if (isPlainObject(bodyAsJson)) { bodyAsJsonObject = bodyAsJson; } let message = ""; message ||= typeof bodyAsJsonObject?.message === "string" ? bodyAsJsonObject.message : ""; message ||= typeof bodyAsJsonObject?.error === "string" ? bodyAsJsonObject.error : ""; if (bodyAsJson === void 0) { message ||= bodyAsText || ""; } message ||= response.statusText; let path; try { path = new URL(response.url).pathname; } catch { } message += path !== void 0 ? ` (got status ${response.status} from ${path})` : ` (got status ${response.status})`; const details = bodyAsJsonObject; return new _HttpError(message, response, details); } /** * Convenience accessor for response.status. */ get status() { return this.response.status; } }; var DONT_RETRY_4XX = (x) => x instanceof HttpError && x.status >= 400 && x.status < 500; async function autoRetry(promiseFn, maxTries, backoff, shouldStopRetrying = DONT_RETRY_4XX) { const fallbackBackoff = backoff.length > 0 ? backoff[backoff.length - 1] : 0; let attempt = 0; while (true) { attempt++; try { return await promiseFn(); } catch (err) { if (shouldStopRetrying(err)) { throw err; } if (attempt >= maxTries) { throw new Error(`Failed after ${maxTries} attempts: ${String(err)}`); } } const delay = backoff[attempt - 1] ?? fallbackBackoff; warn( `Attempt ${attempt} was unsuccessful. Retrying in ${delay} milliseconds.` ); await wait(delay); } } // src/lib/controlledPromise.ts function controlledPromise() { let resolve; let reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return [promise, resolve, reject]; } function Promise_withResolvers() { const [promise, resolve, reject] = controlledPromise(); return { promise, resolve, reject }; } // src/lib/stringify.ts function replacer(_key, value) { return value !== null && typeof value === "object" && !Array.isArray(value) ? Object.keys(value).sort().reduce((sorted, key) => { sorted[key] = value[key]; return sorted; }, {}) : value; } function stableStringify(value) { return JSON.stringify(value, replacer); } function stringifyOrLog(value) { try { return JSON.stringify(value); } catch (err) { console.error(`Could not stringify: ${err.message}`); console.error(value); throw err; } } // src/lib/batch.ts var DEFAULT_SIZE = 50; var BatchCall = class { input; resolve; reject; promise; constructor(input) { this.input = input; const { promise, resolve, reject } = Promise_withResolvers(); this.promise = promise; this.resolve = resolve; this.reject = reject; } }; var Batch = class { #queue = []; #callback; #size; #delay; #delayTimeoutId; error = false; constructor(callback, options) { this.#callback = callback; this.#size = options.size ?? DEFAULT_SIZE; this.#delay = options.delay; } #clearDelayTimeout() { if (this.#delayTimeoutId !== void 0) { clearTimeout(this.#delayTimeoutId); this.#delayTimeoutId = void 0; } } #schedule() { if (this.#queue.length === this.#size) { void this.#flush(); } else if (this.#queue.length === 1) { this.#clearDelayTimeout(); this.#delayTimeoutId = setTimeout(() => void this.#flush(), this.#delay); } } async #flush() { if (this.#queue.length === 0) { return; } const calls = this.#queue.splice(0); const inputs = calls.map((call) => call.input); try { const results = await this.#callback(inputs); this.error = false; calls.forEach((call, index) => { const result = results?.[index]; if (!Array.isArray(results)) { call.reject(new Error("Callback must return an array.")); } else if (calls.length !== results.length) { call.reject( new Error( `Callback must return an array of the same length as the number of provided items. Expected ${calls.length}, but got ${results.length}.` ) ); } else if (result instanceof Error) { call.reject(result); } else { call.resolve(result); } }); } catch (error3) { this.error = true; calls.forEach((call) => { call.reject(error3); }); } } get(input) { const existingCall = this.#queue.find( (call2) => stableStringify(call2.input) === stableStringify(input) ); if (existingCall) { return existingCall.promise; } const call = new BatchCall(input); this.#queue.push(call); this.#schedule(); return call.promise; } clear() { this.#queue = []; this.error = false; this.#clearDelayTimeout(); } }; function createBatchStore(batch2) { const signal = new MutableSignal(/* @__PURE__ */ new Map()); function getCacheKey(args) { return stableStringify(args); } function update(cacheKey, state) { signal.mutate((cache) => { cache.set(cacheKey, state); }); } function invalidate(inputs) { signal.mutate((cache) => { if (Array.isArray(inputs)) { for (const input of inputs) { cache.delete(getCacheKey(input)); } } else { cache.clear(); } }); } async function enqueue(input) { const cacheKey = getCacheKey(input); const cache = signal.get(); if (cache.has(cacheKey)) { return; } try { update(cacheKey, { isLoading: true }); const result = await batch2.get(input); update(cacheKey, { isLoading: false, data: result }); } catch (error3) { update(cacheKey, { isLoading: false, error: error3 }); } } function getItemState(input) { const cacheKey = getCacheKey(input); const cache = signal.get(); return cache.get(cacheKey); } function _cacheKeys() { const cache = signal.get(); return [...cache.keys()]; } return { subscribe: signal.subscribe, enqueue, getItemState, invalidate, batch: batch2, _cacheKeys }; } // src/lib/chunk.ts function chunk(array, size) { const chunks = []; for (let i = 0, j = array.length; i < j; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } // src/lib/nanoid.ts var nanoid = (t = 21) => crypto.getRandomValues(new Uint8Array(t)).reduce( (t2, e) => t2 += (e &= 63) < 36 ? e.toString(36) : e < 62 ? (e - 26).toString(36).toUpperCase() : e < 63 ? "_" : "-", "" ); // src/lib/createIds.ts var THREAD_ID_PREFIX = "th"; var COMMENT_ID_PREFIX = "cm"; var COMMENT_ATTACHMENT_ID_PREFIX = "at"; var INBOX_NOTIFICATION_ID_PREFIX = "in"; function createOptimisticId(prefix) { return `${prefix}_${nanoid()}`; } function createThreadId() { return createOptimisticId(THREAD_ID_PREFIX); } function createCommentId() { return createOptimisticId(COMMENT_ID_PREFIX); } function createCommentAttachmentId() { return createOptimisticId(COMMENT_ATTACHMENT_ID_PREFIX); } function createInboxNotificationId() { return createOptimisticId(INBOX_NOTIFICATION_ID_PREFIX); } // src/lib/DefaultMap.ts var DefaultMap = class extends Map { #defaultFn; /** * If the default function is not provided to the constructor, it has to be * provided in each .getOrCreate() call individually. */ constructor(defaultFn, entries2) { super(entries2); this.#defaultFn = defaultFn; } /** * Gets the value at the given key, or creates it. * * Difference from normal Map: if the key does not exist, it will be created * on the fly using the factory function, and that value will get returned * instead of `undefined`. */ getOrCreate(key, defaultFn) { if (super.has(key)) { return super.get(key); } else { const fn = defaultFn ?? this.#defaultFn ?? raise("DefaultMap used without a factory function"); const value = fn(key); this.set(key, value); return value; } } }; // src/lib/objectToQuery.ts var identifierRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/; function objectToQuery(obj) { let filterList = []; const entries2 = Object.entries(obj); const keyValuePairs = []; const keyValuePairsWithOperator = []; const indexedKeys = []; entries2.forEach(([key, value]) => { if (!identifierRegex.test(key)) { throw new Error("Key must only contain letters, numbers, _"); } if (isSimpleValue(value)) { keyValuePairs.push([key, value]); } else if (isPlainObject(value)) { if (isStartsWithOperator(value)) { keyValuePairsWithOperator.push([key, value]); } else { indexedKeys.push([key, value]); } } }); filterList = [ ...getFiltersFromKeyValuePairs(keyValuePairs), ...getFiltersFromKeyValuePairsWithOperator(keyValuePairsWithOperator) ]; indexedKeys.forEach(([key, value]) => { const nestedEntries = Object.entries(value); const nKeyValuePairs = []; const nKeyValuePairsWithOperator = []; nestedEntries.forEach(([nestedKey, nestedValue]) => { if (isStringEmpty(nestedKey)) { throw new Error("Key cannot be empty"); } if (isSimpleValue(nestedValue)) { nKeyValuePairs.push([formatFilterKey(key, nestedKey), nestedValue]); } else if (isStartsWithOperator(nestedValue)) { nKeyValuePairsWithOperator.push([ formatFilterKey(key, nestedKey), nestedValue ]); } }); filterList = [ ...filterList, ...getFiltersFromKeyValuePairs(nKeyValuePairs), ...getFiltersFromKeyValuePairsWithOperator(nKeyValuePairsWithOperator) ]; }); return filterList.map(({ key, operator, value }) => `${key}${operator}${quote(value)}`).join(" "); } var getFiltersFromKeyValuePairs = (keyValuePairs) => { const filters = []; keyValuePairs.forEach(([key, value]) => { filters.push({ key, operator: ":", value }); }); return filters; }; var getFiltersFromKeyValuePairsWithOperator = (keyValuePairsWithOperator) => { const filters = []; keyValuePairsWithOperator.forEach(([key, value]) => { if ("startsWith" in value && typeof value.startsWith === "string") { filters.push({ key, operator: "^", value: value.startsWith }); } }); return filters; }; var isSimpleValue = (value) => { return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null; }; var formatFilterKey = (key, nestedKey) => { if (nestedKey) { return `${key}[${quote(nestedKey)}]`; } return key; }; var isStringEmpty = (value) => { return !value || value.toString().trim() === ""; }; function quote(input) { const result = JSON.stringify(input); if (typeof input !== "string") { return result; } if (result.includes("'")) { return result; } return `'${result.slice(1, -1).replace(/\\"/g, '"')}'`; } // src/lib/url.ts var PLACEHOLDER_BASE_URL = "https://localhost:9999"; var ABSOLUTE_URL_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*?:/; var TRAILING_SLASH_URL_REGEX = /\/(?:(?:\?|#).*)?$/; function toURLSearchParams(params) { const result = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value !== void 0 && value !== null) { result.set(key, value.toString()); } } return result; } function urljoin(baseUrl, path, params) { const url2 = new URL(path, baseUrl); if (params !== void 0) { url2.search = (params instanceof URLSearchParams ? params : toURLSearchParams(params)).toString(); } return url2.toString(); } function url(strings, ...values2) { return strings.reduce( (result, str, i) => result + encodeURIComponent(values2[i - 1] ?? "") + str ); } function sanitizeUrl(url2) { if (url2.startsWith("www.")) { url2 = "https://" + url2; } if (url2 === "#") { return url2; } try { const isAbsolute = ABSOLUTE_URL_REGEX.test(url2); const urlObject = new URL( url2, isAbsolute ? void 0 : PLACEHOLDER_BASE_URL ); if (urlObject.protocol !== "http:" && urlObject.protocol !== "https:") { return null; } const hasTrailingSlash = TRAILING_SLASH_URL_REGEX.test(url2); const sanitizedUrl = ( // 1. Origin, only for absolute URLs (isAbsolute ? urlObject.origin : "") + // 2. Pathname, with a trailing slash if the original URL had one (urlObject.pathname === "/" ? ( // 2.a. Domain-only URLs, they always have their pathname set to "/" hasTrailingSlash ? "/" : "" ) : ( // 2.b. URLs with a path hasTrailingSlash && !urlObject.pathname.endsWith("/") ? urlObject.pathname + "/" : urlObject.pathname )) + // 3. Search params urlObject.search + // 4. Hash urlObject.hash ); return sanitizedUrl !== "" ? sanitizedUrl : null; } catch { return null; } } function generateUrl(url2, params, hash) { const isAbsolute = ABSOLUTE_URL_REGEX.test(url2); const urlObject = new URL(url2, isAbsolute ? void 0 : PLACEHOLDER_BASE_URL); if (params !== void 0) { for (const [param, value] of Object.entries(params)) { if (value) { urlObject.searchParams.set(param, String(value)); } } } if (!urlObject.hash && hash !== void 0) { urlObject.hash = `#${hash}`; } return isAbsolute ? urlObject.href : urlObject.href.replace(PLACEHOLDER_BASE_URL, ""); } function isUrl(string) { try { new URL(string); return true; } catch (_) { return false; } } // src/api-client.ts function createApiClient({ baseUrl, authManager, currentUserId, fetchPolyfill }) { const httpClient = new HttpClient(baseUrl, fetchPolyfill); async function getThreadsSince(options) { const result = await httpClient.get( url`/v2/c/rooms/${options.roomId}/threads/delta`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId: options.roomId }), { since: options.since.toISOString() }, { signal: options.signal } ); return { threads: { updated: result.data.map(convertToThreadData), deleted: result.deletedThreads.map(convertToThreadDeleteInfo) }, inboxNotifications: { updated: result.inboxNotifications.map(convertToInboxNotificationData), deleted: result.deletedInboxNotifications.map( convertToInboxNotificationDeleteInfo ) }, subscriptions: { updated: result.subscriptions.map(convertToSubscriptionData), deleted: result.deletedSubscriptions.map( convertToSubscriptionDeleteInfo ) }, requestedAt: new Date(result.meta.requestedAt), permissionHints: result.meta.permissionHints }; } async function getThreads(options) { let query; if (options.query) { query = objectToQuery(options.query); } const PAGE_SIZE = 50; try { const result = await httpClient.get( url`/v2/c/rooms/${options.roomId}/threads`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId: options.roomId }), { cursor: options.cursor, query, limit: PAGE_SIZE } ); return { threads: result.data.map(convertToThreadData), inboxNotifications: result.inboxNotifications.map( convertToInboxNotificationData ), subscriptions: result.subscriptions.map(convertToSubscriptionData), nextCursor: result.meta.nextCursor, requestedAt: new Date(result.meta.requestedAt), permissionHints: result.meta.permissionHints }; } catch (err) { if (err instanceof HttpError && err.status === 404) { return { threads: [], inboxNotifications: [], subscriptions: [], nextCursor: null, // // HACK // requestedAt needs to be a *server* timestamp here. However, on // this 404 error response, there is no such timestamp. So out of // pure necessity we'll fall back to a local timestamp instead (and // allow for a possible 6 hour clock difference between client and // server). // requestedAt: new Date(Date.now() - 6 * 60 * 60 * 1e3), permissionHints: {} }; } throw err; } } async function createThread(options) { const commentId = options.commentId ?? createCommentId(); const threadId = options.threadId ?? createThreadId(); const thread = await httpClient.post( url`/v2/c/rooms/${options.roomId}/threads`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId: options.roomId }), { id: threadId, comment: { id: commentId, body: options.body, attachmentIds: options.attachmentIds }, metadata: options.metadata } ); return convertToThreadData(thread); } async function deleteThread(options) { await httpClient.delete( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId: options.roomId }) ); } async function getThread(options) { const response = await httpClient.rawGet( url`/v2/c/rooms/${options.roomId}/thread-with-notification/${options.threadId}`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId: options.roomId }) ); if (response.ok) { const json = await response.json(); return { thread: convertToThreadData(json.thread), inboxNotification: json.inboxNotification ? convertToInboxNotificationData(json.inboxNotification) : void 0, subscription: json.subscription ? convertToSubscriptionData(json.subscription) : void 0 }; } else if (response.status === 404) { return { thread: void 0, inboxNotification: void 0, subscription: void 0 }; } else { throw new Error( `There was an error while getting thread ${options.threadId}.` ); } } async function editThreadMetadata(options) { return await httpClient.post( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/metadata`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId: options.roomId }), options.metadata ); } async function createComment(options) { const commentId = options.commentId ?? createCommentId(); const comment = await httpClient.post( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/comments`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId: options.roomId }), { id: commentId, body: options.body, attachmentIds: options.attachmentIds } ); return convertToCommentData(comment); } async function editComment(options) { const comment = await httpClient.post( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/comments/${options.commentId}`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId: options.roomId }), { body: options.body, attachmentIds: options.attachmentIds } ); return convertToCommentData(comment); } async function deleteComment(options) { await httpClient.delete( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/comments/${options.commentId}`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId: options.roomId }) ); } async function addReaction(options) { const reaction = await httpClient.post( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/comments/${options.commentId}/reactions`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId: options.roomId }), { emoji: options.emoji } ); return convertToCommentUserReaction(reaction); } async function removeReaction(options) { await httpClient.delete( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/comments/${options.commentId}/reactions/${options.emoji}`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId: options.roomId }) ); } async function markThreadAsResolved(options) { await httpClient.post( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/mark-as-resolved`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId: options.roomId }) ); } async function markThreadAsUnresolved(options) { await httpClient.post( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/mark-as-unresolved`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId: options.roomId }) ); } async function subscribeToThread(options) { const subscription = await httpClient.post( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/subscribe`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId: options.roomId }) ); return convertToSubscriptionData(subscription); } async function unsubscribeFromThread(options) { await httpClient.post( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/unsubscribe`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId: options.roomId }) ); } async function uploadAttachment(options) { const roomId = options.roomId; const abortSignal = options.signal; const attachment = options.attachment; const abortError = abortSignal ? new DOMException( `Upload of attachment ${options.attachment.id} was aborted.`, "AbortError" ) : void 0; if (abortSignal?.aborted) { throw abortError; } const handleRetryError = (err) => { if (abortSignal?.aborted) { throw abortError; } if (err instanceof HttpError && err.status === 413) { throw err; } return false; }; const ATTACHMENT_PART_SIZE = 5 * 1024 * 1024; const RETRY_ATTEMPTS = 10; const RETRY_DELAYS = [ 2e3, 2e3, 2e3, 2e3, 2e3, 2e3, 2e3, 2e3, 2e3, 2e3 ]; function splitFileIntoParts(file) { const parts = []; let start = 0; while (start < file.size) { const end = Math.min(start + ATTACHMENT_PART_SIZE, file.size); parts.push({ partNumber: parts.length + 1, part: file.slice(start, end) }); start = end; } return parts; } if (attachment.size <= ATTACHMENT_PART_SIZE) { return autoRetry( async () => httpClient.putBlob( url`/v2/c/rooms/${roomId}/attachments/${attachment.id}/upload/${encodeURIComponent(attachment.name)}`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId }), attachment.file, { fileSize: attachment.size }, { signal: abortSignal } ), RETRY_ATTEMPTS, RETRY_DELAYS, handleRetryError ); } else { let uploadId; const uploadedParts = []; const createMultiPartUpload = await autoRetry( async () => httpClient.post( url`/v2/c/rooms/${roomId}/attachments/${attachment.id}/multipart/${encodeURIComponent(attachment.name)}`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId }), void 0, { signal: abortSignal }, { fileSize: attachment.size } ), RETRY_ATTEMPTS, RETRY_DELAYS, handleRetryError ); try { uploadId = createMultiPartUpload.uploadId; const parts = splitFileIntoParts(attachment.file); if (abortSignal?.aborted) { throw abortError; } const batches = chunk(parts, 5); for (const parts2 of batches) { const uploadedPartsPromises = []; for (const { part, partNumber } of parts2) { uploadedPartsPromises.push( autoRetry( async () => httpClient.putBlob( url`/v2/c/rooms/${roomId}/attachments/${attachment.id}/multipart/${createMultiPartUpload.uploadId}/${String(partNumber)}`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId }), part, void 0, { signal: abortSignal } ), RETRY_ATTEMPTS, RETRY_DELAYS, handleRetryError ) ); } uploadedParts.push(...await Promise.all(uploadedPartsPromises)); } if (abortSignal?.aborted) { throw abortError; } const sortedUploadedParts = uploadedParts.sort( (a, b) => a.partNumber - b.partNumber ); return httpClient.post( url`/v2/c/rooms/${roomId}/attachments/${attachment.id}/multipart/${uploadId}/complete`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId }), { parts: sortedUploadedParts }, { signal: abortSignal } ); } catch (error3) { if (uploadId && error3?.name && (error3.name === "AbortError" || error3.name === "TimeoutError")) { try { await httpClient.rawDelete( url`/v2/c/rooms/${roomId}/attachments/${attachment.id}/multipart/${uploadId}`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId }) ); } catch (error4) { } } throw error3; } } } const attachmentUrlsBatchStoresByRoom = new DefaultMap((roomId) => { const batch2 = new Batch( async (batchedAttachmentIds) => { const attachmentIds = batchedAttachmentIds.flat(); const { urls } = await httpClient.post( url`/v2/c/rooms/${roomId}/attachments/presigned-urls`, await authManager.getAuthValue({ requestedScope: "comments:read", roomId }), { attachmentIds } ); return urls.map( (url2) => url2 ?? new Error("There was an error while getting this attachment's URL") ); }, { delay: 50 } ); return createBatchStore(batch2); }); function getOrCreateAttachmentUrlsStore(roomId) { return attachmentUrlsBatchStoresByRoom.getOrCreate(roomId); } function getAttachmentUrl(options) { const batch2 = getOrCreateAttachm