jazz-tools 0.9.1 → 0.9.9

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.
@@ -13,9 +13,9 @@ import {
13
13
  MembersSym,
14
14
  Ref,
15
15
  ensureCoValueLoaded,
16
- loadCoValue,
17
- parseCoValueCreateOptions,
18
- subscribeToCoValue,
16
+ loadCoValueWithoutMe,
17
+ parseGroupCreateOptions,
18
+ subscribeToCoValueWithoutMe,
19
19
  subscribeToExistingCoValue,
20
20
  } from "../internal.js";
21
21
  import { AccountAndGroupProxyHandler, isControlledAccount } from "./account.js";
@@ -124,9 +124,9 @@ export class Group extends CoValueBase implements CoValue {
124
124
 
125
125
  static create<G extends Group>(
126
126
  this: CoValueClass<G>,
127
- options: { owner: Account } | Account,
127
+ options?: { owner: Account } | Account,
128
128
  ) {
129
- return new this(parseCoValueCreateOptions(options));
129
+ return new this(parseGroupCreateOptions(options));
130
130
  }
131
131
 
132
132
  myRole(): Role | undefined {
@@ -179,24 +179,56 @@ export class Group extends CoValueBase implements CoValue {
179
179
  }
180
180
 
181
181
  /** @category Subscription & Loading */
182
- static load<G extends Group, Depth>(
183
- this: CoValueClass<G>,
184
- id: ID<G>,
182
+ static load<C extends Group, Depth>(
183
+ this: CoValueClass<C>,
184
+ id: ID<C>,
185
+ depth: Depth & DepthsIn<C>,
186
+ ): Promise<DeeplyLoaded<C, Depth> | undefined>;
187
+ static load<C extends Group, Depth>(
188
+ this: CoValueClass<C>,
189
+ id: ID<C>,
185
190
  as: Account,
186
- depth: Depth & DepthsIn<G>,
187
- ): Promise<DeeplyLoaded<G, Depth> | undefined> {
188
- return loadCoValue(this, id, as, depth);
191
+ depth: Depth & DepthsIn<C>,
192
+ ): Promise<DeeplyLoaded<C, Depth> | undefined>;
193
+ static load<C extends Group, Depth>(
194
+ this: CoValueClass<C>,
195
+ id: ID<C>,
196
+ asOrDepth: Account | (Depth & DepthsIn<C>),
197
+ depth?: Depth & DepthsIn<C>,
198
+ ): Promise<DeeplyLoaded<C, Depth> | undefined> {
199
+ return loadCoValueWithoutMe(this, id, asOrDepth, depth);
189
200
  }
190
201
 
191
202
  /** @category Subscription & Loading */
192
- static subscribe<G extends Group, Depth>(
193
- this: CoValueClass<G>,
194
- id: ID<G>,
203
+ static subscribe<C extends Group, Depth>(
204
+ this: CoValueClass<C>,
205
+ id: ID<C>,
206
+ depth: Depth & DepthsIn<C>,
207
+ listener: (value: DeeplyLoaded<C, Depth>) => void,
208
+ ): () => void;
209
+ static subscribe<C extends Group, Depth>(
210
+ this: CoValueClass<C>,
211
+ id: ID<C>,
195
212
  as: Account,
196
- depth: Depth & DepthsIn<G>,
197
- listener: (value: DeeplyLoaded<G, Depth>) => void,
213
+ depth: Depth & DepthsIn<C>,
214
+ listener: (value: DeeplyLoaded<C, Depth>) => void,
215
+ ): () => void;
216
+ static subscribe<C extends Group, Depth>(
217
+ this: CoValueClass<C>,
218
+ id: ID<C>,
219
+ asOrDepth: Account | (Depth & DepthsIn<C>),
220
+ depthOrListener:
221
+ | (Depth & DepthsIn<C>)
222
+ | ((value: DeeplyLoaded<C, Depth>) => void),
223
+ listener?: (value: DeeplyLoaded<C, Depth>) => void,
198
224
  ): () => void {
199
- return subscribeToCoValue<G, Depth>(this, id, as, depth, listener);
225
+ return subscribeToCoValueWithoutMe<C, Depth>(
226
+ this,
227
+ id,
228
+ asOrDepth,
229
+ depthOrListener,
230
+ listener,
231
+ );
200
232
  }
201
233
 
202
234
  /** @category Subscription & Loading */
@@ -7,6 +7,7 @@ import {
7
7
  SessionID,
8
8
  } from "cojson";
9
9
  import { CoStreamItem, RawCoStream } from "cojson";
10
+ import { activeAccountContext } from "../implementation/activeAccountContext.js";
10
11
  import { type Account } from "./account.js";
11
12
  import { CoValue, CoValueClass, ID, loadCoValue } from "./interfaces.js";
12
13
 
@@ -312,7 +313,9 @@ export class InboxSender<I extends CoValue, O extends CoValue | undefined> {
312
313
  static async load<
313
314
  I extends CoValue,
314
315
  O extends CoValue | undefined = undefined,
315
- >(inboxOwnerID: ID<Account>, currentAccount: Account) {
316
+ >(inboxOwnerID: ID<Account>, currentAccount?: Account) {
317
+ currentAccount ||= activeAccountContext.get();
318
+
316
319
  const node = currentAccount._raw.core.node;
317
320
 
318
321
  const inboxOwnerRaw = await node.load(
@@ -347,7 +350,9 @@ export class InboxSender<I extends CoValue, O extends CoValue | undefined> {
347
350
  }
348
351
  }
349
352
 
350
- async function acceptInvite(invite: string, account: Account) {
353
+ async function acceptInvite(invite: string, account?: Account) {
354
+ account ||= activeAccountContext.get();
355
+
351
356
  const id = invite.slice(0, invite.indexOf("/")) as CoID<MessagesStream>;
352
357
 
353
358
  const inviteSecret = invite.slice(invite.indexOf("/") + 1) as InviteSecret;
@@ -4,6 +4,7 @@ import type {
4
4
  RawCoValue,
5
5
  } from "cojson";
6
6
  import { RawAccount } from "cojson";
7
+ import { activeAccountContext } from "../implementation/activeAccountContext.js";
7
8
  import { AnonymousJazzAgent } from "../implementation/anonymousJazzAgent.js";
8
9
  import type { DeeplyLoaded, DepthsIn } from "../internal.js";
9
10
  import {
@@ -154,6 +155,24 @@ export class CoValueBase implements CoValue {
154
155
  }
155
156
  }
156
157
 
158
+ export function loadCoValueWithoutMe<V extends CoValue, Depth>(
159
+ cls: CoValueClass<V>,
160
+ id: ID<V>,
161
+ asOrDepth: Account | AnonymousJazzAgent | (Depth & DepthsIn<V>),
162
+ depth?: Depth & DepthsIn<V>,
163
+ ) {
164
+ if (isAccountInstance(asOrDepth) || isAnonymousAgentInstance(asOrDepth)) {
165
+ if (!depth) {
166
+ throw new Error(
167
+ "Depth is required when loading a CoValue as an Account or AnonymousJazzAgent",
168
+ );
169
+ }
170
+ return loadCoValue(cls, id, asOrDepth, depth);
171
+ }
172
+
173
+ return loadCoValue(cls, id, activeAccountContext.get(), asOrDepth);
174
+ }
175
+
157
176
  export function loadCoValue<V extends CoValue, Depth>(
158
177
  cls: CoValueClass<V>,
159
178
  id: ID<V>,
@@ -189,6 +208,41 @@ export function ensureCoValueLoaded<V extends CoValue, Depth>(
189
208
  );
190
209
  }
191
210
 
211
+ export function subscribeToCoValueWithoutMe<V extends CoValue, Depth>(
212
+ cls: CoValueClass<V>,
213
+ id: ID<V>,
214
+ asOrDepth: Account | AnonymousJazzAgent | (Depth & DepthsIn<V>),
215
+ depthOrListener:
216
+ | (Depth & DepthsIn<V>)
217
+ | ((value: DeeplyLoaded<V, Depth>) => void),
218
+ listener?: (value: DeeplyLoaded<V, Depth>) => void,
219
+ ) {
220
+ if (isAccountInstance(asOrDepth) || isAnonymousAgentInstance(asOrDepth)) {
221
+ if (typeof depthOrListener !== "function") {
222
+ return subscribeToCoValue<V, Depth>(
223
+ cls,
224
+ id,
225
+ asOrDepth,
226
+ depthOrListener,
227
+ listener!,
228
+ );
229
+ }
230
+ throw new Error("Invalid arguments");
231
+ }
232
+
233
+ if (typeof depthOrListener !== "function") {
234
+ throw new Error("Invalid arguments");
235
+ }
236
+
237
+ return subscribeToCoValue<V, Depth>(
238
+ cls,
239
+ id,
240
+ activeAccountContext.get(),
241
+ asOrDepth,
242
+ depthOrListener,
243
+ );
244
+ }
245
+
192
246
  export function subscribeToCoValue<V extends CoValue, Depth>(
193
247
  cls: CoValueClass<V>,
194
248
  id: ID<V>,
@@ -304,20 +358,69 @@ export function subscribeToExistingCoValue<V extends CoValue, Depth>(
304
358
  );
305
359
  }
306
360
 
361
+ export function isAccountInstance(instance: unknown): instance is Account {
362
+ if (typeof instance !== "object" || instance === null) {
363
+ return false;
364
+ }
365
+
366
+ return "_type" in instance && instance._type === "Account";
367
+ }
368
+
369
+ export function isAnonymousAgentInstance(
370
+ instance: unknown,
371
+ ): instance is AnonymousJazzAgent {
372
+ if (typeof instance !== "object" || instance === null) {
373
+ return false;
374
+ }
375
+
376
+ return "_type" in instance && instance._type === "Anonymous";
377
+ }
378
+
307
379
  export function parseCoValueCreateOptions(
308
380
  options:
309
381
  | {
310
- owner: Account | Group;
382
+ owner?: Account | Group;
311
383
  unique?: CoValueUniqueness["uniqueness"];
312
384
  }
313
385
  | Account
314
- | Group,
386
+ | Group
387
+ | undefined,
315
388
  ) {
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
- };
389
+ const Group = RegisteredSchemas["Group"];
390
+
391
+ if (!options) {
392
+ return { owner: Group.create(), uniqueness: undefined };
393
+ }
394
+
395
+ if ("_type" in options) {
396
+ if (options._type === "Account" || options._type === "Group") {
397
+ return { owner: options, uniqueness: undefined };
398
+ }
399
+ }
400
+
401
+ const uniqueness = options.unique
402
+ ? { uniqueness: options.unique }
403
+ : undefined;
404
+
405
+ return {
406
+ owner: options.owner ?? Group.create(),
407
+ uniqueness,
408
+ };
409
+ }
410
+
411
+ export function parseGroupCreateOptions(
412
+ options:
413
+ | {
414
+ owner?: Account;
415
+ }
416
+ | Account
417
+ | undefined,
418
+ ) {
419
+ if (!options) {
420
+ return { owner: activeAccountContext.get() };
421
+ }
422
+
423
+ return "_type" in options && isAccountInstance(options)
424
+ ? { owner: options }
425
+ : { owner: options.owner ?? activeAccountContext.get() };
323
426
  }
package/src/exports.ts CHANGED
@@ -12,27 +12,32 @@ export type { CoValue, ID } from "./internal.js";
12
12
 
13
13
  export { Encoders, co } from "./internal.js";
14
14
 
15
- export {
16
- Inbox,
17
- InboxSender,
18
- } from "./coValues/inbox.js";
15
+ export { Inbox, InboxSender } from "./coValues/inbox.js";
19
16
 
20
17
  export {
21
18
  Account,
22
19
  isControlledAccount,
23
20
  type AccountClass,
24
21
  } from "./coValues/account.js";
25
- export { Group } from "./coValues/group.js";
26
22
  export {
27
- CoStream,
23
+ BinaryCoStream,
28
24
  CoFeed,
25
+ CoStream,
29
26
  FileStream,
30
- BinaryCoStream,
31
27
  } from "./coValues/coFeed.js";
32
28
  export { CoList } from "./coValues/coList.js";
33
29
  export { CoMap, type CoMapInit } from "./coValues/coMap.js";
34
- export { CoValueBase } from "./coValues/interfaces.js";
30
+ export { CoPlainText, type TextPos } from "./coValues/coPlainText.js";
31
+ export {
32
+ CoRichText,
33
+ Marks,
34
+ type TreeLeaf,
35
+ type TreeNode,
36
+ type ResolvedMark,
37
+ } from "./coValues/coRichText.js";
35
38
  export { ImageDefinition } from "./coValues/extensions/imageDef.js";
39
+ export { Group } from "./coValues/group.js";
40
+ export { CoValueBase } from "./coValues/interfaces.js";
36
41
  export { Profile } from "./coValues/profile.js";
37
42
  export { SchemaUnion } from "./coValues/schemaUnion.js";
38
43
 
@@ -0,0 +1,33 @@
1
+ import type { Account } from "../coValues/account.js";
2
+
3
+ class ActiveAccountContext {
4
+ private activeAccount: Account | null = null;
5
+ private guestMode: boolean = false;
6
+
7
+ set(account: Account) {
8
+ this.activeAccount = account;
9
+ this.guestMode = false;
10
+ }
11
+
12
+ setGuestMode() {
13
+ this.guestMode = true;
14
+ }
15
+
16
+ get() {
17
+ if (!this.activeAccount) {
18
+ if (this.guestMode) {
19
+ throw new Error(
20
+ "Something that expects a full active account was called in guest mode.",
21
+ );
22
+ }
23
+
24
+ throw new Error("No active account");
25
+ }
26
+
27
+ return this.activeAccount;
28
+ }
29
+ }
30
+
31
+ export type { ActiveAccountContext };
32
+
33
+ export const activeAccountContext = new ActiveAccountContext();
@@ -12,6 +12,7 @@ import {
12
12
  import { type Account, type AccountClass } from "../coValues/account.js";
13
13
  import { RegisteredSchemas } from "../coValues/registeredSchemas.js";
14
14
  import type { ID } from "../internal.js";
15
+ import { activeAccountContext } from "./activeAccountContext.js";
15
16
  import { AnonymousJazzAgent } from "./anonymousJazzAgent.js";
16
17
 
17
18
  export type Credentials = {
@@ -169,11 +170,14 @@ export async function createJazzContext<Acc extends Account>(
169
170
  fromRaw: rawAccount,
170
171
  }) as Acc;
171
172
 
173
+ activeAccountContext.set(account);
174
+
172
175
  await account.applyMigration(creationProps);
173
176
  },
174
177
  });
175
178
 
176
179
  const account = AccountSchema.fromNode(node);
180
+ activeAccountContext.set(account);
177
181
 
178
182
  if (authResult.saveCredentials) {
179
183
  await authResult.saveCredentials({
@@ -217,12 +221,14 @@ export async function createJazzContext<Acc extends Account>(
217
221
  const account = new AccountSchema({
218
222
  fromRaw: rawAccount,
219
223
  }) as Acc;
224
+ activeAccountContext.set(account);
220
225
 
221
226
  await account.applyMigration(creationProps);
222
227
  },
223
228
  });
224
229
 
225
230
  const account = AccountSchema.fromNode(node);
231
+ activeAccountContext.set(account);
226
232
 
227
233
  await authResult.saveCredentials({
228
234
  accountID: node.account.id as unknown as ID<Account>,
@@ -267,6 +273,8 @@ export async function createAnonymousJazzContext({
267
273
  node.syncManager.addPeer(peer);
268
274
  }
269
275
 
276
+ activeAccountContext.setGuestMode();
277
+
270
278
  return {
271
279
  agent: new AnonymousJazzAgent(node),
272
280
  done: () => {},
package/src/internal.ts CHANGED
@@ -1,13 +1,13 @@
1
- export * from "./implementation/symbols.js";
2
- export * from "./implementation/inspect.js";
3
1
  export * from "./coValues/interfaces.js";
2
+ export * from "./implementation/inspect.js";
3
+ export * from "./implementation/symbols.js";
4
4
 
5
- export * from "./implementation/errors.js";
5
+ export * from "./coValues/deepLoading.js";
6
6
  export * from "./implementation/anonymousJazzAgent.js";
7
+ export * from "./implementation/errors.js";
7
8
  export * from "./implementation/refs.js";
8
9
  export * from "./implementation/schema.js";
9
10
  export * from "./implementation/subscriptionScope.js";
10
- export * from "./coValues/deepLoading.js";
11
11
 
12
12
  export * from "./implementation/createContext.js";
13
13
 
package/src/testing.ts CHANGED
@@ -2,6 +2,7 @@ import { AgentSecret, CryptoProvider, Peer } from "cojson";
2
2
  import { cojsonInternals } from "cojson";
3
3
  import { PureJSCrypto } from "cojson/crypto";
4
4
  import { Account, type AccountClass } from "./exports.js";
5
+ import { activeAccountContext } from "./implementation/activeAccountContext.js";
5
6
  import {
6
7
  type AnonymousJazzAgent,
7
8
  type CoValueClass,
@@ -42,6 +43,7 @@ class TestJSCrypto extends PureJSCrypto {
42
43
  }
43
44
 
44
45
  export async function createJazzTestAccount<Acc extends Account>(options?: {
46
+ isCurrentActiveAccount?: boolean;
45
47
  AccountSchema?: CoValueClass<Acc>;
46
48
  }): Promise<Acc> {
47
49
  const AccountSchema = (options?.AccountSchema ??
@@ -52,10 +54,17 @@ export async function createJazzTestAccount<Acc extends Account>(options?: {
52
54
  },
53
55
  crypto: await TestJSCrypto.create(),
54
56
  });
57
+ if (options?.isCurrentActiveAccount) {
58
+ activeAccountContext.set(account);
59
+ }
55
60
 
56
61
  return account;
57
62
  }
58
63
 
64
+ export function setActiveAccount(account: Account) {
65
+ activeAccountContext.set(account);
66
+ }
67
+
59
68
  export async function createJazzTestGuest() {
60
69
  const ctx = await createAnonymousJazzContext({
61
70
  crypto: await PureJSCrypto.create(),
@@ -1,5 +1,6 @@
1
1
  import { expect, test } from "vitest";
2
2
  import { CoMap, co } from "../exports.js";
3
+ import { createJazzTestAccount } from "../testing.js";
3
4
  import { setupTwoNodes } from "./utils.js";
4
5
 
5
6
  test("waitForAllCoValuesSync should resolve when all the values are synced", async () => {
@@ -14,6 +14,7 @@ import {
14
14
  isControlledAccount,
15
15
  } from "../index.web.js";
16
16
  import { randomSessionProvider } from "../internal.js";
17
+ import { createJazzTestAccount } from "../testing.js";
17
18
  import { setupTwoNodes } from "./utils.js";
18
19
 
19
20
  const Crypto = await WasmCrypto.create();
@@ -499,4 +500,16 @@ describe("waitForSync", async () => {
499
500
 
500
501
  expect(loadedStream).not.toBe("unavailable");
501
502
  });
503
+
504
+ test("should rely on the current active account if no account is provided", async () => {
505
+ const account = await createJazzTestAccount({
506
+ isCurrentActiveAccount: true,
507
+ });
508
+
509
+ const stream = FileStream.create();
510
+ expect(stream._owner._type).toEqual("Group");
511
+ expect(stream._owner.castAs(Group)._raw.roleOf(account._raw.id)).toEqual(
512
+ "admin",
513
+ );
514
+ });
502
515
  });
@@ -0,0 +1,161 @@
1
+ import { connectedPeers } from "cojson/src/streamUtils.js";
2
+ import { describe, expect, test } from "vitest";
3
+ import {
4
+ Account,
5
+ CoPlainText,
6
+ WasmCrypto,
7
+ cojsonInternals,
8
+ createJazzContext,
9
+ fixedCredentialsAuth,
10
+ isControlledAccount,
11
+ } from "../index.web.js";
12
+ import { randomSessionProvider } from "../internal.js";
13
+
14
+ const Crypto = await WasmCrypto.create();
15
+
16
+ describe("CoPlainText", () => {
17
+ const initNodeAndText = async () => {
18
+ const me = await Account.create({
19
+ creationProps: { name: "Hermes Puggington" },
20
+ crypto: Crypto,
21
+ });
22
+
23
+ const text = CoPlainText.create("hello world", { owner: me });
24
+
25
+ return { me, text };
26
+ };
27
+
28
+ describe("Simple CoPlainText operations", async () => {
29
+ const { me, text } = await initNodeAndText();
30
+
31
+ test("Construction", () => {
32
+ expect(text + "").toEqual("hello world");
33
+ });
34
+
35
+ describe("Mutation", () => {
36
+ test("insertion", () => {
37
+ const text = CoPlainText.create("hello world", { owner: me });
38
+
39
+ text.insertAfter(5, " cruel");
40
+ expect(text + "").toEqual("hello cruel world");
41
+
42
+ text.insertAfter(0, "Hello, ");
43
+ expect(text + "").toEqual("Hello, hello cruel world");
44
+ });
45
+
46
+ test("deletion", () => {
47
+ const text = CoPlainText.create("hello world", { owner: me });
48
+
49
+ text.deleteRange({ from: 3, to: 8 });
50
+ expect(text + "").toEqual("helrld");
51
+ });
52
+ });
53
+
54
+ describe("Position operations", () => {
55
+ test("idxBefore returns index before a position", () => {
56
+ const text = CoPlainText.create("hello world", { owner: me });
57
+
58
+ // Get position at index 5 (between "hello" and " world")
59
+ const pos = text.posBefore(5);
60
+ expect(pos).toBeDefined();
61
+
62
+ // Verify idxBefore returns the index before the position (4)
63
+ // This makes sense as the position is between characters,
64
+ // and idxBefore returns the index of the last character before that position
65
+ const idx = text.idxBefore(pos!);
66
+ expect(idx).toBe(4); // Index of 'o' in "hello"
67
+ });
68
+
69
+ test("idxAfter returns index after a position", () => {
70
+ const text = CoPlainText.create("hello world", { owner: me });
71
+
72
+ // Get position at index 5 (between "hello" and " world")
73
+ const pos = text.posBefore(5);
74
+ expect(pos).toBeDefined();
75
+
76
+ // Verify idxAfter returns the index after the position (5)
77
+ // This makes sense as the position is between characters,
78
+ // and idxAfter returns the index of the first character after that position
79
+ const idx = text.idxAfter(pos!);
80
+ expect(idx).toBe(5); // Index of ' ' in "hello world"
81
+ });
82
+ });
83
+ });
84
+
85
+ describe("Loading and availability", () => {
86
+ test("can load text across peers", async () => {
87
+ const { me, text } = await initNodeAndText();
88
+ const id = text.id;
89
+
90
+ // Set up peer connections
91
+ const [initialAsPeer, secondPeer] = connectedPeers("initial", "second", {
92
+ peer1role: "server",
93
+ peer2role: "client",
94
+ });
95
+
96
+ if (!isControlledAccount(me)) {
97
+ throw "me is not a controlled account";
98
+ }
99
+ me._raw.core.node.syncManager.addPeer(secondPeer);
100
+ const { account: meOnSecondPeer } = await createJazzContext({
101
+ auth: fixedCredentialsAuth({
102
+ accountID: me.id,
103
+ secret: me._raw.agentSecret,
104
+ }),
105
+ sessionProvider: randomSessionProvider,
106
+ peersToLoadFrom: [initialAsPeer],
107
+ crypto: Crypto,
108
+ });
109
+
110
+ // Load the text on the second peer
111
+ const loaded = await CoPlainText.load(id, meOnSecondPeer);
112
+ expect(loaded).toBeDefined();
113
+ expect(loaded!.toString()).toBe("hello world");
114
+ });
115
+ });
116
+
117
+ test("Subscription & auto-resolution", async () => {
118
+ const { me, text } = await initNodeAndText();
119
+
120
+ // Set up peer connections
121
+ const [initialAsPeer, secondPeer] = connectedPeers("initial", "second", {
122
+ peer1role: "server",
123
+ peer2role: "client",
124
+ });
125
+
126
+ if (!isControlledAccount(me)) {
127
+ throw "me is not a controlled account";
128
+ }
129
+ me._raw.core.node.syncManager.addPeer(secondPeer);
130
+ const { account: meOnSecondPeer } = await createJazzContext({
131
+ auth: fixedCredentialsAuth({
132
+ accountID: me.id,
133
+ secret: me._raw.agentSecret,
134
+ }),
135
+ sessionProvider: randomSessionProvider,
136
+ peersToLoadFrom: [initialAsPeer],
137
+ crypto: Crypto,
138
+ });
139
+
140
+ const queue = new cojsonInternals.Channel();
141
+
142
+ // Subscribe to text updates
143
+ CoPlainText.subscribe(text.id, meOnSecondPeer, (subscribedText) => {
144
+ void queue.push(subscribedText);
145
+ });
146
+
147
+ // Initial subscription should give us the text
148
+ const update1 = (await queue.next()).value;
149
+ expect(update1.toString()).toBe("hello world");
150
+
151
+ // When we make a change, we should get an update
152
+ text.insertAfter(5, " beautiful");
153
+ const update2 = (await queue.next()).value;
154
+ expect(update2.toString()).toBe("hello beautiful world");
155
+
156
+ // When we make another change, we should get another update
157
+ update2.deleteRange({ from: 5, to: 15 }); // Delete " beautiful"
158
+ const update3 = (await queue.next()).value;
159
+ expect(update3.toString()).toBe("hello world");
160
+ });
161
+ });