jazz-tools 0.19.11 → 0.19.12

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.
Files changed (44) hide show
  1. package/.turbo/turbo-build.log +46 -46
  2. package/CHANGELOG.md +12 -0
  3. package/dist/{chunk-HX5S6W5E.js → chunk-AGF4HEDH.js} +56 -27
  4. package/dist/chunk-AGF4HEDH.js.map +1 -0
  5. package/dist/index.js +1 -1
  6. package/dist/inspector/{chunk-C6BJPHBQ.js → chunk-YQNK5Y7B.js} +47 -35
  7. package/dist/inspector/chunk-YQNK5Y7B.js.map +1 -0
  8. package/dist/inspector/{custom-element-GJVBPZES.js → custom-element-KYV64IOC.js} +47 -35
  9. package/dist/inspector/{custom-element-GJVBPZES.js.map → custom-element-KYV64IOC.js.map} +1 -1
  10. package/dist/inspector/index.js +1 -1
  11. package/dist/inspector/register-custom-element.js +1 -1
  12. package/dist/inspector/standalone.js +1 -1
  13. package/dist/inspector/tests/utils/transactions-changes.test.d.ts +2 -0
  14. package/dist/inspector/tests/utils/transactions-changes.test.d.ts.map +1 -0
  15. package/dist/inspector/utils/transactions-changes.d.ts +13 -13
  16. package/dist/inspector/utils/transactions-changes.d.ts.map +1 -1
  17. package/dist/react/index.js +4 -1
  18. package/dist/react/index.js.map +1 -1
  19. package/dist/react/provider.d.ts.map +1 -1
  20. package/dist/react-core/index.js +2 -2
  21. package/dist/react-core/index.js.map +1 -1
  22. package/dist/react-native/index.js +4 -1
  23. package/dist/react-native/index.js.map +1 -1
  24. package/dist/react-native-core/index.js +4 -1
  25. package/dist/react-native-core/index.js.map +1 -1
  26. package/dist/react-native-core/provider.d.ts.map +1 -1
  27. package/dist/testing.js +1 -1
  28. package/dist/tools/implementation/ContextManager.d.ts.map +1 -1
  29. package/dist/tools/subscribe/SubscriptionScope.d.ts +3 -6
  30. package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
  31. package/package.json +4 -4
  32. package/src/inspector/tests/utils/transactions-changes.test.ts +102 -0
  33. package/src/inspector/ui/icons/add-icon.tsx +3 -3
  34. package/src/inspector/utils/history.ts +6 -6
  35. package/src/inspector/utils/transactions-changes.ts +37 -3
  36. package/src/inspector/viewer/history-view.tsx +13 -13
  37. package/src/react/provider.tsx +6 -1
  38. package/src/react-core/hooks.ts +2 -2
  39. package/src/react-core/tests/useSuspenseCoState.test.tsx +47 -0
  40. package/src/react-native-core/provider.tsx +6 -1
  41. package/src/tools/implementation/ContextManager.ts +10 -0
  42. package/src/tools/subscribe/SubscriptionScope.ts +61 -39
  43. package/dist/chunk-HX5S6W5E.js.map +0 -1
  44. package/dist/inspector/chunk-C6BJPHBQ.js.map +0 -1
@@ -128,7 +128,7 @@ function mapTransactionToAction(
128
128
  coValue: RawCoValue,
129
129
  ): string {
130
130
  // Group changes
131
- if (TransactionChanges.isUserPromotion(change)) {
131
+ if (TransactionChanges.isUserPromotion(coValue, change)) {
132
132
  if (change.value === "revoked") {
133
133
  return `${change.key} has been revoked`;
134
134
  }
@@ -136,28 +136,28 @@ function mapTransactionToAction(
136
136
  return `${change.key} has been promoted to ${change.value}`;
137
137
  }
138
138
 
139
- if (TransactionChanges.isGroupExtension(change)) {
139
+ if (TransactionChanges.isGroupExtension(coValue, change)) {
140
140
  const child = change.key.slice(6);
141
141
  return `Group became a member of ${child}`;
142
142
  }
143
143
 
144
- if (TransactionChanges.isGroupExtendRevocation(change)) {
144
+ if (TransactionChanges.isGroupExtendRevocation(coValue, change)) {
145
145
  const child = change.key.slice(6);
146
146
  return `Group's membership of ${child} has been revoked.`;
147
147
  }
148
148
 
149
- if (TransactionChanges.isGroupPromotion(change)) {
149
+ if (TransactionChanges.isGroupPromotion(coValue, change)) {
150
150
  const parent = change.key.slice(7);
151
151
  return `Group ${parent} has been promoted to ${change.value}`;
152
152
  }
153
153
 
154
- if (TransactionChanges.isKeyRevelation(change)) {
154
+ if (TransactionChanges.isKeyRevelation(coValue, change)) {
155
155
  const [key, target] = change.key.split("_for_");
156
156
  return `Key "${key}" has been revealed to "${target}"`;
157
157
  }
158
158
 
159
159
  // coList changes
160
- if (TransactionChanges.isItemAppend(change)) {
160
+ if (TransactionChanges.isItemAppend(coValue, change)) {
161
161
  if (change.after === "start") {
162
162
  return `"${change.value}" has been appended`;
163
163
  }
@@ -171,7 +171,7 @@ function mapTransactionToAction(
171
171
  return `"${change.value}" has been inserted after "${(after as any).value}"`;
172
172
  }
173
173
 
174
- if (TransactionChanges.isItemPrepend(change)) {
174
+ if (TransactionChanges.isItemPrepend(coValue, change)) {
175
175
  if (change.before === "end") {
176
176
  return `"${change.value}" has been prepended`;
177
177
  }
@@ -185,7 +185,7 @@ function mapTransactionToAction(
185
185
  return `"${change.value}" has been inserted before "${(before as any).value}"`;
186
186
  }
187
187
 
188
- if (TransactionChanges.isItemDeletion(change)) {
188
+ if (TransactionChanges.isItemDeletion(coValue, change)) {
189
189
  const insertion = findListChange(change.insertion, coValue);
190
190
  if (insertion === undefined) {
191
191
  return `An undefined item has been deleted`;
@@ -195,24 +195,24 @@ function mapTransactionToAction(
195
195
  }
196
196
 
197
197
  // coStream changes
198
- if (TransactionChanges.isStreamStart(change)) {
198
+ if (TransactionChanges.isStreamStart(coValue, change)) {
199
199
  return `Stream started with mime type "${change.mimeType}" and file name "${change.fileName}"`;
200
200
  }
201
201
 
202
- if (TransactionChanges.isStreamChunk(change)) {
202
+ if (TransactionChanges.isStreamChunk(coValue, change)) {
203
203
  return `Stream chunk added`;
204
204
  }
205
205
 
206
- if (TransactionChanges.isStreamEnd(change)) {
206
+ if (TransactionChanges.isStreamEnd(coValue, change)) {
207
207
  return `Stream ended`;
208
208
  }
209
209
 
210
210
  // coMap changes
211
- if (TransactionChanges.isPropertySet(change)) {
211
+ if (TransactionChanges.isPropertySet(coValue, change)) {
212
212
  return `Property "${change.key}" has been set to ${JSON.stringify(change.value)}`;
213
213
  }
214
214
 
215
- if (TransactionChanges.isPropertyDeletion(change)) {
215
+ if (TransactionChanges.isPropertyDeletion(coValue, change)) {
216
216
  return `Property "${change.key}" has been deleted`;
217
217
  }
218
218
 
@@ -58,6 +58,9 @@ export function JazzReactProvider<
58
58
  );
59
59
  const logoutReplacementActiveRef = useRef(false);
60
60
  logoutReplacementActiveRef.current = Boolean(logOutReplacement);
61
+ const onAnonymousAccountDiscardedEnabled = Boolean(
62
+ onAnonymousAccountDiscarded,
63
+ );
61
64
 
62
65
  const value = React.useSyncExternalStore<
63
66
  JazzContextType<InstanceOfSchema<S>> | undefined
@@ -74,7 +77,9 @@ export function JazzReactProvider<
74
77
  logOutReplacement: logoutReplacementActiveRef.current
75
78
  ? logOutReplacementRefCallback
76
79
  : undefined,
77
- onAnonymousAccountDiscarded: onAnonymousAccountDiscardedRefCallback,
80
+ onAnonymousAccountDiscarded: onAnonymousAccountDiscardedEnabled
81
+ ? onAnonymousAccountDiscardedRefCallback
82
+ : undefined,
78
83
  } satisfies JazzContextManagerProps<S>;
79
84
 
80
85
  if (contextManager.propsChanged(props)) {
@@ -489,7 +489,7 @@ export function useSuspenseCoState<
489
489
  throw new Error("Subscription not found");
490
490
  }
491
491
 
492
- use(subscription.getPromise());
492
+ use(subscription.getCachedPromise());
493
493
 
494
494
  const getCurrentValue = () => {
495
495
  const value = subscription.getCurrentValue();
@@ -824,7 +824,7 @@ export function useSuspenseAccount<
824
824
  );
825
825
  }
826
826
 
827
- use(subscription.getPromise());
827
+ use(subscription.getCachedPromise());
828
828
 
829
829
  const getCurrentValue = () => {
830
830
  const value = subscription.getCurrentValue();
@@ -370,6 +370,53 @@ describe("useSuspenseCoState", () => {
370
370
  });
371
371
  });
372
372
 
373
+ it("should throw error when CoValue becomes unauthorized", async () => {
374
+ const TestMap = co.map({
375
+ value: z.string(),
376
+ });
377
+
378
+ const group = Group.create();
379
+ group.addMember("everyone", "reader");
380
+
381
+ // Create CoValue owned by another account without sharing
382
+ const map = TestMap.create(
383
+ {
384
+ value: "123",
385
+ },
386
+ group,
387
+ );
388
+
389
+ await createJazzTestAccount({
390
+ isCurrentActiveAccount: true,
391
+ });
392
+
393
+ const TestComponent = () => {
394
+ const value = useSuspenseCoState(TestMap, map.$jazz.id);
395
+ return <div>{value.value}</div>;
396
+ };
397
+
398
+ const { container } = await act(async () => {
399
+ return render(
400
+ <ErrorBoundary fallback={<div>Error!</div>}>
401
+ <Suspense fallback={<div>Loading...</div>}>
402
+ <TestComponent />
403
+ </Suspense>
404
+ </ErrorBoundary>,
405
+ );
406
+ });
407
+ await waitFor(() => {
408
+ expect(container.textContent).toContain("123");
409
+ expect(container.textContent).not.toContain("Loading...");
410
+ });
411
+
412
+ group.removeMember("everyone");
413
+
414
+ // Wait for error to be thrown (unauthorized access)
415
+ await waitFor(() => {
416
+ expect(container.textContent).toContain("Error!");
417
+ });
418
+ });
419
+
373
420
  it("should update value when CoValue changes", async () => {
374
421
  const TestMap = co.map({
375
422
  value: z.string(),
@@ -55,6 +55,9 @@ export function JazzProviderCore<
55
55
  );
56
56
  const logoutReplacementActiveRef = useRef(false);
57
57
  logoutReplacementActiveRef.current = Boolean(logOutReplacement);
58
+ const onAnonymousAccountDiscardedEnabled = Boolean(
59
+ onAnonymousAccountDiscarded,
60
+ );
58
61
 
59
62
  const value = React.useSyncExternalStore<
60
63
  JazzContextType<InstanceOfSchema<S>> | undefined
@@ -71,7 +74,9 @@ export function JazzProviderCore<
71
74
  logOutReplacement: logoutReplacementActiveRef.current
72
75
  ? logOutReplacementRefCallback
73
76
  : undefined,
74
- onAnonymousAccountDiscarded: onAnonymousAccountDiscardedRefCallback,
77
+ onAnonymousAccountDiscarded: onAnonymousAccountDiscardedEnabled
78
+ ? onAnonymousAccountDiscardedRefCallback
79
+ : undefined,
75
80
  CryptoProvider,
76
81
  } satisfies JazzContextManagerProps<S>;
77
82
 
@@ -351,6 +351,16 @@ export class JazzContextManager<
351
351
  // The storage is reachable through currentContext using the connectedPeers
352
352
  prevContext.node.removeStorage();
353
353
 
354
+ // Ensure that the new context is the only peer connected to the previous context
355
+ // This way all the changes made in the previous context are synced only to the new context
356
+ for (const peer of Object.values(prevContext.node.syncManager.peers)) {
357
+ if (!peer.closed) {
358
+ peer.gracefulShutdown();
359
+ }
360
+ }
361
+
362
+ prevContext.node.syncManager.peers = {};
363
+
354
364
  currentContext.node.syncManager.addPeer(prevAccountAsPeer);
355
365
  prevContext.node.syncManager.addPeer(currentAccountAsPeer);
356
366
 
@@ -309,64 +309,86 @@ export class SubscriptionScope<D extends CoValue> {
309
309
 
310
310
  unloadedValue: NotLoaded<D> | undefined;
311
311
 
312
- lastPromise:
313
- | {
314
- value: MaybeLoaded<D> | undefined;
315
- promise: PromiseWithStatus<MaybeLoaded<D>>;
316
- }
317
- | undefined;
318
-
319
- cachePromise(value: MaybeLoaded<D>, callback: () => PromiseWithStatus<D>) {
320
- if (this.lastPromise?.value === value) {
321
- return this.lastPromise.promise;
322
- }
323
-
324
- const promise = callback();
325
- this.lastPromise = { value, promise };
326
-
327
- return promise;
328
- }
312
+ lastPromise: PromiseWithStatus<D> | undefined;
329
313
 
330
314
  getPromise() {
331
315
  const currentValue = this.getCurrentValue();
332
316
 
333
317
  if (currentValue.$isLoaded) {
334
- return resolvedPromise(currentValue);
318
+ return resolvedPromise<D>(currentValue);
335
319
  }
336
320
 
337
321
  if (currentValue.$jazz.loadingState !== CoValueLoadingState.LOADING) {
338
322
  const error = this.getError();
339
- return rejectedPromise(
323
+ return rejectedPromise<D>(
340
324
  new Error(error?.toString() ?? "Unknown error", {
341
325
  cause: this.callerStack,
342
326
  }),
343
327
  );
344
328
  }
345
329
 
346
- // We need to cache rejected promises to make React Suspense happy
347
- return this.cachePromise(currentValue, () => {
348
- return new Promise<D>((resolve, reject) => {
349
- const unsubscribe = this.subscribe(() => {
350
- const currentValue = this.getCurrentValue();
330
+ const promise = new Promise<D>((resolve, reject) => {
331
+ const unsubscribe = this.subscribe(() => {
332
+ const currentValue = this.getCurrentValue();
351
333
 
352
- if (currentValue.$jazz.loadingState === CoValueLoadingState.LOADING) {
353
- return;
354
- }
334
+ if (currentValue.$jazz.loadingState === CoValueLoadingState.LOADING) {
335
+ return;
336
+ }
355
337
 
356
- if (currentValue.$isLoaded) {
357
- resolve(currentValue);
358
- } else {
359
- reject(
360
- new Error(this.getError()?.toString() ?? "Unknown error", {
361
- cause: this.callerStack,
362
- }),
363
- );
364
- }
338
+ if (currentValue.$isLoaded) {
339
+ promise.status = "fulfilled";
340
+ promise.value = currentValue;
341
+ resolve(currentValue);
342
+ } else {
343
+ promise.status = "rejected";
344
+ promise.reason = new Error(
345
+ this.getError()?.toString() ?? "Unknown error",
346
+ {
347
+ cause: this.callerStack,
348
+ },
349
+ );
350
+ reject(
351
+ new Error(this.getError()?.toString() ?? "Unknown error", {
352
+ cause: this.callerStack,
353
+ }),
354
+ );
355
+ }
365
356
 
366
- unsubscribe();
367
- });
357
+ unsubscribe();
368
358
  });
369
- });
359
+ }) as PromiseWithStatus<D>;
360
+
361
+ promise.status = "pending";
362
+
363
+ return promise;
364
+ }
365
+
366
+ getCachedPromise() {
367
+ if (this.lastPromise) {
368
+ const value = this.getCurrentValue();
369
+
370
+ // if the value is loaded, we update the promise state
371
+ // to ensure that the value provided is always up to date
372
+ if (value.$isLoaded) {
373
+ this.lastPromise.status = "fulfilled";
374
+ this.lastPromise.value = value;
375
+ } else if (value.$jazz.loadingState !== CoValueLoadingState.LOADING) {
376
+ this.lastPromise.status = "rejected";
377
+ this.lastPromise.reason = new Error(
378
+ this.getError()?.toString() ?? "Unknown error",
379
+ {
380
+ cause: this.callerStack,
381
+ },
382
+ );
383
+ } else if (this.lastPromise.status !== "pending") {
384
+ // Value got into loading state, we need to suspend again
385
+ this.lastPromise = this.getPromise();
386
+ }
387
+ } else {
388
+ this.lastPromise = this.getPromise();
389
+ }
390
+
391
+ return this.lastPromise;
370
392
  }
371
393
 
372
394
  private getUnloadedValue(reason: NotLoadedCoValueState): NotLoaded<D> {