jazz-tools 0.8.50 → 0.9.0

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.
@@ -32,6 +32,7 @@ import {
32
32
  inspect,
33
33
  isRefEncoded,
34
34
  loadCoValue,
35
+ parseCoValueCreateOptions,
35
36
  subscribeToCoValue,
36
37
  subscribeToExistingCoValue,
37
38
  } from "../internal.js";
@@ -204,10 +205,11 @@ export class CoFeed<Item = any> extends CoValueBase implements CoValue {
204
205
  static create<S extends CoFeed>(
205
206
  this: CoValueClass<S>,
206
207
  init: S extends CoFeed<infer Item> ? UnCo<Item>[] : never,
207
- options: { owner: Account | Group },
208
+ options: { owner: Account | Group } | Account | Group,
208
209
  ) {
209
- const instance = new this({ init, owner: options.owner });
210
- const raw = options.owner._raw.createStream();
210
+ const { owner } = parseCoValueCreateOptions(options);
211
+ const instance = new this({ init, owner });
212
+ const raw = owner._raw.createStream();
211
213
 
212
214
  Object.defineProperties(instance, {
213
215
  id: {
@@ -673,9 +675,9 @@ export class FileStream extends CoValueBase implements CoValue {
673
675
 
674
676
  static create<S extends FileStream>(
675
677
  this: CoValueClass<S>,
676
- options: { owner: Account | Group },
678
+ options: { owner: Account | Group } | Account | Group,
677
679
  ) {
678
- return new this(options);
680
+ return new this(parseCoValueCreateOptions(options));
679
681
  }
680
682
 
681
683
  getChunks(options?: {
@@ -735,7 +737,7 @@ export class FileStream extends CoValueBase implements CoValue {
735
737
  */
736
738
  if (!options?.allowUnfinished && !stream?.isBinaryStreamEnded()) {
737
739
  stream = await new Promise<FileStream>((resolve) => {
738
- const unsubscribe = subscribeToCoValue(this, id, as, [], (value) => {
740
+ subscribeToCoValue(this, id, as, [], (value, unsubscribe) => {
739
741
  if (value.isBinaryStreamEnded()) {
740
742
  unsubscribe();
741
743
  resolve(value);
@@ -762,12 +764,16 @@ export class FileStream extends CoValueBase implements CoValue {
762
764
  */
763
765
  static async createFromBlob(
764
766
  blob: Blob | File,
765
- options: {
766
- owner: Group | Account;
767
- onProgress?: (progress: number) => void;
768
- },
767
+ options:
768
+ | {
769
+ owner: Group | Account;
770
+ onProgress?: (progress: number) => void;
771
+ }
772
+ | Account
773
+ | Group,
769
774
  ): Promise<FileStream> {
770
- const stream = this.create({ owner: options.owner });
775
+ const stream = this.create(options);
776
+ const onProgress = "onProgress" in options ? options.onProgress : undefined;
771
777
 
772
778
  const start = Date.now();
773
779
 
@@ -785,7 +791,7 @@ export class FileStream extends CoValueBase implements CoValue {
785
791
  stream.push(data.slice(idx, idx + chunkSize));
786
792
 
787
793
  if (Date.now() - lastProgressUpdate > 100) {
788
- options.onProgress?.(idx / data.length);
794
+ onProgress?.(idx / data.length);
789
795
  lastProgressUpdate = Date.now();
790
796
  }
791
797
 
@@ -800,7 +806,7 @@ export class FileStream extends CoValueBase implements CoValue {
800
806
  "s - Throughput in MB/s",
801
807
  (1000 * (blob.size / (end - start))) / (1024 * 1024),
802
808
  );
803
- options.onProgress?.(1);
809
+ onProgress?.(1);
804
810
 
805
811
  return stream;
806
812
  }
@@ -23,6 +23,7 @@ import {
23
23
  isRefEncoded,
24
24
  loadCoValue,
25
25
  makeRefs,
26
+ parseCoValueCreateOptions,
26
27
  subscribeToCoValue,
27
28
  subscribeToExistingCoValue,
28
29
  subscriptionsScopes,
@@ -220,10 +221,11 @@ export class CoList<Item = any> extends Array<Item> implements CoValue {
220
221
  static create<L extends CoList>(
221
222
  this: CoValueClass<L>,
222
223
  items: UnCo<L[number]>[],
223
- options: { owner: Account | Group },
224
+ options: { owner: Account | Group } | Account | Group,
224
225
  ) {
225
- const instance = new this({ init: items, owner: options.owner });
226
- const raw = options.owner._raw.createList(
226
+ const { owner } = parseCoValueCreateOptions(options);
227
+ const instance = new this({ init: items, owner });
228
+ const raw = owner._raw.createList(
227
229
  toRawItems(items, instance._schema[ItemsSym]),
228
230
  );
229
231
 
@@ -239,9 +241,11 @@ export class CoList<Item = any> extends Array<Item> implements CoValue {
239
241
  }
240
242
 
241
243
  push(...items: Item[]): number {
242
- for (const item of toRawItems(items as Item[], this._schema[ItemsSym])) {
243
- this._raw.append(item);
244
- }
244
+ this._raw.appendItems(
245
+ toRawItems(items, this._schema[ItemsSym]),
246
+ undefined,
247
+ "private",
248
+ );
245
249
 
246
250
  return this._raw.entries().length;
247
251
  }
@@ -30,6 +30,7 @@ import {
30
30
  isRefEncoded,
31
31
  loadCoValue,
32
32
  makeRefs,
33
+ parseCoValueCreateOptions,
33
34
  subscribeToCoValue,
34
35
  subscribeToExistingCoValue,
35
36
  subscriptionsScopes,
@@ -43,6 +44,7 @@ type CoMapEdit<V> = {
43
44
  ref?: RefIfCoValue<V>;
44
45
  by?: Account;
45
46
  madeAt: Date;
47
+ key?: string;
46
48
  };
47
49
 
48
50
  type LastAndAllCoMapEdits<V> = CoMapEdit<V> & { all: CoMapEdit<V>[] };
@@ -183,6 +185,7 @@ export class CoMap extends CoValueBase implements CoValue {
183
185
  optional: false,
184
186
  }).accessFrom(target, "_edits." + key + ".by"),
185
187
  madeAt: rawEdit.at,
188
+ key,
186
189
  };
187
190
  }
188
191
 
@@ -271,17 +274,19 @@ export class CoMap extends CoValueBase implements CoValue {
271
274
  static create<M extends CoMap>(
272
275
  this: CoValueClass<M>,
273
276
  init: Simplify<CoMapInit<M>>,
274
- options: {
275
- owner: Account | Group;
276
- unique?: CoValueUniqueness["uniqueness"];
277
- },
277
+ options:
278
+ | {
279
+ owner: Account | Group;
280
+ unique?: CoValueUniqueness["uniqueness"];
281
+ }
282
+ | Account
283
+ | Group,
278
284
  ) {
279
285
  const instance = new this();
280
- const raw = instance.rawFromInit(
281
- init,
282
- options.owner,
283
- options.unique === undefined ? undefined : { uniqueness: options.unique },
284
- );
286
+
287
+ const { owner, uniqueness } = parseCoValueCreateOptions(options);
288
+ const raw = instance.rawFromInit(init, owner, uniqueness);
289
+
285
290
  Object.defineProperties(instance, {
286
291
  id: {
287
292
  value: raw.id,
@@ -14,6 +14,7 @@ import {
14
14
  Ref,
15
15
  ensureCoValueLoaded,
16
16
  loadCoValue,
17
+ parseCoValueCreateOptions,
17
18
  subscribeToCoValue,
18
19
  subscribeToExistingCoValue,
19
20
  } from "../internal.js";
@@ -123,9 +124,9 @@ export class Group extends CoValueBase implements CoValue {
123
124
 
124
125
  static create<G extends Group>(
125
126
  this: CoValueClass<G>,
126
- options: { owner: Account },
127
+ options: { owner: Account } | Account,
127
128
  ) {
128
- return new this(options);
129
+ return new this(parseCoValueCreateOptions(options));
129
130
  }
130
131
 
131
132
  myRole(): Role | undefined {
@@ -1,4 +1,8 @@
1
- import type { CojsonInternalTypes, RawCoValue } from "cojson";
1
+ import type {
2
+ CoValueUniqueness,
3
+ CojsonInternalTypes,
4
+ RawCoValue,
5
+ } from "cojson";
2
6
  import { RawAccount } from "cojson";
3
7
  import { AnonymousJazzAgent } from "../implementation/anonymousJazzAgent.js";
4
8
  import type { DeeplyLoaded, DepthsIn } from "../internal.js";
@@ -157,18 +161,17 @@ export function loadCoValue<V extends CoValue, Depth>(
157
161
  depth: Depth & DepthsIn<V>,
158
162
  ): Promise<DeeplyLoaded<V, Depth> | undefined> {
159
163
  return new Promise((resolve) => {
160
- const unsubscribe = subscribeToCoValue(
164
+ subscribeToCoValue(
161
165
  cls,
162
166
  id,
163
167
  as,
164
168
  depth,
165
- (value) => {
169
+ (value, unsubscribe) => {
166
170
  resolve(value);
167
171
  unsubscribe();
168
172
  },
169
173
  () => {
170
174
  resolve(undefined);
171
- unsubscribe();
172
175
  },
173
176
  );
174
177
  });
@@ -191,37 +194,49 @@ export function subscribeToCoValue<V extends CoValue, Depth>(
191
194
  id: ID<V>,
192
195
  as: Account | AnonymousJazzAgent,
193
196
  depth: Depth & DepthsIn<V>,
194
- listener: (value: DeeplyLoaded<V, Depth>) => void,
197
+ listener: (value: DeeplyLoaded<V, Depth>, unsubscribe: () => void) => void,
195
198
  onUnavailable?: () => void,
199
+ syncResolution?: boolean,
196
200
  ): () => void {
197
201
  const ref = new Ref(id, as, { ref: cls, optional: false });
198
202
 
199
203
  let unsubscribed = false;
200
204
  let unsubscribe: (() => void) | undefined;
201
205
 
202
- ref
203
- .load()
204
- .then((value) => {
205
- if (!value) {
206
- onUnavailable && onUnavailable();
207
- return;
208
- }
209
- if (unsubscribed) return;
210
- const subscription = new SubscriptionScope(
211
- value,
212
- cls as CoValueClass<V> & CoValueFromRaw<V>,
213
- (update) => {
214
- if (fulfillsDepth(depth, update)) {
215
- listener(update as DeeplyLoaded<V, Depth>);
216
- }
217
- },
218
- );
206
+ function subscribe(value: V | undefined) {
207
+ if (!value) {
208
+ onUnavailable && onUnavailable();
209
+ return;
210
+ }
211
+ if (unsubscribed) return;
212
+ const subscription = new SubscriptionScope(
213
+ value,
214
+ cls as CoValueClass<V> & CoValueFromRaw<V>,
215
+ (update, subscription) => {
216
+ if (fulfillsDepth(depth, update)) {
217
+ listener(
218
+ update as DeeplyLoaded<V, Depth>,
219
+ subscription.unsubscribeAll,
220
+ );
221
+ }
222
+ },
223
+ );
219
224
 
220
- unsubscribe = () => subscription.unsubscribeAll();
221
- })
222
- .catch((e) => {
223
- console.error("Failed to load / subscribe to CoValue", e);
224
- });
225
+ unsubscribe = subscription.unsubscribeAll;
226
+ }
227
+
228
+ const sync = syncResolution ? ref.syncLoad() : undefined;
229
+
230
+ if (sync) {
231
+ subscribe(sync);
232
+ } else {
233
+ ref
234
+ .load()
235
+ .then((value) => subscribe(value))
236
+ .catch((e) => {
237
+ console.error("Failed to load / subscribe to CoValue", e);
238
+ });
239
+ }
225
240
 
226
241
  return function unsubscribeAtAnyPoint() {
227
242
  unsubscribed = true;
@@ -229,7 +244,9 @@ export function subscribeToCoValue<V extends CoValue, Depth>(
229
244
  };
230
245
  }
231
246
 
232
- export function createCoValueObservable<V extends CoValue, Depth>() {
247
+ export function createCoValueObservable<V extends CoValue, Depth>(options?: {
248
+ syncResolution?: boolean;
249
+ }) {
233
250
  let currentValue: DeeplyLoaded<V, Depth> | undefined = undefined;
234
251
  let subscriberCount = 0;
235
252
 
@@ -253,6 +270,7 @@ export function createCoValueObservable<V extends CoValue, Depth>() {
253
270
  listener();
254
271
  },
255
272
  onUnavailable,
273
+ options?.syncResolution,
256
274
  );
257
275
 
258
276
  return () => {
@@ -285,3 +303,21 @@ export function subscribeToExistingCoValue<V extends CoValue, Depth>(
285
303
  listener,
286
304
  );
287
305
  }
306
+
307
+ export function parseCoValueCreateOptions(
308
+ options:
309
+ | {
310
+ owner: Account | Group;
311
+ unique?: CoValueUniqueness["uniqueness"];
312
+ }
313
+ | Account
314
+ | Group,
315
+ ) {
316
+ return "_type" in options &&
317
+ (options._type === "Account" || options._type === "Group")
318
+ ? { owner: options, uniqueness: undefined }
319
+ : {
320
+ owner: options.owner,
321
+ uniqueness: options.unique ? { uniqueness: options.unique } : undefined,
322
+ };
323
+ }
@@ -55,6 +55,23 @@ export class Ref<out V extends CoValue> {
55
55
  }
56
56
  }
57
57
 
58
+ syncLoad(): V | undefined {
59
+ const node =
60
+ "node" in this.controlledAccount
61
+ ? this.controlledAccount.node
62
+ : this.controlledAccount._raw.core.node;
63
+
64
+ const entry = node.coValuesStore.get(
65
+ this.id as unknown as CoID<RawCoValue>,
66
+ );
67
+
68
+ if (entry.state.type === "available") {
69
+ return new Ref(this.id, this.controlledAccount, this.schema).value!;
70
+ }
71
+
72
+ return undefined;
73
+ }
74
+
58
75
  async load(): Promise<V | undefined> {
59
76
  const result = await this.loadHelper();
60
77
  if (result === "unavailable") {
@@ -37,7 +37,7 @@ export class SubscriptionScope<Root extends CoValue> {
37
37
  constructor(
38
38
  root: Root,
39
39
  rootSchema: CoValueClass<Root> & CoValueFromRaw<Root>,
40
- onUpdate: (newRoot: Root) => void,
40
+ onUpdate: (newRoot: Root, scope: SubscriptionScope<Root>) => void,
41
41
  ) {
42
42
  this.rootEntry = {
43
43
  state: "loaded" as const,
@@ -52,7 +52,7 @@ export class SubscriptionScope<Root extends CoValue> {
52
52
  this.scheduleUpdate = () => {
53
53
  const value = rootSchema.fromRaw(this.rootEntry.value) as Root;
54
54
  subscriptionsScopes.set(value, this);
55
- onUpdate(value);
55
+ onUpdate(value, this);
56
56
  };
57
57
 
58
58
  this.rootEntry.rawUnsub = root._raw.core.subscribe(
@@ -125,7 +125,7 @@ export class SubscriptionScope<Root extends CoValue> {
125
125
  }
126
126
  }
127
127
 
128
- unsubscribeAll() {
128
+ unsubscribeAll = () => {
129
129
  for (const entry of this.entries.values()) {
130
130
  if (entry.state === "loaded") {
131
131
  entry.rawUnsub();
@@ -134,5 +134,5 @@ export class SubscriptionScope<Root extends CoValue> {
134
134
  }
135
135
  }
136
136
  this.entries.clear();
137
- }
137
+ };
138
138
  }
package/src/testing.ts ADDED
@@ -0,0 +1,103 @@
1
+ import { AgentSecret, CryptoProvider, Peer } from "cojson";
2
+ import { cojsonInternals } from "cojson";
3
+ import { PureJSCrypto } from "cojson/crypto";
4
+ import { Account, type AccountClass } from "./exports.js";
5
+ import {
6
+ type AnonymousJazzAgent,
7
+ type CoValueClass,
8
+ createAnonymousJazzContext,
9
+ } from "./internal.js";
10
+
11
+ type TestAccountSchema<Acc extends Account> = CoValueClass<Acc> & {
12
+ fromNode: (typeof Account)["fromNode"];
13
+ create: (options: {
14
+ creationProps: { name: string };
15
+ initialAgentSecret?: AgentSecret;
16
+ peersToLoadFrom?: Peer[];
17
+ crypto: CryptoProvider;
18
+ }) => Promise<Acc>;
19
+ };
20
+
21
+ class TestJSCrypto extends PureJSCrypto {
22
+ static async create() {
23
+ if ("navigator" in globalThis && navigator.userAgent.includes("jsdom")) {
24
+ // Mocking crypto seal & encrypt to make it work with JSDom. Getting "Error: Uint8Array expected" there
25
+ const crypto = new PureJSCrypto();
26
+
27
+ crypto.seal = (options) =>
28
+ `sealed_U${cojsonInternals.stableStringify(options.message)}` as any;
29
+ crypto.unseal = (sealed) =>
30
+ JSON.parse(sealed.substring("sealed_U".length));
31
+ crypto.encrypt = (message) =>
32
+ `encrypted_U${cojsonInternals.stableStringify(message)}` as any;
33
+ crypto.decryptRaw = (encrypted) =>
34
+ encrypted.substring("encrypted_U".length) as any;
35
+
36
+ return crypto;
37
+ }
38
+
39
+ // For non-jsdom environments, we use the real crypto
40
+ return new PureJSCrypto();
41
+ }
42
+ }
43
+
44
+ export async function createJazzTestAccount<Acc extends Account>(options?: {
45
+ AccountSchema?: CoValueClass<Acc>;
46
+ }): Promise<Acc> {
47
+ const AccountSchema = (options?.AccountSchema ??
48
+ Account) as unknown as TestAccountSchema<Acc>;
49
+ const account = await AccountSchema.create({
50
+ creationProps: {
51
+ name: "Test Account",
52
+ },
53
+ crypto: await TestJSCrypto.create(),
54
+ });
55
+
56
+ return account;
57
+ }
58
+
59
+ export async function createJazzTestGuest() {
60
+ const ctx = await createAnonymousJazzContext({
61
+ crypto: await PureJSCrypto.create(),
62
+ peersToLoadFrom: [],
63
+ });
64
+
65
+ return {
66
+ guest: ctx.agent,
67
+ };
68
+ }
69
+
70
+ export function getJazzContextShape<Acc extends Account>(
71
+ account: Acc | { guest: AnonymousJazzAgent },
72
+ ) {
73
+ if ("guest" in account) {
74
+ return {
75
+ guest: account.guest,
76
+ AccountSchema: Account,
77
+ logOut: () => account.guest.node.gracefulShutdown(),
78
+ done: () => account.guest.node.gracefulShutdown(),
79
+ };
80
+ }
81
+
82
+ return {
83
+ me: account,
84
+ AccountSchema: account.constructor as AccountClass<Acc>,
85
+ logOut: () => account._raw.core.node.gracefulShutdown(),
86
+ done: () => account._raw.core.node.gracefulShutdown(),
87
+ };
88
+ }
89
+
90
+ export function linkAccounts(
91
+ a: Account,
92
+ b: Account,
93
+ aRole: "server" | "client" = "server",
94
+ bRole: "server" | "client" = "server",
95
+ ) {
96
+ const [aPeer, bPeer] = cojsonInternals.connectedPeers(b.id, a.id, {
97
+ peer1role: aRole,
98
+ peer2role: bRole,
99
+ });
100
+
101
+ a._raw.core.node.syncManager.addPeer(aPeer);
102
+ b._raw.core.node.syncManager.addPeer(bPeer);
103
+ }
@@ -4,6 +4,7 @@ import {
4
4
  Account,
5
5
  CoFeed,
6
6
  FileStream,
7
+ Group,
7
8
  ID,
8
9
  WasmCrypto,
9
10
  co,
@@ -34,6 +35,21 @@ describe("Simple CoFeed operations", async () => {
34
35
  expect(stream.perSession[me.sessionID]?.value).toEqual("milk");
35
36
  });
36
37
 
38
+ test("Construction with an Account", () => {
39
+ const stream = TestStream.create(["milk"], me);
40
+
41
+ expect(stream[me.id]?.value).toEqual("milk");
42
+ expect(stream.perSession[me.sessionID]?.value).toEqual("milk");
43
+ });
44
+
45
+ test("Construction with a Group", () => {
46
+ const group = Group.create(me);
47
+ const stream = TestStream.create(["milk"], group);
48
+
49
+ expect(stream[me.id]?.value).toEqual("milk");
50
+ expect(stream.perSession[me.sessionID]?.value).toEqual("milk");
51
+ });
52
+
37
53
  describe("Mutation", () => {
38
54
  test("pushing", () => {
39
55
  stream.push("bread");
@@ -3,6 +3,7 @@ import { describe, expect, test } from "vitest";
3
3
  import {
4
4
  Account,
5
5
  CoList,
6
+ Group,
6
7
  WasmCrypto,
7
8
  co,
8
9
  cojsonInternals,
@@ -37,6 +38,19 @@ describe("Simple CoList operations", async () => {
37
38
  ]);
38
39
  });
39
40
 
41
+ test("Construction with an Account", () => {
42
+ const list = TestList.create(["milk"], me);
43
+
44
+ expect(list[0]).toEqual("milk");
45
+ });
46
+
47
+ test("Construction with a Group", () => {
48
+ const group = Group.create(me);
49
+ const list = TestList.create(["milk"], group);
50
+
51
+ expect(list[0]).toEqual("milk");
52
+ });
53
+
40
54
  describe("Mutation", () => {
41
55
  test("assignment", () => {
42
56
  const list = TestList.create(["bread", "butter", "onion"], {
@@ -64,6 +64,25 @@ describe("Simple CoMap operations", async () => {
64
64
  ]);
65
65
  });
66
66
 
67
+ test("Construction with an Account", () => {
68
+ const map = TestMap.create(
69
+ { color: "red", _height: 10, birthday: birthday },
70
+ me,
71
+ );
72
+
73
+ expect(map.color).toEqual("red");
74
+ });
75
+
76
+ test("Construction with a Group", () => {
77
+ const group = Group.create(me);
78
+ const map = TestMap.create(
79
+ { color: "red", _height: 10, birthday: birthday },
80
+ group,
81
+ );
82
+
83
+ expect(map.color).toEqual("red");
84
+ });
85
+
67
86
  test("Construction with too many things provided", () => {
68
87
  const mapWithExtra = TestMap.create(
69
88
  {
@@ -99,6 +99,7 @@ describe("SchemaUnion", () => {
99
99
  { type: "button", label: "Submit" },
100
100
  { owner: me },
101
101
  );
102
+ let currentValue = "Submit";
102
103
  const unsubscribe = subscribeToCoValue(
103
104
  WidgetUnion,
104
105
  buttonWidget.id,
@@ -106,13 +107,16 @@ describe("SchemaUnion", () => {
106
107
  {},
107
108
  (value: BaseWidget) => {
108
109
  if (value instanceof ButtonWidget) {
109
- expect(value.label).toBe("Changed");
110
- unsubscribe();
110
+ expect(value.label).toBe(currentValue);
111
111
  } else {
112
112
  throw new Error("Unexpected widget type");
113
113
  }
114
114
  },
115
+ () => {},
116
+ true,
115
117
  );
118
+ currentValue = "Changed";
116
119
  buttonWidget.label = "Changed";
120
+ unsubscribe();
117
121
  });
118
122
  });
@@ -1,9 +1,15 @@
1
1
  import { describe, expect, it, onTestFinished, vi } from "vitest";
2
- import { Account, CoFeed, CoList, CoMap, co } from "../index.web.js";
3
2
  import {
4
- type DepthsIn,
3
+ Account,
4
+ CoFeed,
5
+ CoList,
6
+ CoMap,
5
7
  FileStream,
6
8
  Group,
9
+ co,
10
+ } from "../index.web.js";
11
+ import {
12
+ type DepthsIn,
7
13
  createCoValueObservable,
8
14
  subscribeToCoValue,
9
15
  } from "../internal.js";
@@ -64,6 +70,7 @@ describe("subscribeToCoValue", () => {
64
70
  messages: null,
65
71
  name: "General",
66
72
  }),
73
+ expect.any(Function),
67
74
  );
68
75
 
69
76
  updateFn.mockClear();
@@ -78,6 +85,7 @@ describe("subscribeToCoValue", () => {
78
85
  name: "General",
79
86
  messages: expect.any(Array),
80
87
  }),
88
+ expect.any(Function),
81
89
  );
82
90
 
83
91
  updateFn.mockClear();
@@ -93,6 +101,7 @@ describe("subscribeToCoValue", () => {
93
101
  name: "Lounge",
94
102
  messages: expect.any(Array),
95
103
  }),
104
+ expect.any(Function),
96
105
  );
97
106
  });
98
107
 
@@ -125,6 +134,7 @@ describe("subscribeToCoValue", () => {
125
134
  name: "General",
126
135
  messages: expect.any(Array),
127
136
  }),
137
+ expect.any(Function),
128
138
  );
129
139
  });
130
140