jazz-tools 0.18.5 → 0.18.7

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 (111) hide show
  1. package/.turbo/turbo-build.log +57 -57
  2. package/CHANGELOG.md +33 -0
  3. package/dist/better-auth/auth/client.d.ts.map +1 -1
  4. package/dist/better-auth/auth/client.js +7 -1
  5. package/dist/better-auth/auth/client.js.map +1 -1
  6. package/dist/better-auth/auth/react.d.ts +0 -2145
  7. package/dist/better-auth/auth/react.d.ts.map +1 -1
  8. package/dist/better-auth/auth/react.js +2 -14
  9. package/dist/better-auth/auth/react.js.map +1 -1
  10. package/dist/better-auth/auth/server.d.ts.map +1 -1
  11. package/dist/better-auth/auth/server.js +77 -22
  12. package/dist/better-auth/auth/server.js.map +1 -1
  13. package/dist/better-auth/auth/tests/react.test.d.ts +2 -0
  14. package/dist/better-auth/auth/tests/react.test.d.ts.map +1 -0
  15. package/dist/{chunk-3LE7N6TH.js → chunk-CFAY3FMQ.js} +192 -101
  16. package/dist/chunk-CFAY3FMQ.js.map +1 -0
  17. package/dist/index.js +1 -1
  18. package/dist/inspector/{custom-element-WCY6D3QJ.js → custom-element-G6SPZEBR.js} +308 -97
  19. package/dist/inspector/custom-element-G6SPZEBR.js.map +1 -0
  20. package/dist/inspector/index.d.ts +5 -1
  21. package/dist/inspector/index.d.ts.map +1 -1
  22. package/dist/inspector/index.js +318 -56
  23. package/dist/inspector/index.js.map +1 -1
  24. package/dist/inspector/register-custom-element.js +1 -1
  25. package/dist/inspector/ui/button.d.ts +1 -1
  26. package/dist/inspector/ui/button.d.ts.map +1 -1
  27. package/dist/inspector/ui/heading.d.ts +2 -1
  28. package/dist/inspector/ui/heading.d.ts.map +1 -1
  29. package/dist/inspector/ui/input.d.ts.map +1 -1
  30. package/dist/inspector/ui/modal.d.ts +16 -0
  31. package/dist/inspector/ui/modal.d.ts.map +1 -0
  32. package/dist/inspector/viewer/delete-local-data.d.ts +2 -0
  33. package/dist/inspector/viewer/delete-local-data.d.ts.map +1 -0
  34. package/dist/inspector/viewer/{inpsector-button.d.ts → inspector-button.d.ts} +1 -1
  35. package/dist/inspector/viewer/{inpsector-button.d.ts.map → inspector-button.d.ts.map} +1 -1
  36. package/dist/inspector/viewer/new-app.d.ts +1 -4
  37. package/dist/inspector/viewer/new-app.d.ts.map +1 -1
  38. package/dist/react/hooks.d.ts +1 -1
  39. package/dist/react/hooks.d.ts.map +1 -1
  40. package/dist/react/index.d.ts +1 -1
  41. package/dist/react/index.d.ts.map +1 -1
  42. package/dist/react/index.js +3 -1
  43. package/dist/react/index.js.map +1 -1
  44. package/dist/react-core/hooks.d.ts +133 -0
  45. package/dist/react-core/hooks.d.ts.map +1 -1
  46. package/dist/react-core/index.js +83 -17
  47. package/dist/react-core/index.js.map +1 -1
  48. package/dist/react-core/tests/useCoStateWithSelector.test.d.ts +2 -0
  49. package/dist/react-core/tests/useCoStateWithSelector.test.d.ts.map +1 -0
  50. package/dist/react-native-core/hooks.d.ts +1 -1
  51. package/dist/react-native-core/hooks.d.ts.map +1 -1
  52. package/dist/react-native-core/index.js +3 -1
  53. package/dist/react-native-core/index.js.map +1 -1
  54. package/dist/testing.js +2 -2
  55. package/dist/testing.js.map +1 -1
  56. package/dist/tools/coValues/CoValueBase.d.ts +14 -0
  57. package/dist/tools/coValues/CoValueBase.d.ts.map +1 -1
  58. package/dist/tools/coValues/coMap.d.ts +0 -12
  59. package/dist/tools/coValues/coMap.d.ts.map +1 -1
  60. package/dist/tools/coValues/inbox.d.ts +5 -5
  61. package/dist/tools/coValues/inbox.d.ts.map +1 -1
  62. package/dist/tools/implementation/createContext.d.ts +2 -1
  63. package/dist/tools/implementation/createContext.d.ts.map +1 -1
  64. package/dist/tools/tests/utils.d.ts.map +1 -1
  65. package/dist/worker/index.d.ts +12 -2
  66. package/dist/worker/index.d.ts.map +1 -1
  67. package/dist/worker/index.js +10 -4
  68. package/dist/worker/index.js.map +1 -1
  69. package/package.json +6 -4
  70. package/src/better-auth/auth/client.ts +8 -2
  71. package/src/better-auth/auth/react.tsx +2 -51
  72. package/src/better-auth/auth/server.ts +98 -24
  73. package/src/better-auth/auth/tests/client.test.ts +92 -4
  74. package/src/better-auth/auth/tests/react.test.tsx +43 -0
  75. package/src/better-auth/auth/tests/server.test.ts +276 -98
  76. package/src/inspector/custom-element.tsx +1 -1
  77. package/src/inspector/index.tsx +44 -0
  78. package/src/inspector/ui/button.tsx +15 -1
  79. package/src/inspector/ui/heading.tsx +7 -2
  80. package/src/inspector/ui/input.tsx +6 -2
  81. package/src/inspector/ui/modal.tsx +158 -0
  82. package/src/inspector/viewer/delete-local-data.tsx +101 -0
  83. package/src/inspector/viewer/new-app.tsx +3 -19
  84. package/src/react/hooks.tsx +1 -0
  85. package/src/react/index.ts +1 -0
  86. package/src/react-core/hooks.ts +162 -0
  87. package/src/react-core/tests/useCoStateWithSelector.test.ts +149 -0
  88. package/src/react-native-core/hooks.tsx +1 -0
  89. package/src/tools/coValues/CoValueBase.ts +32 -0
  90. package/src/tools/coValues/coList.ts +35 -0
  91. package/src/tools/coValues/coMap.ts +0 -18
  92. package/src/tools/coValues/inbox.ts +190 -108
  93. package/src/tools/implementation/createContext.ts +9 -2
  94. package/src/tools/testing.ts +1 -1
  95. package/src/tools/tests/coFeed.test.ts +33 -22
  96. package/src/tools/tests/coList.test.ts +47 -4
  97. package/src/tools/tests/coMap.test.ts +13 -5
  98. package/src/tools/tests/coPlainText.test.ts +24 -0
  99. package/src/tools/tests/createContext.test.ts +24 -0
  100. package/src/tools/tests/deepLoading.test.ts +2 -0
  101. package/src/tools/tests/exportImport.test.ts +3 -1
  102. package/src/tools/tests/groupsAndAccounts.test.ts +56 -44
  103. package/src/tools/tests/inbox.test.ts +293 -31
  104. package/src/tools/tests/patterns/requestToJoin.test.ts +14 -6
  105. package/src/tools/tests/utils.ts +1 -0
  106. package/src/worker/index.ts +21 -5
  107. package/tsup.config.ts +1 -1
  108. package/dist/chunk-3LE7N6TH.js.map +0 -1
  109. package/dist/inspector/custom-element-WCY6D3QJ.js.map +0 -1
  110. package/src/inspector/index.ts +0 -23
  111. /package/src/inspector/viewer/{inpsector-button.tsx → inspector-button.tsx} +0 -0
@@ -1,5 +1,12 @@
1
- import { CoID, InviteSecret, RawAccount, RawCoMap, SessionID } from "cojson";
2
- import { CoStreamItem, RawCoStream } from "cojson";
1
+ import {
2
+ CoID,
3
+ CoValueCore,
4
+ InviteSecret,
5
+ RawAccount,
6
+ RawCoMap,
7
+ SessionID,
8
+ } from "cojson";
9
+ import { type AvailableCoValueCore, type RawCoStream } from "cojson";
3
10
  import {
4
11
  type Account,
5
12
  CoValue,
@@ -16,10 +23,11 @@ export type InboxInvite = `${CoID<MessagesStream>}/${InviteSecret}`;
16
23
  type TxKey = `${SessionID}/${number}`;
17
24
 
18
25
  type MessagesStream = RawCoStream<CoID<InboxMessage<CoValue, any>>>;
19
- type FailedMessagesStream = RawCoStream<{
26
+ type FailedMessagesStreamItem = {
20
27
  errors: string[];
21
28
  value: CoID<InboxMessage<CoValue, any>>;
22
- }>;
29
+ };
30
+ type FailedMessagesStream = RawCoStream<FailedMessagesStreamItem>;
23
31
  type TxKeyStream = RawCoStream<TxKey>;
24
32
  export type InboxRoot = RawCoMap<{
25
33
  messages: CoID<MessagesStream>;
@@ -55,6 +63,25 @@ export function createInboxRoot(account: Account) {
55
63
  };
56
64
  }
57
65
 
66
+ /**
67
+ * An abstraction on top of CoStream to get the new items in a performant way.
68
+ */
69
+ class IncrementalFeed {
70
+ constructor(private feed: AvailableCoValueCore) {}
71
+
72
+ private sessions = {};
73
+ getNewItems() {
74
+ const items = this.feed.getValidTransactions({
75
+ ignorePrivateTransactions: false,
76
+ from: this.sessions,
77
+ });
78
+
79
+ this.sessions = this.feed.knownState().sessions;
80
+
81
+ return items;
82
+ }
83
+ }
84
+
58
85
  type InboxMessage<I extends CoValue, O extends CoValue | undefined> = RawCoMap<{
59
86
  payload: ID<I>;
60
87
  result: ID<O> | undefined;
@@ -87,13 +114,68 @@ async function createInboxMessage<
87
114
  return message;
88
115
  }
89
116
 
117
+ class MessageQueue {
118
+ private queue: Array<{
119
+ txKey: TxKey;
120
+ messageId: CoID<InboxMessage<CoValue, any>>;
121
+ }> = [];
122
+ private processing = new Set<TxKey>();
123
+ private concurrencyLimit: number;
124
+ private activeCount = 0;
125
+
126
+ constructor(
127
+ concurrencyLimit: number = 10,
128
+ private processMessage: (
129
+ txKey: TxKey,
130
+ messageId: CoID<InboxMessage<CoValue, any>>,
131
+ ) => Promise<void>,
132
+ private handleError: (
133
+ txKey: TxKey,
134
+ messageId: CoID<InboxMessage<CoValue, any>>,
135
+ error: Error,
136
+ ) => void,
137
+ ) {
138
+ this.concurrencyLimit = concurrencyLimit;
139
+ }
140
+
141
+ enqueue(txKey: TxKey, messageId: CoID<InboxMessage<CoValue, any>>) {
142
+ this.queue.push({ txKey, messageId });
143
+ this.processNext();
144
+ }
145
+
146
+ private async processNext() {
147
+ if (this.activeCount >= this.concurrencyLimit || this.queue.length === 0) {
148
+ return;
149
+ }
150
+
151
+ const { txKey, messageId } = this.queue.shift()!;
152
+
153
+ if (this.processing.has(txKey)) {
154
+ this.processNext();
155
+ return;
156
+ }
157
+
158
+ this.processing.add(txKey);
159
+ this.activeCount++;
160
+
161
+ try {
162
+ await this.processMessage(txKey, messageId);
163
+ } catch (error) {
164
+ this.handleError(txKey, messageId, error as Error);
165
+ } finally {
166
+ this.processing.delete(txKey);
167
+ this.activeCount--;
168
+ this.processNext();
169
+ }
170
+ }
171
+ }
172
+
90
173
  export class Inbox {
91
174
  account: Account;
92
175
  messages: MessagesStream;
93
176
  processed: TxKeyStream;
94
177
  failed: FailedMessagesStream;
95
178
  root: InboxRoot;
96
- processing = new Set<`${SessionID}/${number}`>();
97
179
 
98
180
  private constructor(
99
181
  account: Account,
@@ -115,127 +197,126 @@ export class Inbox {
115
197
  message: InstanceOfSchema<M>,
116
198
  senderAccountID: ID<Account>,
117
199
  ) => Promise<O | undefined | void>,
118
- options: { retries?: number } = {},
200
+ options?: { concurrencyLimit?: number },
119
201
  ) {
120
202
  const processed = new Set<`${SessionID}/${number}`>();
121
- const failed = new Map<`${SessionID}/${number}`, string[]>();
122
203
  const node = this.account.$jazz.localNode;
123
204
 
124
- this.processed.subscribe((stream) => {
125
- for (const items of Object.values(stream.items)) {
126
- for (const item of items) {
127
- processed.add(item.value as TxKey);
128
- }
205
+ // Create queue instance inside subscribe function
206
+ const concurrencyLimit = options?.concurrencyLimit ?? 10;
207
+
208
+ const processedFeed = new IncrementalFeed(this.processed.core);
209
+
210
+ // Track the already processed messages, triggered immediately so we know the messages processed in the previous sessions
211
+ this.processed.subscribe(() => {
212
+ for (const { changes } of processedFeed.getNewItems()) {
213
+ processed.add(changes[0] as TxKey);
129
214
  }
130
215
  });
131
216
 
132
217
  const { account } = this;
133
- const { retries = 3 } = options;
134
218
 
135
- let failTimer: ReturnType<typeof setTimeout> | number | undefined =
136
- undefined;
219
+ const messagesFeed = new IncrementalFeed(this.messages.core);
220
+
221
+ // Set up the message processing handler for the queue
222
+ const processMessage = async (
223
+ txKey: TxKey,
224
+ messageId: CoID<InboxMessage<CoValue, any>>,
225
+ ) => {
226
+ const message = await node.load(messageId);
227
+ if (message === "unavailable") {
228
+ throw new Error(`Inbox: message ${messageId} is unavailable`);
229
+ }
230
+
231
+ const value = await loadCoValue(
232
+ coValueClassFromCoValueClassOrSchema(Schema),
233
+ message.get("payload")!,
234
+ {
235
+ loadAs: account,
236
+ },
237
+ );
238
+
239
+ if (!value) {
240
+ throw new Error(
241
+ `Inbox: Unable to load the payload of message ${messageId}`,
242
+ );
243
+ }
244
+
245
+ const accountID = getAccountIDfromSessionID(
246
+ txKey.split("/")[0] as SessionID,
247
+ );
248
+ if (!accountID) {
249
+ throw new Error(`Inbox: Unknown account for message ${messageId}`);
250
+ }
251
+
252
+ const result = await callback(value as InstanceOfSchema<M>, accountID);
253
+
254
+ const inboxMessage = node
255
+ .expectCoValueLoaded(messageId)
256
+ .getCurrentContent() as RawCoMap;
257
+
258
+ if (result) {
259
+ inboxMessage.set("result", result.$jazz.id);
260
+ }
137
261
 
138
- const clearFailTimer = () => {
139
- clearTimeout(failTimer);
140
- failTimer = undefined;
262
+ inboxMessage.set("processed", true);
263
+ this.processed.push(txKey);
141
264
  };
142
265
 
143
- const handleNewMessages = (stream: MessagesStream) => {
144
- clearFailTimer(); // Stop the failure timers, we're going to process the failed entries anyway
266
+ const handleError = (
267
+ txKey: TxKey,
268
+ messageId: CoID<InboxMessage<CoValue, any>>,
269
+ error: Error,
270
+ ) => {
271
+ console.error(error);
145
272
 
146
- for (const [sessionID, items] of Object.entries(stream.items) as [
147
- SessionID,
148
- CoStreamItem<CoID<InboxMessage<NonNullable<InstanceOfSchema<M>>, O>>>[],
149
- ][]) {
150
- const accountID = getAccountIDfromSessionID(sessionID);
273
+ const stringifiedError = String(error);
274
+
275
+ this.processed.push(txKey);
276
+ this.failed.push({ errors: [stringifiedError], value: messageId });
277
+
278
+ try {
279
+ const inboxMessage = node
280
+ .expectCoValueLoaded(messageId)
281
+ .getCurrentContent() as RawCoMap;
282
+
283
+ inboxMessage.set("error", stringifiedError);
284
+ inboxMessage.set("processed", true);
285
+ } catch (error) {}
286
+ };
287
+
288
+ const messageQueue = new MessageQueue(
289
+ concurrencyLimit,
290
+ processMessage,
291
+ handleError,
292
+ );
293
+
294
+ const handleNewMessages = () => {
295
+ for (const tx of messagesFeed.getNewItems()) {
296
+ const accountID = getAccountIDfromSessionID(tx.txID.sessionID);
151
297
 
152
298
  if (!accountID) {
153
- console.warn("Received message from unknown account", sessionID);
299
+ console.warn(
300
+ "Received message from unknown account",
301
+ tx.txID.sessionID,
302
+ );
154
303
  continue;
155
304
  }
156
305
 
157
- for (const item of items) {
158
- const txKey = `${sessionID}/${item.tx.txIndex}` as const;
159
-
160
- if (!processed.has(txKey) && !this.processing.has(txKey)) {
161
- this.processing.add(txKey);
162
-
163
- const id = item.value;
164
-
165
- node
166
- .load(id)
167
- .then((message) => {
168
- if (message === "unavailable") {
169
- return Promise.reject(
170
- new Error("Unable to load inbox message " + id),
171
- );
172
- }
173
-
174
- return loadCoValue(
175
- coValueClassFromCoValueClassOrSchema(Schema),
176
- message.get("payload")!,
177
- {
178
- loadAs: account,
179
- },
180
- );
181
- })
182
- .then((value) => {
183
- if (!value) {
184
- return Promise.reject(
185
- new Error("Unable to load inbox message " + id),
186
- );
187
- }
188
-
189
- return callback(value as InstanceOfSchema<M>, accountID);
190
- })
191
- .then((result) => {
192
- const inboxMessage = node
193
- .expectCoValueLoaded(item.value)
194
- .getCurrentContent() as RawCoMap;
195
-
196
- if (result) {
197
- inboxMessage.set("result", result.$jazz.id);
198
- }
199
-
200
- inboxMessage.set("processed", true);
201
-
202
- this.processed.push(txKey);
203
- this.processing.delete(txKey);
204
- })
205
- .catch((error) => {
206
- console.error("Error processing inbox message", error);
207
- this.processing.delete(txKey);
208
- const errors = failed.get(txKey) ?? [];
209
-
210
- const stringifiedError = String(error);
211
- errors.push(stringifiedError);
212
-
213
- let inboxMessage: RawCoMap | undefined;
214
-
215
- try {
216
- inboxMessage = node
217
- .expectCoValueLoaded(item.value)
218
- .getCurrentContent() as RawCoMap;
219
-
220
- inboxMessage.set("error", stringifiedError);
221
- } catch (error) {}
222
-
223
- if (errors.length > retries) {
224
- inboxMessage?.set("processed", true);
225
- this.processed.push(txKey);
226
- this.failed.push({ errors, value: item.value });
227
- } else {
228
- failed.set(txKey, errors);
229
- if (!failTimer) {
230
- failTimer = setTimeout(
231
- () => handleNewMessages(stream),
232
- 100,
233
- );
234
- }
235
- }
236
- });
237
- }
306
+ const id = tx.changes[0] as CoID<InboxMessage<CoValue, any>>;
307
+
308
+ if (!isCoValueId(id)) {
309
+ continue;
238
310
  }
311
+
312
+ const txKey = `${tx.txID.sessionID}/${tx.txID.txIndex}` as const;
313
+
314
+ if (processed.has(txKey)) {
315
+ continue;
316
+ }
317
+
318
+ // Enqueue the message for processing
319
+ messageQueue.enqueue(txKey, id);
239
320
  }
240
321
  };
241
322
 
@@ -243,7 +324,6 @@ export class Inbox {
243
324
 
244
325
  return () => {
245
326
  unsubscribe();
246
- clearFailTimer();
247
327
  };
248
328
  }
249
329
 
@@ -280,6 +360,8 @@ export class Inbox {
280
360
  throw new Error("Inbox not found");
281
361
  }
282
362
 
363
+ await processed.core.waitForFullStreaming();
364
+
283
365
  return new Inbox(account, root, messages, processed, failed);
284
366
  }
285
367
  }
@@ -96,6 +96,7 @@ export async function createJazzContextFromExistingCredentials<
96
96
  AccountSchema: PropsAccountSchema,
97
97
  sessionProvider,
98
98
  onLogOut,
99
+ asActiveAccount,
99
100
  }: {
100
101
  credentials: Credentials;
101
102
  peersToLoadFrom: Peer[];
@@ -104,6 +105,7 @@ export async function createJazzContextFromExistingCredentials<
104
105
  sessionProvider: SessionProvider;
105
106
  onLogOut?: () => void;
106
107
  storage?: StorageAPI;
108
+ asActiveAccount: boolean;
107
109
  }): Promise<JazzContextWithAccount<InstanceOfSchema<S>>> {
108
110
  const { sessionID, sessionDone } = await sessionProvider(
109
111
  credentials.accountID,
@@ -125,14 +127,18 @@ export async function createJazzContextFromExistingCredentials<
125
127
  storage,
126
128
  migration: async (rawAccount, _node, creationProps) => {
127
129
  const account = AccountClass.fromRaw(rawAccount) as InstanceOfSchema<S>;
128
- activeAccountContext.set(account);
130
+ if (asActiveAccount) {
131
+ activeAccountContext.set(account);
132
+ }
129
133
 
130
134
  await account.applyMigration(creationProps);
131
135
  },
132
136
  });
133
137
 
134
138
  const account = AccountClass.fromNode(node);
135
- activeAccountContext.set(account);
139
+ if (asActiveAccount) {
140
+ activeAccountContext.set(account);
141
+ }
136
142
 
137
143
  return {
138
144
  node,
@@ -245,6 +251,7 @@ export async function createJazzContext<
245
251
  authSecretStorage.clearWithoutNotify();
246
252
  },
247
253
  storage: options.storage,
254
+ asActiveAccount: true,
248
255
  });
249
256
  } else {
250
257
  const secretSeed = options.crypto.newRandomSecretSeed();
@@ -56,7 +56,7 @@ export function getPeerConnectedToTestSyncServer() {
56
56
  Math.random().toString(),
57
57
  Math.random().toString(),
58
58
  {
59
- peer1role: "server",
59
+ peer1role: "client",
60
60
  peer2role: "server",
61
61
  },
62
62
  );
@@ -17,8 +17,13 @@ import {
17
17
  z,
18
18
  } from "../index.js";
19
19
  import { createJazzTestAccount, setupJazzTestSync } from "../testing.js";
20
- import { setupTwoNodes } from "./utils.js";
21
- import { CoFeed, ControlledAccount, TypeSym } from "../internal.js";
20
+ import { setupTwoNodes, waitFor } from "./utils.js";
21
+ import {
22
+ CoFeed,
23
+ CoFeedInstanceCoValuesNullable,
24
+ ControlledAccount,
25
+ TypeSym,
26
+ } from "../internal.js";
22
27
 
23
28
  const Crypto = await WasmCrypto.create();
24
29
 
@@ -203,7 +208,7 @@ describe("CoFeed resolution", async () => {
203
208
 
204
209
  assert(myStream);
205
210
 
206
- expect(myStream.value).toBeTruthy();
211
+ await waitFor(() => expect(myStream.value).toBeTruthy());
207
212
 
208
213
  assert(myStream.value);
209
214
 
@@ -211,7 +216,7 @@ describe("CoFeed resolution", async () => {
211
216
 
212
217
  assert(loadedNestedStreamByMe);
213
218
 
214
- expect(loadedNestedStreamByMe.value).toBeTruthy();
219
+ await waitFor(() => expect(loadedNestedStreamByMe.value).toBeTruthy());
215
220
 
216
221
  assert(loadedNestedStreamByMe.value);
217
222
 
@@ -220,7 +225,7 @@ describe("CoFeed resolution", async () => {
220
225
 
221
226
  assert(loadedTwiceNestedStreamByMe);
222
227
 
223
- expect(loadedTwiceNestedStreamByMe.value).toBe("milk");
228
+ await waitFor(() => expect(loadedTwiceNestedStreamByMe.value).toBe("milk"));
224
229
 
225
230
  assert(loadedTwiceNestedStreamByMe.value);
226
231
  });
@@ -231,21 +236,24 @@ describe("CoFeed resolution", async () => {
231
236
 
232
237
  const anotherAccount = await createJazzTestAccount();
233
238
 
234
- const queue = new Channel();
239
+ let result: CoFeedInstanceCoValuesNullable<co.Feed<co.Feed<z.z.ZodString>>>;
235
240
 
236
241
  TestStream.subscribe(
237
242
  stream.$jazz.id,
238
243
  { loadAs: anotherAccount },
239
244
  (subscribedStream) => {
240
- void queue.push(subscribedStream);
245
+ result = subscribedStream;
241
246
  },
242
247
  );
243
248
 
244
- const update1 = (await queue.next()).value;
245
- expect(
246
- update1.perAccount[accountId]?.value?.perAccount[accountId]?.value
247
- ?.perAccount[accountId]?.value,
248
- ).toBe("milk");
249
+ await waitFor(() => expect(result).toBeDefined());
250
+
251
+ await waitFor(() => {
252
+ expect(
253
+ result.perAccount[accountId]?.value?.perAccount[accountId]?.value
254
+ ?.perAccount[accountId]?.value,
255
+ ).toBe("milk");
256
+ });
249
257
 
250
258
  // When assigning a new nested stream, we get an update
251
259
  const newTwiceNested = TwiceNestedStream.create(["butter"], {
@@ -258,19 +266,22 @@ describe("CoFeed resolution", async () => {
258
266
 
259
267
  stream.$jazz.push(newNested);
260
268
 
261
- const update2 = (await queue.next()).value;
262
- expect(
263
- update2.perAccount[me.$jazz.id]?.value?.perAccount[me.$jazz.id]?.value
264
- ?.perAccount[me.$jazz.id]?.value,
265
- ).toBe("butter");
269
+ await waitFor(() => {
270
+ expect(
271
+ result.perAccount[accountId]?.value?.perAccount[accountId]?.value
272
+ ?.perAccount[accountId]?.value,
273
+ ).toBe("butter");
274
+ });
266
275
 
267
276
  // we get updates when the new nested stream changes
268
277
  newTwiceNested.$jazz.push("jam");
269
- const update3 = (await queue.next()).value;
270
- expect(
271
- update3.perAccount[me.$jazz.id]?.value?.perAccount[me.$jazz.id]?.value
272
- ?.perAccount[me.$jazz.id]?.value,
273
- ).toBe("jam");
278
+
279
+ await waitFor(() => {
280
+ expect(
281
+ result.perAccount[accountId]?.value?.perAccount[accountId]?.value
282
+ ?.perAccount[accountId]?.value,
283
+ ).toBe("jam");
284
+ });
274
285
  });
275
286
 
276
287
  test("Subscription without options", async () => {
@@ -122,6 +122,25 @@ describe("Simple CoList operations", async () => {
122
122
  expect(list[0]).toEqual("milk");
123
123
  });
124
124
 
125
+ test("CoList keys can be iterated over just like an array's", () => {
126
+ const TestList = co.list(z.string());
127
+ const list = ["a", "b", "c", "d", "e"];
128
+ const coList = TestList.create(list);
129
+ const keys = [];
130
+ for (const key in coList) {
131
+ keys.push(key);
132
+ }
133
+ expect(keys).toEqual(Object.keys(list));
134
+ expect(Object.keys(coList)).toEqual(Object.keys(list));
135
+ });
136
+
137
+ test("a CoList is structurally equal to an array", () => {
138
+ const TestList = co.list(z.string());
139
+ const list = ["a", "b", "c", "d", "e"];
140
+ const coList = TestList.create(list);
141
+ expect(coList).toEqual(list);
142
+ });
143
+
125
144
  describe("Mutation", () => {
126
145
  test("assignment", () => {
127
146
  const list = TestList.create(["bread", "butter", "onion"], {
@@ -963,17 +982,19 @@ describe("CoList subscription", async () => {
963
982
 
964
983
  expect(spy).toHaveBeenCalledTimes(1);
965
984
 
966
- expect(updates[0]?.[0]?.name).toEqual("Item 1");
967
- expect(updates[0]?.[1]?.name).toEqual("Item 2");
985
+ await waitFor(() => {
986
+ expect(updates[0]?.[0]?.name).toEqual("Item 1");
987
+ expect(updates[0]?.[1]?.name).toEqual("Item 2");
988
+ });
968
989
 
969
990
  list[0]!.$jazz.set("name", "Updated Item 1");
970
991
 
971
- await waitFor(() => expect(spy).toHaveBeenCalledTimes(2));
992
+ await waitFor(() => expect(spy).toHaveBeenCalledTimes(4));
972
993
 
973
994
  expect(updates[1]?.[0]?.name).toEqual("Updated Item 1");
974
995
  expect(updates[1]?.[1]?.name).toEqual("Item 2");
975
996
 
976
- expect(spy).toHaveBeenCalledTimes(2);
997
+ expect(spy).toHaveBeenCalledTimes(4);
977
998
  });
978
999
 
979
1000
  test("replacing list items triggers updates", async () => {
@@ -1312,3 +1333,25 @@ describe("co.list schema", () => {
1312
1333
  expect(keywords[1]?.toString()).toEqual("world");
1313
1334
  });
1314
1335
  });
1336
+
1337
+ describe("lastUpdatedAt", () => {
1338
+ test("empty list last updated time", () => {
1339
+ const emptyList = co.list(z.number()).create([]);
1340
+
1341
+ expect(emptyList.$jazz.lastUpdatedAt).not.toEqual(0);
1342
+ expect(emptyList.$jazz.lastUpdatedAt).toEqual(emptyList.$jazz.createdAt);
1343
+ });
1344
+
1345
+ test("last update should change on push", async () => {
1346
+ const list = co.list(z.string()).create(["John"]);
1347
+
1348
+ expect(list.$jazz.lastUpdatedAt).not.toEqual(0);
1349
+
1350
+ const updatedAt = list.$jazz.lastUpdatedAt;
1351
+
1352
+ await new Promise((r) => setTimeout(r, 10));
1353
+ list.$jazz.push("Jane");
1354
+
1355
+ expect(list.$jazz.lastUpdatedAt).not.toEqual(updatedAt);
1356
+ });
1357
+ });
@@ -1009,7 +1009,10 @@ describe("CoMap resolution", async () => {
1009
1009
  });
1010
1010
 
1011
1011
  assert(loadedPerson);
1012
- expect(loadedPerson.dog?.name).toEqual("Rex");
1012
+
1013
+ await waitFor(() => {
1014
+ expect(loadedPerson.dog?.name).toEqual("Rex");
1015
+ });
1013
1016
  });
1014
1017
 
1015
1018
  test("loading a remotely available map with skipRetry set to true", async () => {
@@ -1118,7 +1121,10 @@ describe("CoMap resolution", async () => {
1118
1121
 
1119
1122
  expect(resolved).toBe(true);
1120
1123
  assert(loadedPerson);
1121
- expect(loadedPerson.dog?.name).toEqual("Rex");
1124
+
1125
+ await waitFor(() => {
1126
+ expect(loadedPerson.dog?.name).toEqual("Rex");
1127
+ });
1122
1128
  });
1123
1129
 
1124
1130
  test("accessing the value refs", async () => {
@@ -1396,15 +1402,17 @@ describe("CoMap resolution", async () => {
1396
1402
 
1397
1403
  expect(spy).toHaveBeenCalledTimes(1);
1398
1404
 
1399
- expect(updates[0]?.dog?.name).toEqual("Rex");
1405
+ await waitFor(() => {
1406
+ expect(updates[0]?.dog?.name).toEqual("Rex");
1407
+ });
1400
1408
 
1401
1409
  person.dog!.$jazz.set("name", "Fido");
1402
1410
 
1403
- await waitFor(() => expect(spy).toHaveBeenCalledTimes(2));
1411
+ await waitFor(() => expect(spy).toHaveBeenCalledTimes(3));
1404
1412
 
1405
1413
  expect(updates[1]?.dog?.name).toEqual("Fido");
1406
1414
 
1407
- expect(spy).toHaveBeenCalledTimes(2);
1415
+ expect(spy).toHaveBeenCalledTimes(3);
1408
1416
  });
1409
1417
 
1410
1418
  test("replacing nested object triggers updates", async () => {
@@ -183,6 +183,7 @@ describe("CoPlainText", () => {
183
183
  sessionProvider: randomSessionProvider,
184
184
  peersToLoadFrom: [initialAsPeer],
185
185
  crypto: Crypto,
186
+ asActiveAccount: true,
186
187
  });
187
188
 
188
189
  // Load the text on the second peer
@@ -214,6 +215,7 @@ describe("CoPlainText", () => {
214
215
  sessionProvider: randomSessionProvider,
215
216
  peersToLoadFrom: [initialAsPeer],
216
217
  crypto: Crypto,
218
+ asActiveAccount: true,
217
219
  });
218
220
 
219
221
  const queue = new Channel();
@@ -242,3 +244,25 @@ describe("CoPlainText", () => {
242
244
  expect(update3.toString()).toBe("hello world");
243
245
  });
244
246
  });
247
+
248
+ describe("lastUpdatedAt", () => {
249
+ test("empty text last updated time", () => {
250
+ const text = co.plainText().create("");
251
+
252
+ expect(text.$jazz.lastUpdatedAt).toEqual(text.$jazz.createdAt);
253
+ expect(text.$jazz.lastUpdatedAt).not.toEqual(0);
254
+ });
255
+
256
+ test("last update should change on push", async () => {
257
+ const text = co.plainText().create("John");
258
+
259
+ expect(text.$jazz.lastUpdatedAt).not.toEqual(0);
260
+
261
+ const updatedAt = text.$jazz.lastUpdatedAt;
262
+
263
+ await new Promise((r) => setTimeout(r, 10));
264
+ text.$jazz.applyDiff("Jane");
265
+
266
+ expect(text.$jazz.lastUpdatedAt).not.toEqual(updatedAt);
267
+ });
268
+ });