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.
- package/.turbo/turbo-build.log +57 -57
- package/CHANGELOG.md +33 -0
- package/dist/better-auth/auth/client.d.ts.map +1 -1
- package/dist/better-auth/auth/client.js +7 -1
- package/dist/better-auth/auth/client.js.map +1 -1
- package/dist/better-auth/auth/react.d.ts +0 -2145
- package/dist/better-auth/auth/react.d.ts.map +1 -1
- package/dist/better-auth/auth/react.js +2 -14
- package/dist/better-auth/auth/react.js.map +1 -1
- package/dist/better-auth/auth/server.d.ts.map +1 -1
- package/dist/better-auth/auth/server.js +77 -22
- package/dist/better-auth/auth/server.js.map +1 -1
- package/dist/better-auth/auth/tests/react.test.d.ts +2 -0
- package/dist/better-auth/auth/tests/react.test.d.ts.map +1 -0
- package/dist/{chunk-3LE7N6TH.js → chunk-CFAY3FMQ.js} +192 -101
- package/dist/chunk-CFAY3FMQ.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/inspector/{custom-element-WCY6D3QJ.js → custom-element-G6SPZEBR.js} +308 -97
- package/dist/inspector/custom-element-G6SPZEBR.js.map +1 -0
- package/dist/inspector/index.d.ts +5 -1
- package/dist/inspector/index.d.ts.map +1 -1
- package/dist/inspector/index.js +318 -56
- package/dist/inspector/index.js.map +1 -1
- package/dist/inspector/register-custom-element.js +1 -1
- package/dist/inspector/ui/button.d.ts +1 -1
- package/dist/inspector/ui/button.d.ts.map +1 -1
- package/dist/inspector/ui/heading.d.ts +2 -1
- package/dist/inspector/ui/heading.d.ts.map +1 -1
- package/dist/inspector/ui/input.d.ts.map +1 -1
- package/dist/inspector/ui/modal.d.ts +16 -0
- package/dist/inspector/ui/modal.d.ts.map +1 -0
- package/dist/inspector/viewer/delete-local-data.d.ts +2 -0
- package/dist/inspector/viewer/delete-local-data.d.ts.map +1 -0
- package/dist/inspector/viewer/{inpsector-button.d.ts → inspector-button.d.ts} +1 -1
- package/dist/inspector/viewer/{inpsector-button.d.ts.map → inspector-button.d.ts.map} +1 -1
- package/dist/inspector/viewer/new-app.d.ts +1 -4
- package/dist/inspector/viewer/new-app.d.ts.map +1 -1
- package/dist/react/hooks.d.ts +1 -1
- package/dist/react/hooks.d.ts.map +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react-core/hooks.d.ts +133 -0
- package/dist/react-core/hooks.d.ts.map +1 -1
- package/dist/react-core/index.js +83 -17
- package/dist/react-core/index.js.map +1 -1
- package/dist/react-core/tests/useCoStateWithSelector.test.d.ts +2 -0
- package/dist/react-core/tests/useCoStateWithSelector.test.d.ts.map +1 -0
- package/dist/react-native-core/hooks.d.ts +1 -1
- package/dist/react-native-core/hooks.d.ts.map +1 -1
- package/dist/react-native-core/index.js +3 -1
- package/dist/react-native-core/index.js.map +1 -1
- package/dist/testing.js +2 -2
- package/dist/testing.js.map +1 -1
- package/dist/tools/coValues/CoValueBase.d.ts +14 -0
- package/dist/tools/coValues/CoValueBase.d.ts.map +1 -1
- package/dist/tools/coValues/coMap.d.ts +0 -12
- package/dist/tools/coValues/coMap.d.ts.map +1 -1
- package/dist/tools/coValues/inbox.d.ts +5 -5
- package/dist/tools/coValues/inbox.d.ts.map +1 -1
- package/dist/tools/implementation/createContext.d.ts +2 -1
- package/dist/tools/implementation/createContext.d.ts.map +1 -1
- package/dist/tools/tests/utils.d.ts.map +1 -1
- package/dist/worker/index.d.ts +12 -2
- package/dist/worker/index.d.ts.map +1 -1
- package/dist/worker/index.js +10 -4
- package/dist/worker/index.js.map +1 -1
- package/package.json +6 -4
- package/src/better-auth/auth/client.ts +8 -2
- package/src/better-auth/auth/react.tsx +2 -51
- package/src/better-auth/auth/server.ts +98 -24
- package/src/better-auth/auth/tests/client.test.ts +92 -4
- package/src/better-auth/auth/tests/react.test.tsx +43 -0
- package/src/better-auth/auth/tests/server.test.ts +276 -98
- package/src/inspector/custom-element.tsx +1 -1
- package/src/inspector/index.tsx +44 -0
- package/src/inspector/ui/button.tsx +15 -1
- package/src/inspector/ui/heading.tsx +7 -2
- package/src/inspector/ui/input.tsx +6 -2
- package/src/inspector/ui/modal.tsx +158 -0
- package/src/inspector/viewer/delete-local-data.tsx +101 -0
- package/src/inspector/viewer/new-app.tsx +3 -19
- package/src/react/hooks.tsx +1 -0
- package/src/react/index.ts +1 -0
- package/src/react-core/hooks.ts +162 -0
- package/src/react-core/tests/useCoStateWithSelector.test.ts +149 -0
- package/src/react-native-core/hooks.tsx +1 -0
- package/src/tools/coValues/CoValueBase.ts +32 -0
- package/src/tools/coValues/coList.ts +35 -0
- package/src/tools/coValues/coMap.ts +0 -18
- package/src/tools/coValues/inbox.ts +190 -108
- package/src/tools/implementation/createContext.ts +9 -2
- package/src/tools/testing.ts +1 -1
- package/src/tools/tests/coFeed.test.ts +33 -22
- package/src/tools/tests/coList.test.ts +47 -4
- package/src/tools/tests/coMap.test.ts +13 -5
- package/src/tools/tests/coPlainText.test.ts +24 -0
- package/src/tools/tests/createContext.test.ts +24 -0
- package/src/tools/tests/deepLoading.test.ts +2 -0
- package/src/tools/tests/exportImport.test.ts +3 -1
- package/src/tools/tests/groupsAndAccounts.test.ts +56 -44
- package/src/tools/tests/inbox.test.ts +293 -31
- package/src/tools/tests/patterns/requestToJoin.test.ts +14 -6
- package/src/tools/tests/utils.ts +1 -0
- package/src/worker/index.ts +21 -5
- package/tsup.config.ts +1 -1
- package/dist/chunk-3LE7N6TH.js.map +0 -1
- package/dist/inspector/custom-element-WCY6D3QJ.js.map +0 -1
- package/src/inspector/index.ts +0 -23
- /package/src/inspector/viewer/{inpsector-button.tsx → inspector-button.tsx} +0 -0
@@ -1,5 +1,12 @@
|
|
1
|
-
import {
|
2
|
-
|
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
|
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
|
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
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
136
|
-
|
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
|
-
|
139
|
-
|
140
|
-
failTimer = undefined;
|
262
|
+
inboxMessage.set("processed", true);
|
263
|
+
this.processed.push(txKey);
|
141
264
|
};
|
142
265
|
|
143
|
-
const
|
144
|
-
|
266
|
+
const handleError = (
|
267
|
+
txKey: TxKey,
|
268
|
+
messageId: CoID<InboxMessage<CoValue, any>>,
|
269
|
+
error: Error,
|
270
|
+
) => {
|
271
|
+
console.error(error);
|
145
272
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
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(
|
299
|
+
console.warn(
|
300
|
+
"Received message from unknown account",
|
301
|
+
tx.txID.sessionID,
|
302
|
+
);
|
154
303
|
continue;
|
155
304
|
}
|
156
305
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
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
|
-
|
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
|
-
|
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();
|
package/src/tools/testing.ts
CHANGED
@@ -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 {
|
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
|
-
|
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
|
-
|
245
|
+
result = subscribedStream;
|
241
246
|
},
|
242
247
|
);
|
243
248
|
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
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
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
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
|
-
|
270
|
-
|
271
|
-
|
272
|
-
?.perAccount[
|
273
|
-
|
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
|
-
|
967
|
-
|
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(
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
1411
|
+
await waitFor(() => expect(spy).toHaveBeenCalledTimes(3));
|
1404
1412
|
|
1405
1413
|
expect(updates[1]?.dog?.name).toEqual("Fido");
|
1406
1414
|
|
1407
|
-
expect(spy).toHaveBeenCalledTimes(
|
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
|
+
});
|