@liveblocks/core
Version:
Private internals for Liveblocks. DO NOT import directly from this package!
1,773 lines (1,741 loc) • 345 kB
JavaScript
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2;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 = "cjs";
// 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(() => _optionalChain([unsub, 'optionalCall', _2 => _2()]));
}
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 */
#eventSource;
/** @internal */
constructor(equals) {
this.equals = _nullishCoalesce(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() {
_optionalChain([trackedReads, 'optionalAccess', _3 => _3.add, 'call', _4 => _4(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();
}
_optionalChain([trackedReads, 'optionalAccess', _5 => _5.add, 'call', _6 => _6(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() {
_optionalChain([trackedReads, 'optionalAccess', _7 => _7.add, 'call', _8 => _8(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
constructor() {
this.#byId = /* @__PURE__ */ new Map();
this.#chats = SortedList.from([], (c1, c2) => {
const d2 = _nullishCoalesce(c2.lastMessageAt, () => ( c2.createdAt));
const d1 = _nullishCoalesce(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 {
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 (e2) {
}
const bodyAsJson = bodyAsText ? tryParseJson(bodyAsText) : void 0;
let bodyAsJsonObject;
if (isPlainObject(bodyAsJson)) {
bodyAsJsonObject = bodyAsJson;
}
let message = "";
message ||= typeof _optionalChain([bodyAsJsonObject, 'optionalAccess', _9 => _9.message]) === "string" ? bodyAsJsonObject.message : "";
message ||= typeof _optionalChain([bodyAsJsonObject, 'optionalAccess', _10 => _10.error]) === "string" ? bodyAsJsonObject.error : "";
if (bodyAsJson === void 0) {
message ||= bodyAsText || "";
}
message ||= response.statusText;
let path;
try {
path = new URL(response.url).pathname;
} catch (e3) {
}
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 = _nullishCoalesce(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 {
constructor(input) {
this.input = input;
const { promise, resolve, reject } = Promise_withResolvers();
this.promise = promise;
this.resolve = resolve;
this.reject = reject;
}
};
var Batch = (_class = class {
#queue = [];
#callback;
#size;
#delay;
#delayTimeoutId;
__init() {this.error = false}
constructor(callback, options) {;_class.prototype.__init.call(this);
this.#callback = callback;
this.#size = _nullishCoalesce(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 = _optionalChain([results, 'optionalAccess', _11 => _11[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();
}
}, _class);
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 = _nullishCoalesce(_nullishCoalesce(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(_nullishCoalesce(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 (e4) {
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 = _nullishCoalesce(options.commentId, () => ( createCommentId()));
const threadId = _nullishCoalesce(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 = _nullishCoalesce(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 (_optionalChain([abortSignal, 'optionalAccess', _12 => _12.aborted])) {
throw abortError;
}
const handleRetryError = (err) => {
if (_optionalChain([abortSignal, 'optionalAccess', _13 => _13.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 (_optionalChain([abortSignal, 'optionalAccess', _14 => _14.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 (_optionalChain([abortSignal, 'optionalAccess', _15 => _15.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({