jazz-tools 0.15.0 → 0.15.1

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 (48) hide show
  1. package/.turbo/turbo-build.log +44 -44
  2. package/CHANGELOG.md +12 -0
  3. package/dist/{chunk-FSIM7N33.js → chunk-VBDJM6Z5.js} +142 -31
  4. package/dist/chunk-VBDJM6Z5.js.map +1 -0
  5. package/dist/index.js +1 -1
  6. package/dist/inspector/index.js +30 -4
  7. package/dist/inspector/index.js.map +1 -1
  8. package/dist/inspector/viewer/new-app.d.ts.map +1 -1
  9. package/dist/inspector/viewer/use-open-inspector.d.ts +2 -0
  10. package/dist/inspector/viewer/use-open-inspector.d.ts.map +1 -0
  11. package/dist/inspector/viewer/use-page-path.d.ts.map +1 -1
  12. package/dist/react/index.js +0 -2
  13. package/dist/react/index.js.map +1 -1
  14. package/dist/react/testing.js +0 -2
  15. package/dist/react/testing.js.map +1 -1
  16. package/dist/react-native-core/index.js +2 -18
  17. package/dist/react-native-core/index.js.map +1 -1
  18. package/dist/react-native-core/media.d.ts.map +1 -1
  19. package/dist/testing.js +1 -1
  20. package/dist/tools/coValues/coFeed.d.ts +9 -0
  21. package/dist/tools/coValues/coFeed.d.ts.map +1 -1
  22. package/dist/tools/coValues/coMap.d.ts +98 -2
  23. package/dist/tools/coValues/coMap.d.ts.map +1 -1
  24. package/dist/tools/coValues/interfaces.d.ts +3 -0
  25. package/dist/tools/coValues/interfaces.d.ts.map +1 -1
  26. package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts +12 -0
  27. package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts.map +1 -1
  28. package/dist/tools/implementation/zodSchema/zodCo.d.ts.map +1 -1
  29. package/dist/tools/subscribe/CoValueCoreSubscription.d.ts +2 -1
  30. package/dist/tools/subscribe/CoValueCoreSubscription.d.ts.map +1 -1
  31. package/dist/tools/subscribe/SubscriptionScope.d.ts +2 -1
  32. package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
  33. package/package.json +5 -5
  34. package/src/inspector/viewer/new-app.tsx +2 -1
  35. package/src/inspector/viewer/use-open-inspector.ts +18 -0
  36. package/src/inspector/viewer/use-page-path.ts +14 -1
  37. package/src/react-native-core/media.tsx +2 -22
  38. package/src/tools/coValues/coFeed.ts +38 -0
  39. package/src/tools/coValues/coMap.ts +118 -14
  40. package/src/tools/coValues/interfaces.ts +14 -4
  41. package/src/tools/implementation/zodSchema/schemaTypes/CoMapSchema.ts +38 -0
  42. package/src/tools/implementation/zodSchema/zodCo.ts +6 -0
  43. package/src/tools/subscribe/CoValueCoreSubscription.ts +12 -9
  44. package/src/tools/subscribe/SubscriptionScope.ts +31 -19
  45. package/src/tools/tests/coFeed.test.ts +69 -0
  46. package/src/tools/tests/coMap.test.ts +480 -4
  47. package/src/tools/tests/load.test.ts +2 -1
  48. package/dist/chunk-FSIM7N33.js.map +0 -1
@@ -10,12 +10,10 @@ import {
10
10
  } from "cojson";
11
11
  import type {
12
12
  AnonymousJazzAgent,
13
- AnyAccountSchema,
14
13
  CoValue,
15
14
  CoValueClass,
16
15
  Group,
17
16
  ID,
18
- InstanceOfSchema,
19
17
  RefEncoded,
20
18
  RefIfCoValue,
21
19
  RefsToResolve,
@@ -319,18 +317,7 @@ export class CoMap extends CoValueBase implements CoValue {
319
317
  ) {
320
318
  const instance = new this();
321
319
 
322
- const { owner, uniqueness } = parseCoValueCreateOptions(options);
323
- const raw = instance.rawFromInit(init, owner, uniqueness);
324
-
325
- Object.defineProperties(instance, {
326
- id: {
327
- value: raw.id,
328
- enumerable: false,
329
- },
330
- _raw: { value: raw, enumerable: false },
331
- });
332
-
333
- return instance;
320
+ return instance._createCoMap(init, options);
334
321
  }
335
322
 
336
323
  /**
@@ -388,6 +375,30 @@ export class CoMap extends CoValueBase implements CoValue {
388
375
  return this.toJSON();
389
376
  }
390
377
 
378
+ _createCoMap(
379
+ init: Simplify<CoMapInit<typeof this>>,
380
+ options?:
381
+ | {
382
+ owner: Account | Group;
383
+ unique?: CoValueUniqueness["uniqueness"];
384
+ }
385
+ | Account
386
+ | Group,
387
+ ): typeof this {
388
+ const { owner, uniqueness } = parseCoValueCreateOptions(options);
389
+ const raw = this.rawFromInit(init, owner, uniqueness);
390
+
391
+ Object.defineProperties(this, {
392
+ id: {
393
+ value: raw.id,
394
+ enumerable: false,
395
+ },
396
+ _raw: { value: raw, enumerable: false },
397
+ });
398
+
399
+ return this;
400
+ }
401
+
391
402
  /**
392
403
  * Create a new `RawCoMap` from an initialization object
393
404
  * @internal
@@ -490,6 +501,7 @@ export class CoMap extends CoValueBase implements CoValue {
490
501
  options?: {
491
502
  resolve?: RefsToResolveStrict<M, R>;
492
503
  loadAs?: Account | AnonymousJazzAgent;
504
+ skipRetry?: boolean;
493
505
  },
494
506
  ): Promise<Resolved<M, R> | null> {
495
507
  return loadCoValueWithoutMe(this, id, options);
@@ -542,11 +554,22 @@ export class CoMap extends CoValueBase implements CoValue {
542
554
  return subscribeToCoValueWithoutMe<M, R>(this, id, options, listener);
543
555
  }
544
556
 
557
+ /** @deprecated Use `CoMap.upsertUnique` and `CoMap.loadUnique` instead. */
545
558
  static findUnique<M extends CoMap>(
546
559
  this: CoValueClass<M>,
547
560
  unique: CoValueUniqueness["uniqueness"],
548
561
  ownerID: ID<Account> | ID<Group>,
549
562
  as?: Account | Group | AnonymousJazzAgent,
563
+ ) {
564
+ return CoMap._findUnique(unique, ownerID, as);
565
+ }
566
+
567
+ /** @internal */
568
+ static _findUnique<M extends CoMap>(
569
+ this: CoValueClass<M>,
570
+ unique: CoValueUniqueness["uniqueness"],
571
+ ownerID: ID<Account> | ID<Group>,
572
+ as?: Account | Group | AnonymousJazzAgent,
550
573
  ) {
551
574
  as ||= activeAccountContext.get();
552
575
 
@@ -564,6 +587,87 @@ export class CoMap extends CoValueBase implements CoValue {
564
587
  return cojsonInternals.idforHeader(header, crypto) as ID<M>;
565
588
  }
566
589
 
590
+ /**
591
+ * Given some data, updates an existing CoMap or initialises a new one if none exists.
592
+ *
593
+ * Note: This method respects resolve options, and thus can return `null` if the references cannot be resolved.
594
+ *
595
+ * @example
596
+ * ```ts
597
+ * const activeEvent = await Event.upsertUnique(
598
+ * sourceData.identifier,
599
+ * workspace.id,
600
+ * {
601
+ * title: sourceData.title,
602
+ * identifier: sourceData.identifier,
603
+ * external_id: sourceData._id,
604
+ * },
605
+ * workspace
606
+ * );
607
+ * ```
608
+ *
609
+ * @param options The options for creating or loading the CoMap. This includes the intended state of the CoMap, its unique identifier, its owner, and the references to resolve.
610
+ * @returns Either an existing & modified CoMap, or a new initialised CoMap if none exists.
611
+ * @category Subscription & Loading
612
+ */
613
+ static async upsertUnique<
614
+ M extends CoMap,
615
+ const R extends RefsToResolve<M> = true,
616
+ >(
617
+ this: CoValueClass<M>,
618
+ options: {
619
+ value: Simplify<CoMapInit<M>>;
620
+ unique: CoValueUniqueness["uniqueness"];
621
+ owner: Account | Group;
622
+ resolve?: RefsToResolveStrict<M, R>;
623
+ },
624
+ ): Promise<Resolved<M, R> | null> {
625
+ let mapId = CoMap._findUnique(options.unique, options.owner.id);
626
+ let map: Resolved<M, R> | null = await loadCoValueWithoutMe(this, mapId, {
627
+ ...options,
628
+ loadAs: options.owner._loadedAs,
629
+ skipRetry: true,
630
+ });
631
+ if (!map) {
632
+ const instance = new this();
633
+ map = instance._createCoMap(options.value, {
634
+ owner: options.owner,
635
+ unique: options.unique,
636
+ }) as Resolved<M, R>;
637
+ } else {
638
+ (map as M).applyDiff(options.value as Partial<CoMapInit<M>>);
639
+ }
640
+
641
+ return await loadCoValueWithoutMe(this, mapId, {
642
+ ...options,
643
+ loadAs: options.owner._loadedAs,
644
+ skipRetry: true,
645
+ });
646
+ }
647
+
648
+ /**
649
+ * Loads a CoMap by its unique identifier and owner's ID.
650
+ * @param unique The unique identifier of the CoMap to load.
651
+ * @param ownerID The ID of the owner of the CoMap.
652
+ * @param options Additional options for loading the CoMap.
653
+ * @returns The loaded CoMap, or null if unavailable.
654
+ */
655
+ static loadUnique<M extends CoMap, const R extends RefsToResolve<M> = true>(
656
+ this: CoValueClass<M>,
657
+ unique: CoValueUniqueness["uniqueness"],
658
+ ownerID: ID<Account> | ID<Group>,
659
+ options?: {
660
+ resolve?: RefsToResolveStrict<M, R>;
661
+ loadAs?: Account | AnonymousJazzAgent;
662
+ },
663
+ ): Promise<Resolved<M, R> | null> {
664
+ return loadCoValueWithoutMe(
665
+ this,
666
+ CoMap._findUnique(unique, ownerID, options?.loadAs),
667
+ { ...options, skipRetry: true },
668
+ );
669
+ }
670
+
567
671
  /**
568
672
  * Given an already loaded `CoMap`, ensure that the specified fields are loaded to the specified depth.
569
673
  *
@@ -90,6 +90,7 @@ export function loadCoValueWithoutMe<
90
90
  options?: {
91
91
  resolve?: RefsToResolveStrict<V, R>;
92
92
  loadAs?: Account | AnonymousJazzAgent;
93
+ skipRetry?: boolean;
93
94
  },
94
95
  ): Promise<Resolved<V, R> | null> {
95
96
  return loadCoValue(cls, id, {
@@ -107,6 +108,7 @@ export function loadCoValue<
107
108
  options: {
108
109
  resolve?: RefsToResolveStrict<V, R>;
109
110
  loadAs: Account | AnonymousJazzAgent;
111
+ skipRetry?: boolean;
110
112
  },
111
113
  ): Promise<Resolved<V, R> | null> {
112
114
  return new Promise((resolve) => {
@@ -117,6 +119,7 @@ export function loadCoValue<
117
119
  resolve: options.resolve,
118
120
  loadAs: options.loadAs,
119
121
  syncResolution: true,
122
+ skipRetry: options.skipRetry,
120
123
  onUnavailable: () => {
121
124
  resolve(null);
122
125
  },
@@ -242,6 +245,7 @@ export function subscribeToCoValue<
242
245
  onUnavailable?: () => void;
243
246
  onUnauthorized?: () => void;
244
247
  syncResolution?: boolean;
248
+ skipRetry?: boolean;
245
249
  },
246
250
  listener: SubscribeListener<V, R>,
247
251
  ): () => void {
@@ -252,10 +256,16 @@ export function subscribeToCoValue<
252
256
 
253
257
  let unsubscribed = false;
254
258
 
255
- const rootNode = new SubscriptionScope<V>(node, resolve, id as ID<V>, {
256
- ref: cls,
257
- optional: false,
258
- });
259
+ const rootNode = new SubscriptionScope<V>(
260
+ node,
261
+ resolve,
262
+ id as ID<V>,
263
+ {
264
+ ref: cls,
265
+ optional: false,
266
+ },
267
+ options.skipRetry,
268
+ );
259
269
 
260
270
  const handleUpdate = (value: SubscriptionValue<V, any>) => {
261
271
  if (unsubscribed) return;
@@ -57,6 +57,7 @@ export type CoMapSchema<
57
57
  R
58
58
  >;
59
59
  loadAs?: Account | AnonymousJazzAgent;
60
+ skipRetry?: boolean;
60
61
  },
61
62
  ): Promise<Resolved<
62
63
  Simplify<CoMapInstanceCoValuesNullable<Shape>> & CoMap,
@@ -82,12 +83,49 @@ export type CoMapSchema<
82
83
  ) => void,
83
84
  ): () => void;
84
85
 
86
+ /** @deprecated Use `CoMap.upsertUnique` and `CoMap.loadUnique` instead. */
85
87
  findUnique(
86
88
  unique: CoValueUniqueness["uniqueness"],
87
89
  ownerID: string,
88
90
  as?: Account | Group | AnonymousJazzAgent,
89
91
  ): string;
90
92
 
93
+ upsertUnique: <
94
+ const R extends RefsToResolve<
95
+ Simplify<CoMapInstanceCoValuesNullable<Shape>> & CoMap
96
+ > = true,
97
+ >(options: {
98
+ value: Simplify<CoMapInitZod<Shape>>;
99
+ unique: CoValueUniqueness["uniqueness"];
100
+ owner: Owner;
101
+ resolve?: RefsToResolveStrict<
102
+ Simplify<CoMapInstanceCoValuesNullable<Shape>> & CoMap,
103
+ R
104
+ >;
105
+ }) => Promise<Resolved<
106
+ Simplify<CoMapInstanceCoValuesNullable<Shape>> & CoMap,
107
+ R
108
+ > | null>;
109
+
110
+ loadUnique<
111
+ const R extends RefsToResolve<
112
+ Simplify<CoMapInstanceCoValuesNullable<Shape>> & CoMap
113
+ > = true,
114
+ >(
115
+ unique: CoValueUniqueness["uniqueness"],
116
+ ownerID: string,
117
+ options?: {
118
+ resolve?: RefsToResolveStrict<
119
+ Simplify<CoMapInstanceCoValuesNullable<Shape>> & CoMap,
120
+ R
121
+ >;
122
+ loadAs?: Account | AnonymousJazzAgent;
123
+ },
124
+ ): Promise<Resolved<
125
+ Simplify<CoMapInstanceCoValuesNullable<Shape>> & CoMap,
126
+ R
127
+ > | null>;
128
+
91
129
  catchall<T extends z.core.$ZodType>(
92
130
  schema: T,
93
131
  ): CoMapSchema<Shape, z.core.$catchall<T>>;
@@ -44,6 +44,12 @@ function enrichCoMapSchema<Shape extends z.core.$ZodLooseShape>(
44
44
  findUnique: (...args: any[]) => {
45
45
  return coSchema.findUnique(...args);
46
46
  },
47
+ upsertUnique: (...args: any[]) => {
48
+ return coSchema.upsertUnique(...args);
49
+ },
50
+ loadUnique: (...args: any[]) => {
51
+ return coSchema.loadUnique(...args);
52
+ },
47
53
  catchall: (index: z.core.$ZodType) => {
48
54
  return enrichCoMapSchema(baseCatchall(index));
49
55
  },
@@ -10,22 +10,25 @@ export class CoValueCoreSubscription {
10
10
  public node: LocalNode,
11
11
  public id: string,
12
12
  public listener: (value: RawCoValue | "unavailable") => void,
13
+ public skipRetry?: boolean,
13
14
  ) {
14
15
  const entry = this.node.getCoValue(this.id as any);
15
16
 
16
17
  if (entry?.isAvailable()) {
17
18
  this.subscribe(entry.getCurrentContent());
18
19
  } else {
19
- this.node.loadCoValueCore(this.id as any).then((value) => {
20
- if (this.unsubscribed) return;
20
+ this.node
21
+ .loadCoValueCore(this.id as any, undefined, skipRetry)
22
+ .then((value) => {
23
+ if (this.unsubscribed) return;
21
24
 
22
- if (value.isAvailable()) {
23
- this.subscribe(value.getCurrentContent());
24
- } else {
25
- this.listener("unavailable");
26
- this.subscribeToState();
27
- }
28
- });
25
+ if (value.isAvailable()) {
26
+ this.subscribe(value.getCurrentContent());
27
+ } else {
28
+ this.subscribeToState();
29
+ this.listener("unavailable");
30
+ }
31
+ });
29
32
  }
30
33
  }
31
34
 
@@ -44,34 +44,46 @@ export class SubscriptionScope<D extends CoValue> {
44
44
  resolve: RefsToResolve<D>,
45
45
  public id: ID<D>,
46
46
  public schema: RefEncoded<D>,
47
+ public skipRetry?: boolean,
47
48
  ) {
48
49
  this.resolve = resolve;
49
50
  this.value = { type: "unloaded", id };
50
51
 
51
52
  let lastUpdate: RawCoValue | "unavailable" | undefined;
52
- this.subscription = new CoValueCoreSubscription(node, id, (value) => {
53
- lastUpdate = value;
54
-
55
- // Need all these checks because the migration can trigger new syncronous updates
56
- //
57
- // We want to:
58
- // - Run the migration only once
59
- // - Skip all the updates until the migration is done
60
- // - Trigger handleUpdate only with the final value
61
- if (!this.migrated && value !== "unavailable") {
62
- if (this.migrating) {
53
+ this.subscription = new CoValueCoreSubscription(
54
+ node,
55
+ id,
56
+ (value) => {
57
+ lastUpdate = value;
58
+
59
+ if (skipRetry && value === "unavailable") {
60
+ this.handleUpdate(value);
61
+ this.destroy();
63
62
  return;
64
63
  }
65
64
 
66
- this.migrating = true;
67
- applyCoValueMigrations(instantiateRefEncoded(this.schema, value));
68
- this.migrated = true;
69
- this.handleUpdate(lastUpdate);
70
- return;
71
- }
65
+ // Need all these checks because the migration can trigger new syncronous updates
66
+ //
67
+ // We want to:
68
+ // - Run the migration only once
69
+ // - Skip all the updates until the migration is done
70
+ // - Trigger handleUpdate only with the final value
71
+ if (!this.migrated && value !== "unavailable") {
72
+ if (this.migrating) {
73
+ return;
74
+ }
75
+
76
+ this.migrating = true;
77
+ applyCoValueMigrations(instantiateRefEncoded(this.schema, value));
78
+ this.migrated = true;
79
+ this.handleUpdate(lastUpdate);
80
+ return;
81
+ }
72
82
 
73
- this.handleUpdate(value);
74
- });
83
+ this.handleUpdate(value);
84
+ },
85
+ skipRetry,
86
+ );
75
87
  }
76
88
 
77
89
  updateValue(value: SubscriptionValue<D, any>) {
@@ -688,6 +688,75 @@ describe("FileStream.loadAsBlob", async () => {
688
688
  });
689
689
  });
690
690
 
691
+ describe("FileStream.loadAsBase64", async () => {
692
+ async function setup() {
693
+ const me = await Account.create({
694
+ creationProps: { name: "Hermes Puggington" },
695
+ crypto: Crypto,
696
+ });
697
+
698
+ const stream = FileStream.create({ owner: me });
699
+
700
+ stream.start({ mimeType: "text/plain" });
701
+
702
+ return { stream, me };
703
+ }
704
+
705
+ test("resolves only when the stream is ended", async () => {
706
+ const { stream, me } = await setup();
707
+ stream.push(new Uint8Array([1]));
708
+
709
+ const promise = FileStream.loadAsBase64(stream.id, { loadAs: me });
710
+
711
+ stream.push(new Uint8Array([2]));
712
+ stream.end();
713
+
714
+ const base64 = await promise;
715
+
716
+ // The promise resolves only when the stream is ended
717
+ // so we get a blob with all the chunks
718
+ expect(base64).toBe("AQI=");
719
+ });
720
+
721
+ test("resolves with a data URL if dataURL: true", async () => {
722
+ const { stream, me } = await setup();
723
+ stream.push(new Uint8Array([1]));
724
+
725
+ const promise = FileStream.loadAsBase64(stream.id, {
726
+ loadAs: me,
727
+ dataURL: true,
728
+ });
729
+
730
+ stream.push(new Uint8Array([2]));
731
+ stream.end();
732
+
733
+ const base64 = await promise;
734
+
735
+ // The promise resolves only when the stream is ended
736
+ // so we get a blob with all the chunks
737
+ expect(base64).toBe("data:text/plain;base64,AQI=");
738
+ });
739
+
740
+ test("resolves with a partial base64 if allowUnfinished: true", async () => {
741
+ const { stream, me } = await setup();
742
+ stream.push(new Uint8Array([1]));
743
+
744
+ const promise = FileStream.loadAsBase64(stream.id, {
745
+ loadAs: me,
746
+ allowUnfinished: true,
747
+ });
748
+
749
+ const base64 = await promise;
750
+
751
+ stream.push(new Uint8Array([2]));
752
+ stream.end();
753
+
754
+ // The promise resolves before the stream is ended
755
+ // so we get a blob only with the first chunk
756
+ expect(base64).toBe("AQ==");
757
+ });
758
+ });
759
+
691
760
  describe("FileStream progress tracking", async () => {
692
761
  test("createFromBlob should report upload progress correctly", async () => {
693
762
  // Create 5MB test blob