@liveblocks/core 2.15.1 → 2.16.0-rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -6,7 +6,7 @@ var __export = (target, all) => {
6
6
 
7
7
  // src/version.ts
8
8
  var PKG_NAME = "@liveblocks/core";
9
- var PKG_VERSION = "2.15.1";
9
+ var PKG_VERSION = "2.16.0-rc1";
10
10
  var PKG_FORMAT = "esm";
11
11
 
12
12
  // src/dupe-detection.ts
@@ -166,13 +166,18 @@ function wrapWithTitle(method) {
166
166
  var warnWithTitle = wrapWithTitle("warn");
167
167
  var errorWithTitle = wrapWithTitle("error");
168
168
 
169
+ // src/lib/guards.ts
170
+ function isPlainObject(blob) {
171
+ return blob !== null && typeof blob === "object" && Object.prototype.toString.call(blob) === "[object Object]";
172
+ }
173
+ function isStartsWithOperator(blob) {
174
+ return isPlainObject(blob) && typeof blob.startsWith === "string";
175
+ }
176
+
169
177
  // src/lib/utils.ts
170
178
  function raise(msg) {
171
179
  throw new Error(msg);
172
180
  }
173
- function isPlainObject(blob) {
174
- return blob !== null && typeof blob === "object" && Object.prototype.toString.call(blob) === "[object Object]";
175
- }
176
181
  function entries(obj) {
177
182
  return Object.entries(obj);
178
183
  }
@@ -254,13 +259,48 @@ function memoizeOnSuccess(factoryFn) {
254
259
  }
255
260
 
256
261
  // src/lib/autoRetry.ts
257
- var HttpError = class extends Error {
258
- constructor(message, status, details) {
262
+ var HttpError = class _HttpError extends Error {
263
+ response;
264
+ details;
265
+ constructor(message, response, details) {
259
266
  super(message);
260
- this.message = message;
261
- this.status = status;
267
+ this.name = "HttpError";
268
+ this.response = response;
262
269
  this.details = details;
263
270
  }
271
+ static async fromResponse(response) {
272
+ let bodyAsText;
273
+ try {
274
+ bodyAsText = await response.text();
275
+ } catch {
276
+ }
277
+ const bodyAsJson = bodyAsText ? tryParseJson(bodyAsText) : void 0;
278
+ let bodyAsJsonObject;
279
+ if (isPlainObject(bodyAsJson)) {
280
+ bodyAsJsonObject = bodyAsJson;
281
+ }
282
+ let message = "";
283
+ message ||= typeof bodyAsJsonObject?.message === "string" ? bodyAsJsonObject.message : "";
284
+ message ||= typeof bodyAsJsonObject?.error === "string" ? bodyAsJsonObject.error : "";
285
+ if (bodyAsJson === void 0) {
286
+ message ||= bodyAsText || "";
287
+ }
288
+ message ||= response.statusText;
289
+ let path;
290
+ try {
291
+ path = new URL(response.url).pathname;
292
+ } catch {
293
+ }
294
+ message += path !== void 0 ? ` (got status ${response.status} from ${path})` : ` (got status ${response.status})`;
295
+ const details = bodyAsJsonObject;
296
+ return new _HttpError(message, response, details);
297
+ }
298
+ /**
299
+ * Convenience accessor for response.status.
300
+ */
301
+ get status() {
302
+ return this.response.status;
303
+ }
264
304
  };
265
305
  var DONT_RETRY_4XX = (x) => x instanceof HttpError && x.status >= 400 && x.status < 500;
266
306
  async function autoRetry(promiseFn, maxTries, backoff, shouldStopRetrying = DONT_RETRY_4XX) {
@@ -326,7 +366,12 @@ function makeEventSource() {
326
366
  }).finally(() => unsub?.());
327
367
  }
328
368
  function notify(event) {
329
- _observers.forEach((callback) => callback(event));
369
+ let called = false;
370
+ for (const callback of _observers) {
371
+ callback(event);
372
+ called = true;
373
+ }
374
+ return called;
330
375
  }
331
376
  function count() {
332
377
  return _observers.size;
@@ -367,8 +412,9 @@ function makeBufferableEventSource() {
367
412
  function notifyOrBuffer(event) {
368
413
  if (_buffer !== null) {
369
414
  _buffer.push(event);
415
+ return false;
370
416
  } else {
371
- eventSource2.notify(event);
417
+ return eventSource2.notify(event);
372
418
  }
373
419
  }
374
420
  return {
@@ -385,164 +431,448 @@ function makeBufferableEventSource() {
385
431
  };
386
432
  }
387
433
 
388
- // src/lib/stringify.ts
389
- function stringify(object, ...args) {
390
- if (typeof object !== "object" || object === null || Array.isArray(object)) {
391
- return JSON.stringify(object, ...args);
392
- }
393
- const sortedObject = Object.keys(object).sort().reduce(
394
- (sortedObject2, key) => {
395
- sortedObject2[key] = object[key];
396
- return sortedObject2;
397
- },
398
- {}
399
- );
400
- return JSON.stringify(sortedObject, ...args);
401
- }
434
+ // src/lib/freeze.ts
435
+ var freeze = process.env.NODE_ENV === "production" ? (
436
+ /* istanbul ignore next */
437
+ (x) => x
438
+ ) : Object.freeze;
402
439
 
403
- // src/lib/batch.ts
404
- var DEFAULT_SIZE = 50;
405
- var BatchCall = class {
406
- input;
407
- resolve;
408
- reject;
409
- promise;
410
- constructor(input) {
411
- this.input = input;
412
- const { promise, resolve, reject } = Promise_withResolvers();
413
- this.promise = promise;
414
- this.resolve = resolve;
415
- this.reject = reject;
440
+ // src/lib/signals.ts
441
+ var kSinks = Symbol("kSinks");
442
+ var kTrigger = Symbol("kTrigger");
443
+ var signalsToTrigger = null;
444
+ var trackedReads = null;
445
+ function batch(callback) {
446
+ if (signalsToTrigger !== null) {
447
+ callback();
448
+ return;
416
449
  }
417
- };
418
- var Batch = class {
419
- #queue = [];
420
- #callback;
421
- #size;
422
- #delay;
423
- #delayTimeoutId;
424
- error = false;
425
- constructor(callback, options) {
426
- this.#callback = callback;
427
- this.#size = options.size ?? DEFAULT_SIZE;
428
- this.#delay = options.delay;
450
+ signalsToTrigger = /* @__PURE__ */ new Set();
451
+ try {
452
+ callback();
453
+ } finally {
454
+ for (const signal of signalsToTrigger) {
455
+ signal[kTrigger]();
456
+ }
457
+ signalsToTrigger = null;
429
458
  }
430
- #clearDelayTimeout() {
431
- if (this.#delayTimeoutId !== void 0) {
432
- clearTimeout(this.#delayTimeoutId);
433
- this.#delayTimeoutId = void 0;
459
+ }
460
+ function enqueueTrigger(signal) {
461
+ if (!signalsToTrigger) raise("Expected to be in an active batch");
462
+ signalsToTrigger.add(signal);
463
+ }
464
+ function merge(target, patch) {
465
+ let updated = false;
466
+ const newValue = { ...target };
467
+ Object.keys(patch).forEach((k) => {
468
+ const key = k;
469
+ const val = patch[key];
470
+ if (newValue[key] !== val) {
471
+ if (val === void 0) {
472
+ delete newValue[key];
473
+ } else {
474
+ newValue[key] = val;
475
+ }
476
+ updated = true;
434
477
  }
478
+ });
479
+ return updated ? newValue : target;
480
+ }
481
+ var AbstractSignal = class {
482
+ /** @internal */
483
+ equals;
484
+ #eventSource;
485
+ /** @internal */
486
+ [kSinks];
487
+ constructor(equals) {
488
+ this.equals = equals ?? Object.is;
489
+ this.#eventSource = makeEventSource();
490
+ this[kSinks] = /* @__PURE__ */ new Set();
491
+ this.get = this.get.bind(this);
492
+ this.subscribe = this.subscribe.bind(this);
493
+ this.subscribeOnce = this.subscribeOnce.bind(this);
435
494
  }
436
- #schedule() {
437
- if (this.#queue.length === this.#size) {
438
- void this.#flush();
439
- } else if (this.#queue.length === 1) {
440
- this.#clearDelayTimeout();
441
- this.#delayTimeoutId = setTimeout(() => void this.#flush(), this.#delay);
495
+ [Symbol.dispose]() {
496
+ this.#eventSource[Symbol.dispose]();
497
+ this.#eventSource = "(disposed)";
498
+ this.equals = "(disposed)";
499
+ }
500
+ get hasWatchers() {
501
+ if (this.#eventSource.count() > 0) return true;
502
+ for (const sink of this[kSinks]) {
503
+ if (sink.hasWatchers) {
504
+ return true;
505
+ }
442
506
  }
507
+ return false;
443
508
  }
444
- async #flush() {
445
- if (this.#queue.length === 0) {
446
- return;
509
+ [kTrigger]() {
510
+ this.#eventSource.notify();
511
+ for (const sink of this[kSinks]) {
512
+ enqueueTrigger(sink);
447
513
  }
448
- const calls = this.#queue.splice(0);
449
- const inputs = calls.map((call) => call.input);
450
- try {
451
- const results = await this.#callback(inputs);
452
- this.error = false;
453
- calls.forEach((call, index) => {
454
- const result = results?.[index];
455
- if (!Array.isArray(results)) {
456
- call.reject(new Error("Callback must return an array."));
457
- } else if (calls.length !== results.length) {
458
- call.reject(
459
- new Error(
460
- `Callback must return an array of the same length as the number of provided items. Expected ${calls.length}, but got ${results.length}.`
461
- )
462
- );
463
- } else if (result instanceof Error) {
464
- call.reject(result);
465
- } else {
466
- call.resolve(result);
467
- }
468
- });
469
- } catch (error3) {
470
- this.error = true;
471
- calls.forEach((call) => {
472
- call.reject(error3);
473
- });
514
+ }
515
+ subscribe(callback) {
516
+ if (this.#eventSource.count() === 0) {
517
+ this.get();
474
518
  }
519
+ return this.#eventSource.subscribe(callback);
475
520
  }
476
- get(input) {
477
- const existingCall = this.#queue.find(
478
- (call2) => stringify(call2.input) === stringify(input)
479
- );
480
- if (existingCall) {
481
- return existingCall.promise;
521
+ subscribeOnce(callback) {
522
+ const unsub = this.subscribe(() => {
523
+ unsub();
524
+ return callback();
525
+ });
526
+ return unsub;
527
+ }
528
+ waitUntil() {
529
+ throw new Error("waitUntil not supported on Signals");
530
+ }
531
+ markSinksDirty() {
532
+ for (const sink of this[kSinks]) {
533
+ sink.markDirty();
482
534
  }
483
- const call = new BatchCall(input);
484
- this.#queue.push(call);
485
- this.#schedule();
486
- return call.promise;
487
535
  }
488
- clear() {
489
- this.#queue = [];
490
- this.error = false;
491
- this.#clearDelayTimeout();
536
+ addSink(sink) {
537
+ this[kSinks].add(sink);
538
+ }
539
+ removeSink(sink) {
540
+ this[kSinks].delete(sink);
541
+ }
542
+ asReadonly() {
543
+ return this;
492
544
  }
493
545
  };
494
- function createBatchStore(batch2) {
495
- const cache = /* @__PURE__ */ new Map();
496
- const eventSource2 = makeEventSource();
497
- function getCacheKey(args) {
498
- return stringify(args);
546
+ var Signal = class extends AbstractSignal {
547
+ #value;
548
+ constructor(value, equals) {
549
+ super(equals);
550
+ this.#value = freeze(value);
499
551
  }
500
- function setStateAndNotify(cacheKey, state) {
501
- cache.set(cacheKey, state);
502
- eventSource2.notify();
552
+ [Symbol.dispose]() {
553
+ super[Symbol.dispose]();
554
+ this.#value = "(disposed)";
503
555
  }
504
- function invalidate(inputs) {
505
- if (Array.isArray(inputs)) {
506
- for (const input of inputs) {
507
- cache.delete(getCacheKey(input));
508
- }
509
- } else {
510
- cache.clear();
511
- }
512
- eventSource2.notify();
556
+ get() {
557
+ trackedReads?.add(this);
558
+ return this.#value;
513
559
  }
514
- async function get(input) {
515
- const cacheKey = getCacheKey(input);
516
- if (cache.has(cacheKey)) {
517
- return;
518
- }
519
- try {
520
- setStateAndNotify(cacheKey, { isLoading: true });
521
- const result = await batch2.get(input);
522
- setStateAndNotify(cacheKey, { isLoading: false, data: result });
523
- } catch (error3) {
524
- setStateAndNotify(cacheKey, {
525
- isLoading: false,
526
- error: error3
527
- });
528
- }
560
+ set(newValue) {
561
+ batch(() => {
562
+ if (typeof newValue === "function") {
563
+ newValue = newValue(this.#value);
564
+ }
565
+ if (!this.equals(this.#value, newValue)) {
566
+ this.#value = freeze(newValue);
567
+ this.markSinksDirty();
568
+ enqueueTrigger(this);
569
+ }
570
+ });
529
571
  }
530
- function getState(input) {
531
- const cacheKey = getCacheKey(input);
532
- return cache.get(cacheKey);
572
+ };
573
+ var PatchableSignal = class extends Signal {
574
+ constructor(data) {
575
+ super(freeze(compactObject(data)));
533
576
  }
534
- function _cacheKeys() {
535
- return [...cache.keys()];
577
+ set() {
578
+ throw new Error("Don't call .set() directly, use .patch()");
536
579
  }
537
- function getBatch() {
538
- return batch2;
580
+ /**
581
+ * Patches the current object.
582
+ */
583
+ patch(patch) {
584
+ super.set((old) => merge(old, patch));
539
585
  }
540
- return {
541
- ...eventSource2.observable,
542
- get,
543
- getState,
586
+ };
587
+ var INITIAL = Symbol();
588
+ var DerivedSignal = class _DerivedSignal extends AbstractSignal {
589
+ #prevValue;
590
+ #dirty;
591
+ // When true, the value in #value may not be up-to-date and needs re-checking
592
+ #sources;
593
+ #deps;
594
+ #transform;
595
+ // prettier-ignore
596
+ static from(...args) {
597
+ const last = args.pop();
598
+ if (typeof last !== "function")
599
+ raise("Invalid .from() call, last argument expected to be a function");
600
+ if (typeof args[args.length - 1] === "function") {
601
+ const equals = last;
602
+ const transform = args.pop();
603
+ return new _DerivedSignal(args, transform, equals);
604
+ } else {
605
+ const transform = last;
606
+ return new _DerivedSignal(args, transform);
607
+ }
608
+ }
609
+ constructor(deps, transform, equals) {
610
+ super(equals);
611
+ this.#dirty = true;
612
+ this.#prevValue = INITIAL;
613
+ this.#deps = deps;
614
+ this.#sources = /* @__PURE__ */ new Set();
615
+ this.#transform = transform;
616
+ }
617
+ [Symbol.dispose]() {
618
+ for (const src of this.#sources) {
619
+ src.removeSink(this);
620
+ }
621
+ this.#prevValue = "(disposed)";
622
+ this.#sources = "(disposed)";
623
+ this.#deps = "(disposed)";
624
+ this.#transform = "(disposed)";
625
+ }
626
+ get isDirty() {
627
+ return this.#dirty;
628
+ }
629
+ #recompute() {
630
+ const oldTrackedReads = trackedReads;
631
+ let derived;
632
+ trackedReads = /* @__PURE__ */ new Set();
633
+ try {
634
+ derived = this.#transform(...this.#deps.map((p) => p.get()));
635
+ } finally {
636
+ const oldSources = this.#sources;
637
+ this.#sources = /* @__PURE__ */ new Set();
638
+ for (const sig of trackedReads) {
639
+ this.#sources.add(sig);
640
+ oldSources.delete(sig);
641
+ }
642
+ for (const oldSource of oldSources) {
643
+ oldSource.removeSink(this);
644
+ }
645
+ for (const newSource of this.#sources) {
646
+ newSource.addSink(this);
647
+ }
648
+ trackedReads = oldTrackedReads;
649
+ }
650
+ this.#dirty = false;
651
+ if (!this.equals(this.#prevValue, derived)) {
652
+ this.#prevValue = derived;
653
+ return true;
654
+ }
655
+ return false;
656
+ }
657
+ markDirty() {
658
+ if (!this.#dirty) {
659
+ this.#dirty = true;
660
+ this.markSinksDirty();
661
+ }
662
+ }
663
+ get() {
664
+ if (this.#dirty) {
665
+ this.#recompute();
666
+ }
667
+ trackedReads?.add(this);
668
+ return this.#prevValue;
669
+ }
670
+ /**
671
+ * Called by the Signal system if one or more of the dependent signals have
672
+ * changed. In the case of a DerivedSignal, we'll only want to re-evaluate
673
+ * the actual value if it's being watched, or any of their sinks are being
674
+ * watched actively.
675
+ */
676
+ [kTrigger]() {
677
+ if (!this.hasWatchers) {
678
+ return;
679
+ }
680
+ const updated = this.#recompute();
681
+ if (updated) {
682
+ super[kTrigger]();
683
+ }
684
+ }
685
+ };
686
+ var MutableSignal = class extends AbstractSignal {
687
+ #state;
688
+ constructor(initialState) {
689
+ super();
690
+ this.#state = initialState;
691
+ }
692
+ [Symbol.dispose]() {
693
+ super[Symbol.dispose]();
694
+ this.#state = "(disposed)";
695
+ }
696
+ get() {
697
+ trackedReads?.add(this);
698
+ return this.#state;
699
+ }
700
+ /**
701
+ * Invokes a callback function that is allowed to mutate the given state
702
+ * value. Do not change the value outside of the callback.
703
+ *
704
+ * If the callback explicitly returns `false`, it's assumed that the state
705
+ * was not changed.
706
+ */
707
+ mutate(callback) {
708
+ batch(() => {
709
+ const result = callback ? callback(this.#state) : true;
710
+ if (result !== null && typeof result === "object" && "then" in result) {
711
+ raise("MutableSignal.mutate() does not support async callbacks");
712
+ }
713
+ if (result !== false) {
714
+ this.markSinksDirty();
715
+ enqueueTrigger(this);
716
+ }
717
+ });
718
+ }
719
+ };
720
+
721
+ // src/lib/stringify.ts
722
+ function replacer(_key, value) {
723
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? Object.keys(value).sort().reduce((sorted, key) => {
724
+ sorted[key] = value[key];
725
+ return sorted;
726
+ }, {}) : value;
727
+ }
728
+ function stringify(value) {
729
+ return JSON.stringify(value, replacer);
730
+ }
731
+
732
+ // src/lib/batch.ts
733
+ var DEFAULT_SIZE = 50;
734
+ var BatchCall = class {
735
+ input;
736
+ resolve;
737
+ reject;
738
+ promise;
739
+ constructor(input) {
740
+ this.input = input;
741
+ const { promise, resolve, reject } = Promise_withResolvers();
742
+ this.promise = promise;
743
+ this.resolve = resolve;
744
+ this.reject = reject;
745
+ }
746
+ };
747
+ var Batch = class {
748
+ #queue = [];
749
+ #callback;
750
+ #size;
751
+ #delay;
752
+ #delayTimeoutId;
753
+ error = false;
754
+ constructor(callback, options) {
755
+ this.#callback = callback;
756
+ this.#size = options.size ?? DEFAULT_SIZE;
757
+ this.#delay = options.delay;
758
+ }
759
+ #clearDelayTimeout() {
760
+ if (this.#delayTimeoutId !== void 0) {
761
+ clearTimeout(this.#delayTimeoutId);
762
+ this.#delayTimeoutId = void 0;
763
+ }
764
+ }
765
+ #schedule() {
766
+ if (this.#queue.length === this.#size) {
767
+ void this.#flush();
768
+ } else if (this.#queue.length === 1) {
769
+ this.#clearDelayTimeout();
770
+ this.#delayTimeoutId = setTimeout(() => void this.#flush(), this.#delay);
771
+ }
772
+ }
773
+ async #flush() {
774
+ if (this.#queue.length === 0) {
775
+ return;
776
+ }
777
+ const calls = this.#queue.splice(0);
778
+ const inputs = calls.map((call) => call.input);
779
+ try {
780
+ const results = await this.#callback(inputs);
781
+ this.error = false;
782
+ calls.forEach((call, index) => {
783
+ const result = results?.[index];
784
+ if (!Array.isArray(results)) {
785
+ call.reject(new Error("Callback must return an array."));
786
+ } else if (calls.length !== results.length) {
787
+ call.reject(
788
+ new Error(
789
+ `Callback must return an array of the same length as the number of provided items. Expected ${calls.length}, but got ${results.length}.`
790
+ )
791
+ );
792
+ } else if (result instanceof Error) {
793
+ call.reject(result);
794
+ } else {
795
+ call.resolve(result);
796
+ }
797
+ });
798
+ } catch (error3) {
799
+ this.error = true;
800
+ calls.forEach((call) => {
801
+ call.reject(error3);
802
+ });
803
+ }
804
+ }
805
+ get(input) {
806
+ const existingCall = this.#queue.find(
807
+ (call2) => stringify(call2.input) === stringify(input)
808
+ );
809
+ if (existingCall) {
810
+ return existingCall.promise;
811
+ }
812
+ const call = new BatchCall(input);
813
+ this.#queue.push(call);
814
+ this.#schedule();
815
+ return call.promise;
816
+ }
817
+ clear() {
818
+ this.#queue = [];
819
+ this.error = false;
820
+ this.#clearDelayTimeout();
821
+ }
822
+ };
823
+ function createBatchStore(batch2) {
824
+ const signal = new MutableSignal(/* @__PURE__ */ new Map());
825
+ function getCacheKey(args) {
826
+ return stringify(args);
827
+ }
828
+ function update(cacheKey, state) {
829
+ signal.mutate((cache) => {
830
+ cache.set(cacheKey, state);
831
+ });
832
+ }
833
+ function invalidate(inputs) {
834
+ signal.mutate((cache) => {
835
+ if (Array.isArray(inputs)) {
836
+ for (const input of inputs) {
837
+ cache.delete(getCacheKey(input));
838
+ }
839
+ } else {
840
+ cache.clear();
841
+ }
842
+ });
843
+ }
844
+ async function enqueue(input) {
845
+ const cacheKey = getCacheKey(input);
846
+ const cache = signal.get();
847
+ if (cache.has(cacheKey)) {
848
+ return;
849
+ }
850
+ try {
851
+ update(cacheKey, { isLoading: true });
852
+ const result = await batch2.get(input);
853
+ update(cacheKey, { isLoading: false, data: result });
854
+ } catch (error3) {
855
+ update(cacheKey, {
856
+ isLoading: false,
857
+ error: error3
858
+ });
859
+ }
860
+ }
861
+ function getItemState(input) {
862
+ const cacheKey = getCacheKey(input);
863
+ const cache = signal.get();
864
+ return cache.get(cacheKey);
865
+ }
866
+ function _cacheKeys() {
867
+ const cache = signal.get();
868
+ return [...cache.keys()];
869
+ }
870
+ return {
871
+ subscribe: signal.subscribe,
872
+ enqueue,
873
+ getItemState,
544
874
  invalidate,
545
- getBatch,
875
+ batch: batch2,
546
876
  _cacheKeys
547
877
  };
548
878
  }
@@ -583,6 +913,36 @@ function createInboxNotificationId() {
583
913
  return createOptimisticId(INBOX_NOTIFICATION_ID_PREFIX);
584
914
  }
585
915
 
916
+ // src/lib/DefaultMap.ts
917
+ var DefaultMap = class extends Map {
918
+ #defaultFn;
919
+ /**
920
+ * If the default function is not provided to the constructor, it has to be
921
+ * provided in each .getOrCreate() call individually.
922
+ */
923
+ constructor(defaultFn, entries2) {
924
+ super(entries2);
925
+ this.#defaultFn = defaultFn;
926
+ }
927
+ /**
928
+ * Gets the value at the given key, or creates it.
929
+ *
930
+ * Difference from normal Map: if the key does not exist, it will be created
931
+ * on the fly using the factory function, and that value will get returned
932
+ * instead of `undefined`.
933
+ */
934
+ getOrCreate(key, defaultFn) {
935
+ if (super.has(key)) {
936
+ return super.get(key);
937
+ } else {
938
+ const fn = defaultFn ?? this.#defaultFn ?? raise("DefaultMap used without a factory function");
939
+ const value = fn(key);
940
+ this.set(key, value);
941
+ return value;
942
+ }
943
+ }
944
+ };
945
+
586
946
  // src/lib/objectToQuery.ts
587
947
  var identifierRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
588
948
  function objectToQuery(obj) {
@@ -597,10 +957,12 @@ function objectToQuery(obj) {
597
957
  }
598
958
  if (isSimpleValue(value)) {
599
959
  keyValuePairs.push([key, value]);
600
- } else if (isValueWithOperator(value)) {
601
- keyValuePairsWithOperator.push([key, value]);
602
- } else if (typeof value === "object" && !("startsWith" in value)) {
603
- indexedKeys.push([key, value]);
960
+ } else if (isPlainObject(value)) {
961
+ if (isStartsWithOperator(value)) {
962
+ keyValuePairsWithOperator.push([key, value]);
963
+ } else {
964
+ indexedKeys.push([key, value]);
965
+ }
604
966
  }
605
967
  });
606
968
  filterList = [
@@ -617,7 +979,7 @@ function objectToQuery(obj) {
617
979
  }
618
980
  if (isSimpleValue(nestedValue)) {
619
981
  nKeyValuePairs.push([formatFilterKey(key, nestedKey), nestedValue]);
620
- } else if (isValueWithOperator(nestedValue)) {
982
+ } else if (isStartsWithOperator(nestedValue)) {
621
983
  nKeyValuePairsWithOperator.push([
622
984
  formatFilterKey(key, nestedKey),
623
985
  nestedValue
@@ -630,9 +992,7 @@ function objectToQuery(obj) {
630
992
  ...getFiltersFromKeyValuePairsWithOperator(nKeyValuePairsWithOperator)
631
993
  ];
632
994
  });
633
- return filterList.map(
634
- ({ key, operator, value }) => formatFilter(key, operator, formatFilterValue(value))
635
- ).join(" AND ");
995
+ return filterList.map(({ key, operator, value }) => `${key}${operator}${quote(value)}`).join(" ");
636
996
  }
637
997
  var getFiltersFromKeyValuePairs = (keyValuePairs) => {
638
998
  const filters = [];
@@ -659,38 +1019,20 @@ var getFiltersFromKeyValuePairsWithOperator = (keyValuePairsWithOperator) => {
659
1019
  return filters;
660
1020
  };
661
1021
  var isSimpleValue = (value) => {
662
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
663
- return true;
664
- }
665
- return false;
1022
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null;
666
1023
  };
667
- var isValueWithOperator = (value) => {
668
- if (typeof value === "object" && value !== null && "startsWith" in value) {
669
- return true;
670
- }
671
- return false;
672
- };
673
- var formatFilter = (key, operator, value) => {
674
- return `${key}${operator}${value}`;
675
- };
676
- var formatFilterKey = (key, nestedKey) => {
677
- if (nestedKey) {
678
- return `${key}[${JSON.stringify(nestedKey)}]`;
1024
+ var formatFilterKey = (key, nestedKey) => {
1025
+ if (nestedKey) {
1026
+ return `${key}[${quote(nestedKey)}]`;
679
1027
  }
680
1028
  return key;
681
1029
  };
682
- var formatFilterValue = (value) => {
683
- if (typeof value === "string") {
684
- if (isStringEmpty(value)) {
685
- throw new Error("Value cannot be empty");
686
- }
687
- return JSON.stringify(value);
688
- }
689
- return value.toString();
690
- };
691
1030
  var isStringEmpty = (value) => {
692
1031
  return !value || value.toString().trim() === "";
693
1032
  };
1033
+ function quote(value) {
1034
+ return typeof value !== "string" || value.includes("'") ? JSON.stringify(value) : `'${value}'`;
1035
+ }
694
1036
 
695
1037
  // src/lib/url.ts
696
1038
  function toURLSearchParams(params) {
@@ -1083,40 +1425,31 @@ function createApiClient({
1083
1425
  }
1084
1426
  }
1085
1427
  }
1086
- const getAttachmentUrlsBatchStoreByRoom = /* @__PURE__ */ new Map();
1428
+ const attachmentUrlsBatchStoresByRoom = new DefaultMap((roomId) => {
1429
+ const batch2 = new Batch(
1430
+ async (batchedAttachmentIds) => {
1431
+ const attachmentIds = batchedAttachmentIds.flat();
1432
+ const { urls } = await httpClient.post(
1433
+ url`/v2/c/rooms/${roomId}/attachments/presigned-urls`,
1434
+ await authManager.getAuthValue({
1435
+ requestedScope: "comments:read",
1436
+ roomId
1437
+ }),
1438
+ { attachmentIds }
1439
+ );
1440
+ return urls.map(
1441
+ (url2) => url2 ?? new Error("There was an error while getting this attachment's URL")
1442
+ );
1443
+ },
1444
+ { delay: 50 }
1445
+ );
1446
+ return createBatchStore(batch2);
1447
+ });
1087
1448
  function getOrCreateAttachmentUrlsStore(roomId) {
1088
- let store = getAttachmentUrlsBatchStoreByRoom.get(roomId);
1089
- if (store === void 0) {
1090
- const batch2 = new Batch(
1091
- async (batchedAttachmentIds) => {
1092
- const attachmentIds = batchedAttachmentIds.flat();
1093
- const { urls } = await httpClient.post(
1094
- url`/v2/c/rooms/${roomId}/attachments/presigned-urls`,
1095
- await authManager.getAuthValue({
1096
- requestedScope: "comments:read",
1097
- roomId
1098
- }),
1099
- {
1100
- attachmentIds
1101
- }
1102
- );
1103
- return urls.map(
1104
- (url2) => url2 ?? new Error(
1105
- "There was an error while getting this attachment's URL"
1106
- )
1107
- );
1108
- },
1109
- {
1110
- delay: 50
1111
- }
1112
- );
1113
- store = createBatchStore(batch2);
1114
- getAttachmentUrlsBatchStoreByRoom.set(roomId, store);
1115
- }
1116
- return store;
1449
+ return attachmentUrlsBatchStoresByRoom.getOrCreate(roomId);
1117
1450
  }
1118
1451
  function getAttachmentUrl(options) {
1119
- const batch2 = getOrCreateAttachmentUrlsStore(options.roomId).getBatch();
1452
+ const batch2 = getOrCreateAttachmentUrlsStore(options.roomId).batch;
1120
1453
  return batch2.get(options.attachmentId);
1121
1454
  }
1122
1455
  async function getNotificationSettings(options) {
@@ -1142,33 +1475,25 @@ function createApiClient({
1142
1475
  options.settings
1143
1476
  );
1144
1477
  }
1145
- const markInboxNotificationsAsReadBatchByRoom = /* @__PURE__ */ new Map();
1146
- function getOrCreateMarkInboxNotificationsAsReadBatch(roomId) {
1147
- let batch2 = markInboxNotificationsAsReadBatchByRoom.get(roomId);
1148
- if (batch2 === void 0) {
1149
- batch2 = new Batch(
1150
- async (batchedInboxNotificationIds) => {
1151
- const inboxNotificationIds = batchedInboxNotificationIds.flat();
1152
- await httpClient.post(
1153
- url`/v2/c/rooms/${roomId}/inbox-notifications/read`,
1154
- await authManager.getAuthValue({
1155
- requestedScope: "comments:read",
1156
- roomId
1157
- }),
1158
- { inboxNotificationIds }
1159
- );
1160
- return inboxNotificationIds;
1161
- },
1162
- {
1163
- delay: 50
1164
- }
1165
- );
1166
- markInboxNotificationsAsReadBatchByRoom.set(roomId, batch2);
1167
- }
1168
- return batch2;
1169
- }
1478
+ const markAsReadBatchesByRoom = new DefaultMap(
1479
+ (roomId) => new Batch(
1480
+ async (batchedInboxNotificationIds) => {
1481
+ const inboxNotificationIds = batchedInboxNotificationIds.flat();
1482
+ await httpClient.post(
1483
+ url`/v2/c/rooms/${roomId}/inbox-notifications/read`,
1484
+ await authManager.getAuthValue({
1485
+ requestedScope: "comments:read",
1486
+ roomId
1487
+ }),
1488
+ { inboxNotificationIds }
1489
+ );
1490
+ return inboxNotificationIds;
1491
+ },
1492
+ { delay: 50 }
1493
+ )
1494
+ );
1170
1495
  async function markRoomInboxNotificationAsRead(options) {
1171
- const batch2 = getOrCreateMarkInboxNotificationsAsReadBatch(options.roomId);
1496
+ const batch2 = markAsReadBatchesByRoom.getOrCreate(options.roomId);
1172
1497
  return batch2.get(options.inboxNotificationId);
1173
1498
  }
1174
1499
  async function createTextMention(options) {
@@ -1532,14 +1857,7 @@ var HttpClient = class {
1532
1857
  async #fetch(endpoint, authValue, options, params) {
1533
1858
  const response = await this.#rawFetch(endpoint, authValue, options, params);
1534
1859
  if (!response.ok) {
1535
- let error3;
1536
- try {
1537
- const errorBody = await response.json();
1538
- error3 = new HttpError(errorBody.message, response.status, errorBody);
1539
- } catch {
1540
- error3 = new HttpError(response.statusText, response.status);
1541
- }
1542
- throw error3;
1860
+ throw await HttpError.fromResponse(response);
1543
1861
  }
1544
1862
  let body;
1545
1863
  try {
@@ -2166,6 +2484,7 @@ function toNewConnectionStatus(machine) {
2166
2484
  return machine.context.successCount > 0 ? "reconnecting" : "connecting";
2167
2485
  case "@idle.failed":
2168
2486
  return "disconnected";
2487
+ // istanbul ignore next
2169
2488
  default:
2170
2489
  return assertNever(state, "Unknown state");
2171
2490
  }
@@ -2182,13 +2501,6 @@ var StopRetrying = class extends Error {
2182
2501
  super(reason);
2183
2502
  }
2184
2503
  };
2185
- var LiveblocksError = class extends Error {
2186
- /** @internal */
2187
- constructor(message, code) {
2188
- super(message);
2189
- this.code = code;
2190
- }
2191
- };
2192
2504
  function nextBackoffDelay(currentDelay, delays) {
2193
2505
  return delays.find((delay) => delay > currentDelay) ?? delays[delays.length - 1];
2194
2506
  }
@@ -2298,11 +2610,10 @@ var assign = (patch) => (ctx) => ctx.patch(patch);
2298
2610
  function createConnectionStateMachine(delegates, options) {
2299
2611
  const onMessage = makeBufferableEventSource();
2300
2612
  onMessage.pause();
2301
- const onLiveblocksError = makeEventSource();
2302
- function fireErrorEvent(errmsg, errcode) {
2613
+ const onConnectionError = makeEventSource();
2614
+ function fireErrorEvent(message, code) {
2303
2615
  return () => {
2304
- const err = new LiveblocksError(errmsg, errcode);
2305
- onLiveblocksError.notify(err);
2616
+ onConnectionError.notify({ message, code });
2306
2617
  };
2307
2618
  }
2308
2619
  const initialContext = {
@@ -2660,7 +2971,7 @@ function createConnectionStateMachine(delegates, options) {
2660
2971
  didConnect,
2661
2972
  didDisconnect,
2662
2973
  onMessage: onMessage.observable,
2663
- onLiveblocksError: onLiveblocksError.observable
2974
+ onConnectionError: onConnectionError.observable
2664
2975
  }
2665
2976
  };
2666
2977
  }
@@ -3022,458 +3333,205 @@ function sendToPanel(message, options) {
3022
3333
  }
3023
3334
  window.postMessage(fullMsg, "*");
3024
3335
  }
3025
- var eventSource = makeEventSource();
3026
- if (process.env.NODE_ENV !== "production" && typeof window !== "undefined") {
3027
- window.addEventListener("message", (event) => {
3028
- if (event.source === window && event.data?.source === "liveblocks-devtools-panel") {
3029
- eventSource.notify(event.data);
3030
- } else {
3031
- }
3032
- });
3033
- }
3034
- var onMessageFromPanel = eventSource.observable;
3035
-
3036
- // src/devtools/index.ts
3037
- var VERSION = PKG_VERSION || "dev";
3038
- var _devtoolsSetupHasRun = false;
3039
- function setupDevTools(getAllRooms) {
3040
- if (process.env.NODE_ENV === "production" || typeof window === "undefined") {
3041
- return;
3042
- }
3043
- if (_devtoolsSetupHasRun) {
3044
- return;
3045
- }
3046
- _devtoolsSetupHasRun = true;
3047
- onMessageFromPanel.subscribe((msg) => {
3048
- switch (msg.msg) {
3049
- case "connect": {
3050
- activateBridge(true);
3051
- for (const roomId of getAllRooms()) {
3052
- sendToPanel({
3053
- msg: "room::available",
3054
- roomId,
3055
- clientVersion: VERSION
3056
- });
3057
- }
3058
- break;
3059
- }
3060
- }
3061
- });
3062
- sendToPanel({ msg: "wake-up-devtools" }, { force: true });
3063
- }
3064
- var unsubsByRoomId = /* @__PURE__ */ new Map();
3065
- function stopSyncStream(roomId) {
3066
- const unsubs = unsubsByRoomId.get(roomId) ?? [];
3067
- unsubsByRoomId.delete(roomId);
3068
- for (const unsub of unsubs) {
3069
- unsub();
3070
- }
3071
- }
3072
- function startSyncStream(room) {
3073
- stopSyncStream(room.id);
3074
- fullSync(room);
3075
- unsubsByRoomId.set(room.id, [
3076
- // When the connection status changes
3077
- room.events.status.subscribe(() => partialSyncConnection(room)),
3078
- // When storage initializes, send the update
3079
- room.events.storageDidLoad.subscribeOnce(() => partialSyncStorage(room)),
3080
- // Any time storage updates, send the new storage root
3081
- room.events.storageBatch.subscribe(() => partialSyncStorage(room)),
3082
- // Any time "me" or "others" updates, send the new values accordingly
3083
- room.events.self.subscribe(() => partialSyncMe(room)),
3084
- room.events.others.subscribe(() => partialSyncOthers(room)),
3085
- // Any time ydoc is updated, forward the update
3086
- room.events.ydoc.subscribe((update) => syncYdocUpdate(room, update)),
3087
- // Any time a custom room event is received, forward it
3088
- room.events.customEvent.subscribe(
3089
- (eventData) => forwardEvent(room, eventData)
3090
- )
3091
- ]);
3092
- }
3093
- function syncYdocUpdate(room, update) {
3094
- sendToPanel({
3095
- msg: "room::sync::ydoc",
3096
- roomId: room.id,
3097
- update
3098
- });
3099
- }
3100
- var loadedAt = Date.now();
3101
- var eventCounter = 0;
3102
- function nextEventId() {
3103
- return `event-${loadedAt}-${eventCounter++}`;
3104
- }
3105
- function forwardEvent(room, eventData) {
3106
- sendToPanel({
3107
- msg: "room::events::custom-event",
3108
- roomId: room.id,
3109
- event: {
3110
- type: "CustomEvent",
3111
- id: nextEventId(),
3112
- key: "Event",
3113
- connectionId: eventData.connectionId,
3114
- payload: eventData.event
3115
- }
3116
- });
3117
- }
3118
- function partialSyncConnection(room) {
3119
- sendToPanel({
3120
- msg: "room::sync::partial",
3121
- roomId: room.id,
3122
- status: room.getStatus()
3123
- });
3124
- }
3125
- function partialSyncStorage(room) {
3126
- const root = room.getStorageSnapshot();
3127
- if (root) {
3128
- sendToPanel({
3129
- msg: "room::sync::partial",
3130
- roomId: room.id,
3131
- storage: root.toTreeNode("root").payload
3132
- });
3133
- }
3134
- }
3135
- function partialSyncMe(room) {
3136
- const me = room[kInternal].getSelf_forDevTools();
3137
- if (me) {
3138
- sendToPanel({
3139
- msg: "room::sync::partial",
3140
- roomId: room.id,
3141
- me
3142
- });
3143
- }
3144
- }
3145
- function partialSyncOthers(room) {
3146
- const others = room[kInternal].getOthers_forDevTools();
3147
- if (others) {
3148
- sendToPanel({
3149
- msg: "room::sync::partial",
3150
- roomId: room.id,
3151
- others
3152
- });
3153
- }
3154
- }
3155
- function fullSync(room) {
3156
- const root = room.getStorageSnapshot();
3157
- const me = room[kInternal].getSelf_forDevTools();
3158
- const others = room[kInternal].getOthers_forDevTools();
3159
- room.fetchYDoc("");
3160
- sendToPanel({
3161
- msg: "room::sync::full",
3162
- roomId: room.id,
3163
- status: room.getStatus(),
3164
- storage: root?.toTreeNode("root").payload ?? null,
3165
- me,
3166
- others
3167
- });
3168
- }
3169
- var roomChannelListeners = /* @__PURE__ */ new Map();
3170
- function stopRoomChannelListener(roomId) {
3171
- const listener = roomChannelListeners.get(roomId);
3172
- roomChannelListeners.delete(roomId);
3173
- if (listener) {
3174
- listener();
3175
- }
3176
- }
3177
- function linkDevTools(roomId, room) {
3178
- if (process.env.NODE_ENV === "production" || typeof window === "undefined") {
3179
- return;
3180
- }
3181
- sendToPanel({ msg: "room::available", roomId, clientVersion: VERSION });
3182
- stopRoomChannelListener(roomId);
3183
- roomChannelListeners.set(
3184
- roomId,
3185
- // Returns the unsubscribe callback, that we store in the
3186
- // roomChannelListeners registry
3187
- onMessageFromPanel.subscribe((msg) => {
3188
- switch (msg.msg) {
3189
- case "room::subscribe": {
3190
- if (msg.roomId === roomId) {
3191
- startSyncStream(room);
3192
- }
3193
- break;
3194
- }
3195
- case "room::unsubscribe": {
3196
- if (msg.roomId === roomId) {
3197
- stopSyncStream(roomId);
3198
- }
3199
- break;
3200
- }
3201
- }
3202
- })
3203
- );
3204
- }
3205
- function unlinkDevTools(roomId) {
3206
- if (process.env.NODE_ENV === "production" || typeof window === "undefined") {
3207
- return;
3208
- }
3209
- stopSyncStream(roomId);
3210
- stopRoomChannelListener(roomId);
3211
- sendToPanel({
3212
- msg: "room::unavailable",
3213
- roomId
3214
- });
3215
- }
3216
-
3217
- // src/lib/freeze.ts
3218
- var freeze = process.env.NODE_ENV === "production" ? (
3219
- /* istanbul ignore next */
3220
- (x) => x
3221
- ) : Object.freeze;
3222
-
3223
- // src/lib/signals.ts
3224
- var kSinks = Symbol("kSinks");
3225
- var kTrigger = Symbol("kTrigger");
3226
- var signalsToTrigger = null;
3227
- function batch(callback) {
3228
- if (signalsToTrigger !== null) {
3229
- callback();
3230
- return;
3231
- }
3232
- signalsToTrigger = /* @__PURE__ */ new Set();
3233
- try {
3234
- callback();
3235
- } finally {
3236
- for (const signal of signalsToTrigger) {
3237
- signal[kTrigger]();
3238
- }
3239
- signalsToTrigger = null;
3240
- }
3241
- }
3242
- function enqueueTrigger(signal) {
3243
- if (!signalsToTrigger) raise("Expected to be in an active batch");
3244
- signalsToTrigger.add(signal);
3245
- }
3246
- function merge(target, patch) {
3247
- let updated = false;
3248
- const newValue = { ...target };
3249
- Object.keys(patch).forEach((k) => {
3250
- const key = k;
3251
- const val = patch[key];
3252
- if (newValue[key] !== val) {
3253
- if (val === void 0) {
3254
- delete newValue[key];
3255
- } else {
3256
- newValue[key] = val;
3257
- }
3258
- updated = true;
3259
- }
3260
- });
3261
- return updated ? newValue : target;
3262
- }
3263
- var AbstractSignal = class {
3264
- /** @internal */
3265
- equals;
3266
- #eventSource;
3267
- /** @internal */
3268
- [kSinks];
3269
- constructor(equals) {
3270
- this.equals = equals ?? Object.is;
3271
- this.#eventSource = makeEventSource();
3272
- this[kSinks] = /* @__PURE__ */ new Set();
3273
- this.get = this.get.bind(this);
3274
- this.subscribe = this.subscribe.bind(this);
3275
- this.subscribeOnce = this.subscribeOnce.bind(this);
3276
- }
3277
- [Symbol.dispose]() {
3278
- this.#eventSource[Symbol.dispose]();
3279
- this.#eventSource = "(disposed)";
3280
- this.equals = "(disposed)";
3281
- }
3282
- get hasWatchers() {
3283
- if (this.#eventSource.count() > 0) return true;
3284
- for (const sink of this[kSinks]) {
3285
- if (sink.hasWatchers) {
3286
- return true;
3287
- }
3288
- }
3289
- return false;
3290
- }
3291
- [kTrigger]() {
3292
- this.#eventSource.notify();
3293
- for (const sink of this[kSinks]) {
3294
- enqueueTrigger(sink);
3295
- }
3296
- }
3297
- subscribe(callback) {
3298
- return this.#eventSource.subscribe(callback);
3299
- }
3300
- subscribeOnce(callback) {
3301
- const unsub = this.subscribe(() => {
3302
- unsub();
3303
- return callback();
3304
- });
3305
- return unsub;
3306
- }
3307
- waitUntil() {
3308
- throw new Error("waitUntil not supported on Signals");
3309
- }
3310
- markSinksDirty() {
3311
- for (const sink of this[kSinks]) {
3312
- sink.markDirty();
3313
- }
3314
- }
3315
- addSink(sink) {
3316
- this[kSinks].add(sink);
3317
- }
3318
- removeSink(sink) {
3319
- this[kSinks].delete(sink);
3320
- }
3321
- asReadonly() {
3322
- return this;
3323
- }
3324
- };
3325
- var Signal = class extends AbstractSignal {
3326
- #value;
3327
- constructor(value, equals) {
3328
- super(equals);
3329
- this.#value = freeze(value);
3330
- }
3331
- [Symbol.dispose]() {
3332
- super[Symbol.dispose]();
3333
- this.#value = "(disposed)";
3334
- }
3335
- get() {
3336
- return this.#value;
3337
- }
3338
- set(newValue) {
3339
- batch(() => {
3340
- if (typeof newValue === "function") {
3341
- newValue = newValue(this.#value);
3342
- }
3343
- if (!this.equals(this.#value, newValue)) {
3344
- this.#value = freeze(newValue);
3345
- this.markSinksDirty();
3346
- enqueueTrigger(this);
3347
- }
3348
- });
3349
- }
3350
- };
3351
- var PatchableSignal = class extends Signal {
3352
- constructor(data) {
3353
- super(freeze(compactObject(data)));
3354
- }
3355
- set() {
3356
- throw new Error("Don't call .set() directly, use .patch()");
3357
- }
3358
- /**
3359
- * Patches the current object.
3360
- */
3361
- patch(patch) {
3362
- super.set((old) => merge(old, patch));
3363
- }
3364
- };
3365
- var INITIAL = Symbol();
3366
- var DerivedSignal = class _DerivedSignal extends AbstractSignal {
3367
- #prevValue;
3368
- #dirty;
3369
- // When true, the value in #value may not be up-to-date and needs re-checking
3370
- #parents;
3371
- #transform;
3372
- // prettier-ignore
3373
- static from(...args) {
3374
- const last = args.pop();
3375
- if (typeof last !== "function")
3376
- raise("Invalid .from() call, last argument expected to be a function");
3377
- if (typeof args[args.length - 1] === "function") {
3378
- const equals = last;
3379
- const transform = args.pop();
3380
- return new _DerivedSignal(args, transform, equals);
3381
- } else {
3382
- const transform = last;
3383
- return new _DerivedSignal(args, transform);
3384
- }
3385
- }
3386
- constructor(parents, transform, equals) {
3387
- super(equals);
3388
- this.#dirty = true;
3389
- this.#prevValue = INITIAL;
3390
- this.#parents = parents;
3391
- this.#transform = transform;
3392
- for (const parent of parents) {
3393
- parent.addSink(this);
3394
- }
3395
- }
3396
- [Symbol.dispose]() {
3397
- for (const parent of this.#parents) {
3398
- parent.removeSink(this);
3399
- }
3400
- this.#prevValue = "(disposed)";
3401
- this.#parents = "(disposed)";
3402
- this.#transform = "(disposed)";
3403
- }
3404
- get isDirty() {
3405
- return this.#dirty;
3406
- }
3407
- #recompute() {
3408
- const derived = this.#transform(...this.#parents.map((p) => p.get()));
3409
- this.#dirty = false;
3410
- if (!this.equals(this.#prevValue, derived)) {
3411
- this.#prevValue = derived;
3412
- return true;
3336
+ var eventSource = makeEventSource();
3337
+ if (process.env.NODE_ENV !== "production" && typeof window !== "undefined") {
3338
+ window.addEventListener("message", (event) => {
3339
+ if (event.source === window && event.data?.source === "liveblocks-devtools-panel") {
3340
+ eventSource.notify(event.data);
3341
+ } else {
3413
3342
  }
3414
- return false;
3343
+ });
3344
+ }
3345
+ var onMessageFromPanel = eventSource.observable;
3346
+
3347
+ // src/devtools/index.ts
3348
+ var VERSION = PKG_VERSION || "dev";
3349
+ var _devtoolsSetupHasRun = false;
3350
+ function setupDevTools(getAllRooms) {
3351
+ if (process.env.NODE_ENV === "production" || typeof window === "undefined") {
3352
+ return;
3415
3353
  }
3416
- markDirty() {
3417
- if (!this.#dirty) {
3418
- this.#dirty = true;
3419
- this.markSinksDirty();
3420
- }
3354
+ if (_devtoolsSetupHasRun) {
3355
+ return;
3421
3356
  }
3422
- get() {
3423
- if (this.#dirty) {
3424
- this.#recompute();
3357
+ _devtoolsSetupHasRun = true;
3358
+ onMessageFromPanel.subscribe((msg) => {
3359
+ switch (msg.msg) {
3360
+ // When a devtool panel sends an explicit "connect" message back to this
3361
+ // live running client (in response to the "wake-up-devtools" message,
3362
+ // or when the devtool panel is opened for the first time), it means that it's okay to
3363
+ // start emitting messages.
3364
+ // Before this explicit acknowledgement, any call to sendToPanel() will
3365
+ // be a no-op.
3366
+ case "connect": {
3367
+ activateBridge(true);
3368
+ for (const roomId of getAllRooms()) {
3369
+ sendToPanel({
3370
+ msg: "room::available",
3371
+ roomId,
3372
+ clientVersion: VERSION
3373
+ });
3374
+ }
3375
+ break;
3376
+ }
3425
3377
  }
3426
- return this.#prevValue;
3378
+ });
3379
+ sendToPanel({ msg: "wake-up-devtools" }, { force: true });
3380
+ }
3381
+ var unsubsByRoomId = /* @__PURE__ */ new Map();
3382
+ function stopSyncStream(roomId) {
3383
+ const unsubs = unsubsByRoomId.get(roomId) ?? [];
3384
+ unsubsByRoomId.delete(roomId);
3385
+ for (const unsub of unsubs) {
3386
+ unsub();
3427
3387
  }
3428
- /**
3429
- * Called by the Signal system if one or more of the dependent signals have
3430
- * changed. In the case of a DerivedSignal, we'll only want to re-evaluate
3431
- * the actual value if it's being watched, or any of their sinks are being
3432
- * watched actively.
3433
- */
3434
- [kTrigger]() {
3435
- if (!this.hasWatchers) {
3436
- return;
3437
- }
3438
- const updated = this.#recompute();
3439
- if (updated) {
3440
- super[kTrigger]();
3388
+ }
3389
+ function startSyncStream(room) {
3390
+ stopSyncStream(room.id);
3391
+ fullSync(room);
3392
+ unsubsByRoomId.set(room.id, [
3393
+ // When the connection status changes
3394
+ room.events.status.subscribe(() => partialSyncConnection(room)),
3395
+ // When storage initializes, send the update
3396
+ room.events.storageDidLoad.subscribeOnce(() => partialSyncStorage(room)),
3397
+ // Any time storage updates, send the new storage root
3398
+ room.events.storageBatch.subscribe(() => partialSyncStorage(room)),
3399
+ // Any time "me" or "others" updates, send the new values accordingly
3400
+ room.events.self.subscribe(() => partialSyncMe(room)),
3401
+ room.events.others.subscribe(() => partialSyncOthers(room)),
3402
+ // Any time ydoc is updated, forward the update
3403
+ room.events.ydoc.subscribe((update) => syncYdocUpdate(room, update)),
3404
+ // Any time a custom room event is received, forward it
3405
+ room.events.customEvent.subscribe(
3406
+ (eventData) => forwardEvent(room, eventData)
3407
+ )
3408
+ ]);
3409
+ }
3410
+ function syncYdocUpdate(room, update) {
3411
+ sendToPanel({
3412
+ msg: "room::sync::ydoc",
3413
+ roomId: room.id,
3414
+ update
3415
+ });
3416
+ }
3417
+ var loadedAt = Date.now();
3418
+ var eventCounter = 0;
3419
+ function nextEventId() {
3420
+ return `event-${loadedAt}-${eventCounter++}`;
3421
+ }
3422
+ function forwardEvent(room, eventData) {
3423
+ sendToPanel({
3424
+ msg: "room::events::custom-event",
3425
+ roomId: room.id,
3426
+ event: {
3427
+ type: "CustomEvent",
3428
+ id: nextEventId(),
3429
+ key: "Event",
3430
+ connectionId: eventData.connectionId,
3431
+ payload: eventData.event
3441
3432
  }
3433
+ });
3434
+ }
3435
+ function partialSyncConnection(room) {
3436
+ sendToPanel({
3437
+ msg: "room::sync::partial",
3438
+ roomId: room.id,
3439
+ status: room.getStatus()
3440
+ });
3441
+ }
3442
+ function partialSyncStorage(room) {
3443
+ const root = room.getStorageSnapshot();
3444
+ if (root) {
3445
+ sendToPanel({
3446
+ msg: "room::sync::partial",
3447
+ roomId: room.id,
3448
+ storage: root.toTreeNode("root").payload
3449
+ });
3442
3450
  }
3443
- };
3444
- var MutableSignal = class extends AbstractSignal {
3445
- #state;
3446
- constructor(initialState) {
3447
- super();
3448
- this.#state = initialState;
3451
+ }
3452
+ function partialSyncMe(room) {
3453
+ const me = room[kInternal].getSelf_forDevTools();
3454
+ if (me) {
3455
+ sendToPanel({
3456
+ msg: "room::sync::partial",
3457
+ roomId: room.id,
3458
+ me
3459
+ });
3449
3460
  }
3450
- [Symbol.dispose]() {
3451
- super[Symbol.dispose]();
3452
- this.#state = "(disposed)";
3461
+ }
3462
+ function partialSyncOthers(room) {
3463
+ const others = room[kInternal].getOthers_forDevTools();
3464
+ if (others) {
3465
+ sendToPanel({
3466
+ msg: "room::sync::partial",
3467
+ roomId: room.id,
3468
+ others
3469
+ });
3453
3470
  }
3454
- get() {
3455
- return this.#state;
3471
+ }
3472
+ function fullSync(room) {
3473
+ const root = room.getStorageSnapshot();
3474
+ const me = room[kInternal].getSelf_forDevTools();
3475
+ const others = room[kInternal].getOthers_forDevTools();
3476
+ room.fetchYDoc("");
3477
+ sendToPanel({
3478
+ msg: "room::sync::full",
3479
+ roomId: room.id,
3480
+ status: room.getStatus(),
3481
+ storage: root?.toTreeNode("root").payload ?? null,
3482
+ me,
3483
+ others
3484
+ });
3485
+ }
3486
+ var roomChannelListeners = /* @__PURE__ */ new Map();
3487
+ function stopRoomChannelListener(roomId) {
3488
+ const listener = roomChannelListeners.get(roomId);
3489
+ roomChannelListeners.delete(roomId);
3490
+ if (listener) {
3491
+ listener();
3456
3492
  }
3457
- /**
3458
- * Invokes a callback function that is allowed to mutate the given state
3459
- * value. Do not change the value outside of the callback.
3460
- *
3461
- * If the callback explicitly returns `false`, it's assumed that the state
3462
- * was not changed.
3463
- */
3464
- mutate(callback) {
3465
- batch(() => {
3466
- const result = callback ? callback(this.#state) : true;
3467
- if (result !== null && typeof result === "object" && "then" in result) {
3468
- raise("MutableSignal.mutate() does not support async callbacks");
3469
- }
3470
- if (result !== false) {
3471
- this.markSinksDirty();
3472
- enqueueTrigger(this);
3493
+ }
3494
+ function linkDevTools(roomId, room) {
3495
+ if (process.env.NODE_ENV === "production" || typeof window === "undefined") {
3496
+ return;
3497
+ }
3498
+ sendToPanel({ msg: "room::available", roomId, clientVersion: VERSION });
3499
+ stopRoomChannelListener(roomId);
3500
+ roomChannelListeners.set(
3501
+ roomId,
3502
+ // Returns the unsubscribe callback, that we store in the
3503
+ // roomChannelListeners registry
3504
+ onMessageFromPanel.subscribe((msg) => {
3505
+ switch (msg.msg) {
3506
+ // Sent by the devtool panel when it wants to receive the sync stream
3507
+ // for a room
3508
+ case "room::subscribe": {
3509
+ if (msg.roomId === roomId) {
3510
+ startSyncStream(room);
3511
+ }
3512
+ break;
3513
+ }
3514
+ case "room::unsubscribe": {
3515
+ if (msg.roomId === roomId) {
3516
+ stopSyncStream(roomId);
3517
+ }
3518
+ break;
3519
+ }
3473
3520
  }
3474
- });
3521
+ })
3522
+ );
3523
+ }
3524
+ function unlinkDevTools(roomId) {
3525
+ if (process.env.NODE_ENV === "production" || typeof window === "undefined") {
3526
+ return;
3475
3527
  }
3476
- };
3528
+ stopSyncStream(roomId);
3529
+ stopRoomChannelListener(roomId);
3530
+ sendToPanel({
3531
+ msg: "room::unavailable",
3532
+ roomId
3533
+ });
3534
+ }
3477
3535
 
3478
3536
  // src/lib/position.ts
3479
3537
  var MIN_CODE = 32;
@@ -6177,6 +6235,83 @@ var ManagedOthers = class {
6177
6235
  }
6178
6236
  };
6179
6237
 
6238
+ // src/types/LiveblocksError.ts
6239
+ var LiveblocksError = class _LiveblocksError extends Error {
6240
+ context;
6241
+ constructor(message, context, cause) {
6242
+ super(message, { cause });
6243
+ this.context = context;
6244
+ this.name = "LiveblocksError";
6245
+ }
6246
+ /** Convenience accessor for error.context.roomId (if available) */
6247
+ get roomId() {
6248
+ return this.context.roomId;
6249
+ }
6250
+ /** @deprecated Prefer using `context.code` instead, to enable type narrowing */
6251
+ get code() {
6252
+ return this.context.code;
6253
+ }
6254
+ /**
6255
+ * Creates a LiveblocksError from a generic error, by attaching Liveblocks
6256
+ * contextual information like room ID, thread ID, etc.
6257
+ */
6258
+ static from(context, cause) {
6259
+ return new _LiveblocksError(
6260
+ defaultMessageFromContext(context),
6261
+ context,
6262
+ cause
6263
+ );
6264
+ }
6265
+ };
6266
+ function defaultMessageFromContext(context) {
6267
+ switch (context.type) {
6268
+ case "ROOM_CONNECTION_ERROR": {
6269
+ switch (context.code) {
6270
+ case 4001:
6271
+ return "Not allowed to connect to the room";
6272
+ case 4005:
6273
+ return "Room is already full";
6274
+ case 4006:
6275
+ return "Kicked out of the room, because the room ID changed";
6276
+ default:
6277
+ return "Could not connect to the room";
6278
+ }
6279
+ }
6280
+ case "CREATE_THREAD_ERROR":
6281
+ return "Could not create new thread";
6282
+ case "DELETE_THREAD_ERROR":
6283
+ return "Could not delete thread";
6284
+ case "EDIT_THREAD_METADATA_ERROR":
6285
+ return "Could not edit thread metadata";
6286
+ case "MARK_THREAD_AS_RESOLVED_ERROR":
6287
+ return "Could not mark thread as resolved";
6288
+ case "MARK_THREAD_AS_UNRESOLVED_ERROR":
6289
+ return "Could not mark thread as unresolved";
6290
+ case "CREATE_COMMENT_ERROR":
6291
+ return "Could not create new comment";
6292
+ case "EDIT_COMMENT_ERROR":
6293
+ return "Could not edit comment";
6294
+ case "DELETE_COMMENT_ERROR":
6295
+ return "Could not delete comment";
6296
+ case "ADD_REACTION_ERROR":
6297
+ return "Could not add reaction";
6298
+ case "REMOVE_REACTION_ERROR":
6299
+ return "Could not remove reaction";
6300
+ case "MARK_INBOX_NOTIFICATION_AS_READ_ERROR":
6301
+ return "Could not mark inbox notification as read";
6302
+ case "DELETE_INBOX_NOTIFICATION_ERROR":
6303
+ return "Could not delete inbox notification";
6304
+ case "MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR":
6305
+ return "Could not mark all inbox notifications as read";
6306
+ case "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR":
6307
+ return "Could not delete all inbox notifications";
6308
+ case "UPDATE_NOTIFICATION_SETTINGS_ERROR":
6309
+ return "Could not update notification settings";
6310
+ default:
6311
+ return assertNever(context, "Unhandled case");
6312
+ }
6313
+ }
6314
+
6180
6315
  // src/room.ts
6181
6316
  var MAX_SOCKET_MESSAGE_SIZE = 1024 * 1024 - 1024;
6182
6317
  function makeIdFactory(connectionId) {
@@ -6341,13 +6476,17 @@ function createRoom(options, config) {
6341
6476
  managedSocket.events.statusDidChange.subscribe(handleConnectionLossEvent);
6342
6477
  managedSocket.events.didConnect.subscribe(onDidConnect);
6343
6478
  managedSocket.events.didDisconnect.subscribe(onDidDisconnect);
6344
- managedSocket.events.onLiveblocksError.subscribe((err) => {
6345
- if (process.env.NODE_ENV !== "production") {
6346
- error2(
6347
- `Connection to websocket server closed. Reason: ${err.message} (code: ${err.code}).`
6348
- );
6479
+ managedSocket.events.onConnectionError.subscribe(({ message, code }) => {
6480
+ const type = "ROOM_CONNECTION_ERROR";
6481
+ const err = new LiveblocksError(message, { type, code, roomId });
6482
+ const didNotify = config.errorEventSource.notify(err);
6483
+ if (!didNotify) {
6484
+ if (process.env.NODE_ENV !== "production") {
6485
+ error2(
6486
+ `Connection to websocket server closed. Reason: ${message} (code: ${code}).`
6487
+ );
6488
+ }
6349
6489
  }
6350
- eventHub.error.notify(err);
6351
6490
  });
6352
6491
  const pool = {
6353
6492
  roomId: config.roomId,
@@ -6410,7 +6549,6 @@ function createRoom(options, config) {
6410
6549
  self: makeEventSource(),
6411
6550
  myPresence: makeEventSource(),
6412
6551
  others: makeEventSource(),
6413
- error: makeEventSource(),
6414
6552
  storageBatch: makeEventSource(),
6415
6553
  history: makeEventSource(),
6416
6554
  storageDidLoad: makeEventSource(),
@@ -6889,6 +7027,7 @@ function createRoom(options, config) {
6889
7027
  processInitialStorage(message);
6890
7028
  break;
6891
7029
  }
7030
+ // Write event
6892
7031
  case 201 /* UPDATE_STORAGE */: {
6893
7032
  const applyResult = applyOps(message.ops, false);
6894
7033
  for (const [key, value] of applyResult.updates.storageUpdates) {
@@ -6899,6 +7038,11 @@ function createRoom(options, config) {
6899
7038
  }
6900
7039
  break;
6901
7040
  }
7041
+ // Receiving a RejectedOps message in the client means that the server is no
7042
+ // longer in sync with the client. Trying to synchronize the client again by
7043
+ // rolling back particular Ops may be hard/impossible. It's fine to not try and
7044
+ // accept the out-of-sync reality and throw an error. We look at this kind of bug
7045
+ // as a developer-owned bug. In production, these errors are not expected to happen.
6902
7046
  case 299 /* REJECT_STORAGE_OP */: {
6903
7047
  errorWithTitle(
6904
7048
  "Storage mutation rejection error",
@@ -7242,7 +7386,6 @@ ${Array.from(traces).join("\n\n")}`
7242
7386
  others: eventHub.others.observable,
7243
7387
  self: eventHub.self.observable,
7244
7388
  myPresence: eventHub.myPresence.observable,
7245
- error: eventHub.error.observable,
7246
7389
  /** @deprecated */
7247
7390
  storage: eventHub.storageBatch.observable,
7248
7391
  storageBatch: eventHub.storageBatch.observable,
@@ -7433,7 +7576,11 @@ ${Array.from(traces).join("\n\n")}`
7433
7576
  attachmentUrlsStore: httpClient.getOrCreateAttachmentUrlsStore(roomId)
7434
7577
  },
7435
7578
  id: config.roomId,
7436
- subscribe: makeClassicSubscribeFn(events),
7579
+ subscribe: makeClassicSubscribeFn(
7580
+ config.roomId,
7581
+ events,
7582
+ config.errorEventSource
7583
+ ),
7437
7584
  connect: () => managedSocket.connect(),
7438
7585
  reconnect: () => managedSocket.reconnect(),
7439
7586
  disconnect: () => managedSocket.disconnect(),
@@ -7502,7 +7649,7 @@ ${Array.from(traces).join("\n\n")}`
7502
7649
  { enumerable: false }
7503
7650
  );
7504
7651
  }
7505
- function makeClassicSubscribeFn(events) {
7652
+ function makeClassicSubscribeFn(roomId, events, errorEvents) {
7506
7653
  function subscribeToLiveStructureDeeply(node, callback) {
7507
7654
  return events.storageBatch.subscribe((updates) => {
7508
7655
  const relatedUpdates = updates.filter(
@@ -7542,8 +7689,13 @@ function makeClassicSubscribeFn(events) {
7542
7689
  return cb(others, internalEvent);
7543
7690
  });
7544
7691
  }
7545
- case "error":
7546
- return events.error.subscribe(callback);
7692
+ case "error": {
7693
+ return errorEvents.subscribe((err) => {
7694
+ if (err.roomId === roomId) {
7695
+ return callback(err);
7696
+ }
7697
+ });
7698
+ }
7547
7699
  case "status":
7548
7700
  return events.status.subscribe(callback);
7549
7701
  case "lost-connection":
@@ -7560,6 +7712,7 @@ function makeClassicSubscribeFn(events) {
7560
7712
  return events.comments.subscribe(
7561
7713
  callback
7562
7714
  );
7715
+ // istanbul ignore next
7563
7716
  default:
7564
7717
  return assertNever(
7565
7718
  first,
@@ -7714,6 +7867,7 @@ function createClient(options) {
7714
7867
  },
7715
7868
  enableDebugLogging: clientOptions.enableDebugLogging,
7716
7869
  baseUrl,
7870
+ errorEventSource: liveblocksErrorSource,
7717
7871
  unstable_fallbackToHTTP: !!clientOptions.unstable_fallbackToHTTP,
7718
7872
  unstable_streamData: !!clientOptions.unstable_streamData,
7719
7873
  roomHttpClient: httpClient,
@@ -7796,6 +7950,7 @@ function createClient(options) {
7796
7950
  }
7797
7951
  const syncStatusSources = [];
7798
7952
  const syncStatusSignal = new Signal("synchronized");
7953
+ const liveblocksErrorSource = makeEventSource();
7799
7954
  function getSyncStatus() {
7800
7955
  const status = syncStatusSignal.get();
7801
7956
  return status === "synchronizing" ? status : "synchronized";
@@ -7855,6 +8010,7 @@ function createClient(options) {
7855
8010
  },
7856
8011
  getSyncStatus,
7857
8012
  events: {
8013
+ error: liveblocksErrorSource,
7858
8014
  syncStatus: syncStatusSignal
7859
8015
  },
7860
8016
  // Internal
@@ -7870,7 +8026,14 @@ function createClient(options) {
7870
8026
  httpClient,
7871
8027
  // Type-level helper only, it's effectively only an identity-function at runtime
7872
8028
  as: () => client,
7873
- createSyncSource
8029
+ createSyncSource,
8030
+ emitError: (context, cause) => {
8031
+ const error3 = LiveblocksError.from(context, cause);
8032
+ const didNotify = liveblocksErrorSource.notify(error3);
8033
+ if (!didNotify) {
8034
+ error2(error3.message);
8035
+ }
8036
+ }
7874
8037
  }
7875
8038
  },
7876
8039
  kInternal,
@@ -8842,11 +9005,13 @@ export {
8842
9005
  ClientMsgCode,
8843
9006
  CommentsApiError,
8844
9007
  CrdtType,
9008
+ DefaultMap,
8845
9009
  DerivedSignal,
8846
9010
  HttpError,
8847
9011
  LiveList,
8848
9012
  LiveMap,
8849
9013
  LiveObject,
9014
+ LiveblocksError,
8850
9015
  MutableSignal,
8851
9016
  NotificationsApiError,
8852
9017
  OpCode,
@@ -8896,6 +9061,7 @@ export {
8896
9061
  isLiveNode,
8897
9062
  isPlainObject,
8898
9063
  isRootCrdt,
9064
+ isStartsWithOperator,
8899
9065
  kInternal,
8900
9066
  legacy_patchImmutableObject,
8901
9067
  lsonToJson,