@liveblocks/core 3.23.0-file2 → 3.23.0-file4

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.js 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 = "3.23.0-file2";
9
+ var PKG_VERSION = "3.23.0-file4";
10
10
  var PKG_FORMAT = "esm";
11
11
 
12
12
  // src/dupe-detection.ts
@@ -1919,8 +1919,10 @@ var Batch = class {
1919
1919
  this.#clearDelayTimeout();
1920
1920
  }
1921
1921
  };
1922
- function createBatchStore(batch2) {
1922
+ function createBatchStore(batch2, options) {
1923
1923
  const signal = new MutableSignal(/* @__PURE__ */ new Map());
1924
+ const cacheExpiryTimeouts = /* @__PURE__ */ new Map();
1925
+ const pendingEnqueues = /* @__PURE__ */ new Map();
1924
1926
  function getCacheKey(args) {
1925
1927
  return stableStringify(args);
1926
1928
  }
@@ -1935,7 +1937,42 @@ function createBatchStore(batch2) {
1935
1937
  }
1936
1938
  });
1937
1939
  }
1940
+ function clearCacheExpiry(cacheKey) {
1941
+ const timeoutId = cacheExpiryTimeouts.get(cacheKey);
1942
+ if (timeoutId !== void 0) {
1943
+ clearTimeout(timeoutId);
1944
+ cacheExpiryTimeouts.delete(cacheKey);
1945
+ }
1946
+ }
1947
+ function scheduleCacheExpiry(input, expiresAt) {
1948
+ const cacheKey = getCacheKey(input);
1949
+ clearCacheExpiry(cacheKey);
1950
+ if (expiresAt === void 0 || !Number.isFinite(expiresAt)) {
1951
+ return;
1952
+ }
1953
+ const timeoutId = setTimeout(
1954
+ () => {
1955
+ cacheExpiryTimeouts.delete(cacheKey);
1956
+ invalidate([input]);
1957
+ },
1958
+ Math.max(0, expiresAt - Date.now())
1959
+ );
1960
+ cacheExpiryTimeouts.set(cacheKey, timeoutId);
1961
+ }
1938
1962
  function invalidate(inputs) {
1963
+ if (Array.isArray(inputs)) {
1964
+ for (const input of inputs) {
1965
+ const cacheKey = getCacheKey(input);
1966
+ clearCacheExpiry(cacheKey);
1967
+ pendingEnqueues.delete(cacheKey);
1968
+ }
1969
+ } else {
1970
+ for (const timeoutId of cacheExpiryTimeouts.values()) {
1971
+ clearTimeout(timeoutId);
1972
+ }
1973
+ cacheExpiryTimeouts.clear();
1974
+ pendingEnqueues.clear();
1975
+ }
1939
1976
  signal.mutate((cache) => {
1940
1977
  if (Array.isArray(inputs)) {
1941
1978
  for (const input of inputs) {
@@ -1946,23 +1983,54 @@ function createBatchStore(batch2) {
1946
1983
  }
1947
1984
  });
1948
1985
  }
1949
- async function enqueue(input) {
1950
- const cacheKey = getCacheKey(input);
1951
- const cache = signal.get();
1952
- if (cache.has(cacheKey)) {
1953
- return;
1954
- }
1986
+ async function processEnqueue(input, cacheKey, batchResult$, pendingEnqueueId) {
1955
1987
  try {
1956
- update({ key: cacheKey, state: { isLoading: true } });
1957
- const result = await batch2.get(input);
1988
+ const result = await batchResult$;
1989
+ if (pendingEnqueues.get(cacheKey)?.id !== pendingEnqueueId) {
1990
+ return;
1991
+ }
1958
1992
  update({ key: cacheKey, state: { isLoading: false, data: result } });
1993
+ scheduleCacheExpiry(input, options?.getCacheExpiry?.(result));
1959
1994
  } catch (error3) {
1995
+ if (pendingEnqueues.get(cacheKey)?.id !== pendingEnqueueId) {
1996
+ return;
1997
+ }
1998
+ scheduleCacheExpiry(input, options?.getErrorCacheExpiry?.(error3));
1960
1999
  update({
1961
2000
  key: cacheKey,
1962
2001
  state: { isLoading: false, error: error3 }
1963
2002
  });
2003
+ } finally {
2004
+ if (pendingEnqueues.get(cacheKey)?.id === pendingEnqueueId) {
2005
+ pendingEnqueues.delete(cacheKey);
2006
+ }
1964
2007
  }
1965
2008
  }
2009
+ function enqueue(input) {
2010
+ const cacheKey = getCacheKey(input);
2011
+ const existingEnqueue = pendingEnqueues.get(cacheKey);
2012
+ if (existingEnqueue !== void 0) {
2013
+ return existingEnqueue.promise;
2014
+ }
2015
+ const cache = signal.get();
2016
+ if (cache.has(cacheKey)) {
2017
+ return Promise.resolve();
2018
+ }
2019
+ const batchResult$ = batch2.get(input);
2020
+ const pendingEnqueueId = /* @__PURE__ */ Symbol();
2021
+ const pendingEnqueue$ = processEnqueue(
2022
+ input,
2023
+ cacheKey,
2024
+ batchResult$,
2025
+ pendingEnqueueId
2026
+ );
2027
+ pendingEnqueues.set(cacheKey, {
2028
+ id: pendingEnqueueId,
2029
+ promise: pendingEnqueue$
2030
+ });
2031
+ update({ key: cacheKey, state: { isLoading: true } });
2032
+ return pendingEnqueue$;
2033
+ }
1966
2034
  function setData(entries2) {
1967
2035
  update(
1968
2036
  entries2.map((entry) => ({
@@ -1970,6 +2038,9 @@ function createBatchStore(batch2) {
1970
2038
  state: { isLoading: false, data: entry[1] }
1971
2039
  }))
1972
2040
  );
2041
+ for (const [input, output] of entries2) {
2042
+ scheduleCacheExpiry(input, options?.getCacheExpiry?.(output));
2043
+ }
1973
2044
  }
1974
2045
  function getItemState(input) {
1975
2046
  const cacheKey = getCacheKey(input);
@@ -1981,6 +2052,21 @@ function createBatchStore(batch2) {
1981
2052
  const cache = signal.get();
1982
2053
  return cache.get(cacheKey)?.data;
1983
2054
  }
2055
+ function waitUntilItemCacheExpires(input) {
2056
+ const cacheKey = getCacheKey(input);
2057
+ if (!cacheExpiryTimeouts.has(cacheKey)) {
2058
+ return void 0;
2059
+ }
2060
+ const initialState = signal.get().get(cacheKey);
2061
+ return new Promise((resolve) => {
2062
+ const unsubscribe = signal.subscribe(() => {
2063
+ if (signal.get().get(cacheKey) !== initialState) {
2064
+ unsubscribe();
2065
+ resolve();
2066
+ }
2067
+ });
2068
+ });
2069
+ }
1984
2070
  function _cacheKeys() {
1985
2071
  const cache = signal.get();
1986
2072
  return [...cache.keys()];
@@ -1991,6 +2077,7 @@ function createBatchStore(batch2) {
1991
2077
  setData,
1992
2078
  getItemState,
1993
2079
  getData,
2080
+ waitUntilItemCacheExpires,
1994
2081
  invalidate,
1995
2082
  batch: batch2,
1996
2083
  _cacheKeys
@@ -2274,6 +2361,10 @@ function isUrl(string) {
2274
2361
  // src/api-client.ts
2275
2362
  var ROOM_FILE_PART_SIZE = 5 * 1024 * 1024;
2276
2363
  var ROOM_FILE_RETRY_ATTEMPTS = 10;
2364
+ var FILE_URL_EXPIRY_BUFFER = 3e4;
2365
+ var FILE_URL_ERROR_CACHE_TTL = 1e3;
2366
+ var FILE_URL_ERROR_MAX_ATTEMPTS = 3;
2367
+ var FILE_URL_ERROR_ATTEMPTS_EXPIRY = 3e4;
2277
2368
  var ROOM_FILE_RETRY_DELAYS = [
2278
2369
  2e3,
2279
2370
  2e3,
@@ -2286,10 +2377,13 @@ var ROOM_FILE_RETRY_DELAYS = [
2286
2377
  2e3,
2287
2378
  2e3
2288
2379
  ];
2380
+ var FileUrlUnavailableError = class extends Error {
2381
+ };
2289
2382
  async function uploadRoomFile({
2290
2383
  file,
2291
2384
  signal,
2292
2385
  abortErrorMessage,
2386
+ retryMultipartCompletion,
2293
2387
  uploadSingle,
2294
2388
  createMultipartUpload,
2295
2389
  uploadMultipartPart,
@@ -2304,10 +2398,7 @@ async function uploadRoomFile({
2304
2398
  if (signal?.aborted) {
2305
2399
  throw abortError;
2306
2400
  }
2307
- if (err instanceof HttpError && err.status === 413) {
2308
- throw err;
2309
- }
2310
- return false;
2401
+ return err instanceof HttpError && err.status >= 400 && err.status < 500;
2311
2402
  };
2312
2403
  if (file.size <= ROOM_FILE_PART_SIZE) {
2313
2404
  return autoRetry(
@@ -2325,6 +2416,17 @@ async function uploadRoomFile({
2325
2416
  ROOM_FILE_RETRY_DELAYS,
2326
2417
  handleRetryError
2327
2418
  );
2419
+ const partUploadController = new AbortController();
2420
+ const partUploadSignal = partUploadController.signal;
2421
+ const abortPartUploads = (reason) => {
2422
+ partUploadController.abort(reason);
2423
+ };
2424
+ const handleExternalAbort = () => abortPartUploads();
2425
+ if (signal?.aborted) {
2426
+ handleExternalAbort();
2427
+ } else {
2428
+ signal?.addEventListener("abort", handleExternalAbort, { once: true });
2429
+ }
2328
2430
  try {
2329
2431
  uploadId = multipartUpload.uploadId;
2330
2432
  if (signal?.aborted) {
@@ -2332,34 +2434,66 @@ async function uploadRoomFile({
2332
2434
  }
2333
2435
  const batches = chunk(splitFileIntoParts(file), 5);
2334
2436
  for (const parts of batches) {
2335
- const uploadedPartsPromises = [];
2437
+ const firstPartUploadFailure = {};
2438
+ const partUploads$ = [];
2336
2439
  for (const { part, partNumber } of parts) {
2337
- uploadedPartsPromises.push(
2440
+ partUploads$.push(
2338
2441
  autoRetry(
2339
- () => uploadMultipartPart(multipartUpload.uploadId, partNumber, part),
2442
+ () => uploadMultipartPart(
2443
+ multipartUpload.uploadId,
2444
+ partNumber,
2445
+ part,
2446
+ partUploadSignal
2447
+ ),
2340
2448
  ROOM_FILE_RETRY_ATTEMPTS,
2341
2449
  ROOM_FILE_RETRY_DELAYS,
2342
- handleRetryError
2343
- )
2450
+ (error3) => {
2451
+ if (signal?.aborted) {
2452
+ throw abortError;
2453
+ }
2454
+ return partUploadSignal.aborted || handleRetryError(error3);
2455
+ }
2456
+ ).catch((error3) => {
2457
+ if (firstPartUploadFailure.value === void 0) {
2458
+ firstPartUploadFailure.value = { error: error3 };
2459
+ abortPartUploads(error3);
2460
+ }
2461
+ throw error3;
2462
+ })
2344
2463
  );
2345
2464
  }
2346
- uploadedParts.push(...await Promise.all(uploadedPartsPromises));
2465
+ const settledPartUploads = await Promise.allSettled(partUploads$);
2466
+ if (firstPartUploadFailure.value !== void 0) {
2467
+ throw firstPartUploadFailure.value.error;
2468
+ }
2469
+ for (const settledPartUpload of settledPartUploads) {
2470
+ if (settledPartUpload.status === "fulfilled") {
2471
+ uploadedParts.push(settledPartUpload.value);
2472
+ }
2473
+ }
2347
2474
  }
2348
2475
  if (signal?.aborted) {
2349
2476
  throw abortError;
2350
2477
  }
2351
- return completeMultipartUpload(
2352
- uploadId,
2353
- uploadedParts.sort((a, b) => a.partNumber - b.partNumber)
2478
+ const sortedParts = uploadedParts.sort(
2479
+ (a, b) => a.partNumber - b.partNumber
2354
2480
  );
2481
+ return retryMultipartCompletion ? autoRetry(
2482
+ () => completeMultipartUpload(multipartUpload.uploadId, sortedParts),
2483
+ ROOM_FILE_RETRY_ATTEMPTS,
2484
+ ROOM_FILE_RETRY_DELAYS,
2485
+ handleRetryError
2486
+ ) : completeMultipartUpload(uploadId, sortedParts);
2355
2487
  } catch (error3) {
2356
- if (uploadId && isAbortOrTimeoutError(error3)) {
2488
+ if (uploadId) {
2357
2489
  try {
2358
2490
  await abortMultipartUpload(uploadId);
2359
2491
  } catch {
2360
2492
  }
2361
2493
  }
2362
2494
  throw error3;
2495
+ } finally {
2496
+ signal?.removeEventListener("abort", handleExternalAbort);
2363
2497
  }
2364
2498
  }
2365
2499
  function splitFileIntoParts(file) {
@@ -2375,9 +2509,6 @@ function splitFileIntoParts(file) {
2375
2509
  }
2376
2510
  return parts;
2377
2511
  }
2378
- function isAbortOrTimeoutError(error3) {
2379
- return error3 instanceof Error && (error3.name === "AbortError" || error3.name === "TimeoutError");
2380
- }
2381
2512
  function createAbortError(message) {
2382
2513
  if (typeof DOMException === "function") {
2383
2514
  return new DOMException(message, "AbortError");
@@ -2706,6 +2837,7 @@ function createApiClient({
2706
2837
  file: attachment.file,
2707
2838
  signal: options.signal,
2708
2839
  abortErrorMessage: `Upload of attachment ${attachment.id} was aborted.`,
2840
+ retryMultipartCompletion: false,
2709
2841
  uploadSingle: async () => httpClient.putBlob(
2710
2842
  url`/v2/c/rooms/${roomId}/attachments/${attachment.id}/upload/${attachment.name}`,
2711
2843
  await authManager.getAuthValue({
@@ -2728,7 +2860,7 @@ function createApiClient({
2728
2860
  { signal: options.signal },
2729
2861
  { fileSize: attachment.size }
2730
2862
  ),
2731
- uploadMultipartPart: async (uploadId, partNumber, part) => httpClient.putBlob(
2863
+ uploadMultipartPart: async (uploadId, partNumber, part, signal) => httpClient.putBlob(
2732
2864
  url`/v2/c/rooms/${roomId}/attachments/${attachment.id}/multipart/${uploadId}/${String(partNumber)}`,
2733
2865
  await authManager.getAuthValue({
2734
2866
  roomId,
@@ -2737,7 +2869,7 @@ function createApiClient({
2737
2869
  }),
2738
2870
  part,
2739
2871
  void 0,
2740
- { signal: options.signal }
2872
+ { signal }
2741
2873
  ),
2742
2874
  completeMultipartUpload: async (uploadId, parts) => httpClient.post(
2743
2875
  url`/v2/c/rooms/${roomId}/attachments/${attachment.id}/multipart/${uploadId}/complete`,
@@ -2769,6 +2901,7 @@ function createApiClient({
2769
2901
  file,
2770
2902
  signal: options.signal,
2771
2903
  abortErrorMessage: `Upload of file ${fileId} was aborted.`,
2904
+ retryMultipartCompletion: true,
2772
2905
  uploadSingle: async () => httpClient.putBlob(
2773
2906
  url`/v2/c/rooms/${roomId}/storage/files/${fileId}/upload/${file.name}`,
2774
2907
  await authManager.getAuthValue({
@@ -2791,7 +2924,7 @@ function createApiClient({
2791
2924
  { signal: options.signal },
2792
2925
  { fileSize: file.size }
2793
2926
  ),
2794
- uploadMultipartPart: async (uploadId, partNumber, part) => httpClient.putBlob(
2927
+ uploadMultipartPart: async (uploadId, partNumber, part, signal) => httpClient.putBlob(
2795
2928
  url`/v2/c/rooms/${roomId}/storage/files/${fileId}/multipart/${uploadId}/${String(partNumber)}`,
2796
2929
  await authManager.getAuthValue({
2797
2930
  roomId,
@@ -2800,7 +2933,7 @@ function createApiClient({
2800
2933
  }),
2801
2934
  part,
2802
2935
  void 0,
2803
- { signal: options.signal }
2936
+ { signal }
2804
2937
  ),
2805
2938
  completeMultipartUpload: async (uploadId, parts) => httpClient.post(
2806
2939
  url`/v2/c/rooms/${roomId}/storage/files/${fileId}/multipart/${uploadId}/complete`,
@@ -2854,10 +2987,37 @@ function createApiClient({
2854
2987
  return batch2.get(options.attachmentId);
2855
2988
  }
2856
2989
  const fileUrlsBatchStoresByRoom = new DefaultMap((roomId) => {
2990
+ const pendingFileUrlAttempts = /* @__PURE__ */ new Map();
2991
+ const clearPendingFileUrlAttempts = (fileId) => {
2992
+ const attempts = pendingFileUrlAttempts.get(fileId);
2993
+ if (attempts !== void 0) {
2994
+ clearTimeout(attempts.cleanupTimeoutId);
2995
+ pendingFileUrlAttempts.delete(fileId);
2996
+ }
2997
+ };
2998
+ const shouldRetryPendingFileUrl = (fileId) => {
2999
+ const previousAttempts = pendingFileUrlAttempts.get(fileId);
3000
+ if (previousAttempts !== void 0) {
3001
+ clearTimeout(previousAttempts.cleanupTimeoutId);
3002
+ }
3003
+ const count = (previousAttempts?.count ?? 0) + 1;
3004
+ if (count >= FILE_URL_ERROR_MAX_ATTEMPTS) {
3005
+ pendingFileUrlAttempts.delete(fileId);
3006
+ return false;
3007
+ }
3008
+ pendingFileUrlAttempts.set(fileId, {
3009
+ count,
3010
+ cleanupTimeoutId: setTimeout(
3011
+ () => pendingFileUrlAttempts.delete(fileId),
3012
+ FILE_URL_ERROR_ATTEMPTS_EXPIRY
3013
+ )
3014
+ });
3015
+ return true;
3016
+ };
2857
3017
  const batch2 = new Batch(
2858
3018
  async (batchedFileIds) => {
2859
3019
  const fileIds = batchedFileIds.flat();
2860
- const { urls } = await httpClient.post(
3020
+ const { urls, expiresAt } = await httpClient.post(
2861
3021
  url`/v2/c/rooms/${roomId}/storage/files/presigned-urls`,
2862
3022
  await authManager.getAuthValue({
2863
3023
  roomId,
@@ -2866,20 +3026,31 @@ function createApiClient({
2866
3026
  }),
2867
3027
  { fileIds }
2868
3028
  );
2869
- return urls.map(
2870
- (url2) => url2 ?? new Error("There was an error while getting this file's URL")
2871
- );
3029
+ const expiresAtTimestamp = Date.parse(expiresAt);
3030
+ return urls.map((url2, index) => {
3031
+ const fileId = fileIds[index];
3032
+ if (url2 === false) {
3033
+ return shouldRetryPendingFileUrl(fileId) ? new FileUrlUnavailableError(
3034
+ "There was an error while getting this file's URL"
3035
+ ) : new Error("There was an error while getting this file's URL");
3036
+ }
3037
+ clearPendingFileUrlAttempts(fileId);
3038
+ return url2 !== null ? { url: url2, expiresAt: expiresAtTimestamp } : new Error("There was an error while getting this file's URL");
3039
+ });
2872
3040
  },
2873
3041
  { delay: 50 }
2874
3042
  );
2875
- return createBatchStore(batch2);
3043
+ return createBatchStore(batch2, {
3044
+ getCacheExpiry: ({ expiresAt }) => expiresAt - FILE_URL_EXPIRY_BUFFER,
3045
+ getErrorCacheExpiry: (error3) => error3 instanceof FileUrlUnavailableError ? Date.now() + FILE_URL_ERROR_CACHE_TTL : void 0
3046
+ });
2876
3047
  });
2877
3048
  function getOrCreateFileUrlsStore(roomId) {
2878
3049
  return fileUrlsBatchStoresByRoom.getOrCreate(roomId);
2879
3050
  }
2880
- function getFileUrl(options) {
3051
+ async function getFileUrl(options) {
2881
3052
  const batch2 = getOrCreateFileUrlsStore(options.roomId).batch;
2882
- return batch2.get(options.fileId);
3053
+ return (await batch2.get(options.fileId)).url;
2883
3054
  }
2884
3055
  async function getSubscriptionSettings(options) {
2885
3056
  return httpClient.get(
@@ -8247,6 +8418,8 @@ function reconcile(live, json, config) {
8247
8418
  return reconcileLiveList(live, json, config);
8248
8419
  } else if (isLiveMap(live) && isPlainObject(json)) {
8249
8420
  return reconcileLiveMap(live, config);
8421
+ } else if (isLiveFile(live) && shallow(live.data, json)) {
8422
+ return live;
8250
8423
  } else {
8251
8424
  return deepLiveify(json, config);
8252
8425
  }
@@ -9173,8 +9346,31 @@ function isJsonEq(a, b) {
9173
9346
  }
9174
9347
  function diffNodeMap(prev, next) {
9175
9348
  const ops = [];
9176
- prev.forEach((_, id) => {
9177
- if (!next.get(id)) {
9349
+ const idsToRecreate = /* @__PURE__ */ new Set();
9350
+ next.forEach((nextCrdt, id) => {
9351
+ const currentCrdt = prev.get(id);
9352
+ if (currentCrdt === void 0) {
9353
+ return;
9354
+ }
9355
+ if (currentCrdt.type !== nextCrdt.type || currentCrdt.type === CrdtType.FILE && nextCrdt.type === CrdtType.FILE && (currentCrdt.data.id !== nextCrdt.data.id || currentCrdt.data.name !== nextCrdt.data.name || currentCrdt.data.size !== nextCrdt.data.size || currentCrdt.data.mimeType !== nextCrdt.data.mimeType)) {
9356
+ idsToRecreate.add(id);
9357
+ }
9358
+ });
9359
+ let foundDescendant = true;
9360
+ while (foundDescendant) {
9361
+ foundDescendant = false;
9362
+ for (const nodes of [prev, next]) {
9363
+ nodes.forEach((crdt, id) => {
9364
+ if (!idsToRecreate.has(id) && crdt.parentId !== void 0 && idsToRecreate.has(crdt.parentId)) {
9365
+ idsToRecreate.add(id);
9366
+ foundDescendant = true;
9367
+ }
9368
+ });
9369
+ }
9370
+ }
9371
+ prev.forEach((crdt, id) => {
9372
+ const parentWillBeRecreated = crdt.parentId !== void 0 && idsToRecreate.has(crdt.parentId);
9373
+ if ((!next.has(id) || idsToRecreate.has(id)) && !parentWillBeRecreated) {
9178
9374
  ops.push({ type: OpCode.DELETE_CRDT, id });
9179
9375
  }
9180
9376
  });
@@ -9185,7 +9381,7 @@ function diffNodeMap(prev, next) {
9185
9381
  }
9186
9382
  emitted.add(id);
9187
9383
  const parentId = crdt.parentId;
9188
- if (parentId !== void 0 && !prev.has(parentId)) {
9384
+ if (parentId !== void 0 && (!prev.has(parentId) || idsToRecreate.has(parentId))) {
9189
9385
  const parentCrdt = next.get(parentId);
9190
9386
  if (parentCrdt !== void 0) {
9191
9387
  emitCreate(parentId, parentCrdt);
@@ -9244,29 +9440,25 @@ function diffNodeMap(prev, next) {
9244
9440
  }
9245
9441
  next.forEach((crdt, id) => {
9246
9442
  const currentCrdt = prev.get(id);
9247
- if (currentCrdt) {
9248
- if (crdt.type === CrdtType.OBJECT) {
9249
- if (currentCrdt.type !== CrdtType.OBJECT) {
9250
- ops.push({ type: OpCode.UPDATE_OBJECT, id, data: crdt.data });
9251
- } else {
9252
- const changed = /* @__PURE__ */ new Map();
9253
- for (const key of Object.keys(crdt.data)) {
9254
- const value = crdt.data[key];
9255
- if (value !== void 0 && !isJsonEq(value, currentCrdt.data[key])) {
9256
- changed.set(key, value);
9257
- }
9258
- }
9259
- if (changed.size > 0) {
9260
- ops.push({
9261
- type: OpCode.UPDATE_OBJECT,
9262
- id,
9263
- data: Object.fromEntries(changed)
9264
- });
9443
+ if (currentCrdt && !idsToRecreate.has(id)) {
9444
+ if (crdt.type === CrdtType.OBJECT && currentCrdt.type === CrdtType.OBJECT) {
9445
+ const changed = /* @__PURE__ */ new Map();
9446
+ for (const key of Object.keys(crdt.data)) {
9447
+ const value = crdt.data[key];
9448
+ if (value !== void 0 && !isJsonEq(value, currentCrdt.data[key])) {
9449
+ changed.set(key, value);
9265
9450
  }
9266
- for (const key of Object.keys(currentCrdt.data)) {
9267
- if (!(key in crdt.data)) {
9268
- ops.push({ type: OpCode.DELETE_OBJECT_KEY, id, key });
9269
- }
9451
+ }
9452
+ if (changed.size > 0) {
9453
+ ops.push({
9454
+ type: OpCode.UPDATE_OBJECT,
9455
+ id,
9456
+ data: Object.fromEntries(changed)
9457
+ });
9458
+ }
9459
+ for (const key of Object.keys(currentCrdt.data)) {
9460
+ if (!(key in crdt.data)) {
9461
+ ops.push({ type: OpCode.DELETE_OBJECT_KEY, id, key });
9270
9462
  }
9271
9463
  }
9272
9464
  }