jazz-tools 0.9.1 → 0.9.8

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.
@@ -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,31 @@ 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
+ } from "./coValues/coRichText.js";
35
37
  export { ImageDefinition } from "./coValues/extensions/imageDef.js";
38
+ export { Group } from "./coValues/group.js";
39
+ export { CoValueBase } from "./coValues/interfaces.js";
36
40
  export { Profile } from "./coValues/profile.js";
37
41
  export { SchemaUnion } from "./coValues/schemaUnion.js";
38
42
 
@@ -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/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,33 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { Account, CoPlainText, WasmCrypto } from "../index.web.js";
3
+
4
+ const Crypto = await WasmCrypto.create();
5
+
6
+ describe("Simple CoPlainText operations", async () => {
7
+ const me = await Account.create({
8
+ creationProps: { name: "Hermes Puggington" },
9
+ crypto: Crypto,
10
+ });
11
+
12
+ const text = CoPlainText.create("hello world", { owner: me });
13
+
14
+ test("Construction", () => {
15
+ expect(text + "").toEqual("hello world");
16
+ });
17
+
18
+ describe("Mutation", () => {
19
+ test("insertion", () => {
20
+ const text = CoPlainText.create("hello world", { owner: me });
21
+
22
+ text.insertAfter(5, " cruel");
23
+ expect(text + "").toEqual("hello cruel world");
24
+ });
25
+
26
+ test("deletion", () => {
27
+ const text = CoPlainText.create("hello world", { owner: me });
28
+
29
+ text.deleteRange({ from: 3, to: 8 });
30
+ expect(text + "").toEqual("helrld");
31
+ });
32
+ });
33
+ });
@@ -0,0 +1,57 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { Account, CoRichText, Marks, WasmCrypto } from "../index.web.js";
3
+
4
+ const Crypto = await WasmCrypto.create();
5
+
6
+ describe("Simple CoRichText operations", async () => {
7
+ const me = await Account.create({
8
+ creationProps: { name: "Hermes Puggington" },
9
+ crypto: Crypto,
10
+ });
11
+
12
+ const text = CoRichText.createFromPlainText("hello world", { owner: me });
13
+
14
+ test("Construction", () => {
15
+ expect(text + "").toEqual("hello world");
16
+ });
17
+
18
+ describe("Mutation", () => {
19
+ test("insertion", () => {
20
+ const text = CoRichText.createFromPlainText("hello world", {
21
+ owner: me,
22
+ });
23
+
24
+ text.insertAfter(5, " cruel");
25
+ expect(text + "").toEqual("hello cruel world");
26
+ });
27
+
28
+ test("deletion", () => {
29
+ const text = CoRichText.createFromPlainText("hello world", {
30
+ owner: me,
31
+ });
32
+
33
+ text.deleteRange({ from: 3, to: 8 });
34
+ expect(text + "").toEqual("helrld");
35
+ });
36
+
37
+ test("inserting ranges", () => {
38
+ const text = CoRichText.createFromPlainText("hello world", {
39
+ owner: me,
40
+ });
41
+
42
+ text.insertMark(6, 9, Marks.Strong, { tag: "strong" });
43
+
44
+ expect(text.resolveMarks()).toEqual([
45
+ {
46
+ startAfter: 6,
47
+ startBefore: 7,
48
+ endAfter: 9,
49
+ endBefore: 10,
50
+ tag: "strong",
51
+ from: text.marks![0],
52
+ sourceMark: text.marks![0],
53
+ },
54
+ ]);
55
+ });
56
+ });
57
+ });
@@ -0,0 +1,90 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { Group } from "../coValues/group";
3
+ import { Account } from "../exports";
4
+ import {
5
+ parseCoValueCreateOptions,
6
+ parseGroupCreateOptions,
7
+ } from "../internal";
8
+ import { createJazzTestAccount } from "../testing";
9
+
10
+ beforeEach(async () => {
11
+ await createJazzTestAccount({
12
+ isCurrentActiveAccount: true,
13
+ });
14
+ });
15
+
16
+ describe("parseCoValueCreateOptions", () => {
17
+ it("should create a new group when no options provided", () => {
18
+ const result = parseCoValueCreateOptions(undefined);
19
+ expect(result.owner._type).toBe("Group");
20
+ expect(
21
+ result.owner.castAs(Group)._raw.roleOf(Account.getMe()._raw.id),
22
+ ).toBe("admin");
23
+ expect(result.uniqueness).toBeUndefined();
24
+ });
25
+
26
+ it("should use the account as the owner when passing an Account", async () => {
27
+ const account = await createJazzTestAccount();
28
+ const result = parseCoValueCreateOptions(account);
29
+ expect(result.owner).toBe(account);
30
+ expect(result.uniqueness).toBeUndefined();
31
+ });
32
+
33
+ it("should use existing group when passing a Group", () => {
34
+ const group = Group.create();
35
+ const result = parseCoValueCreateOptions(group);
36
+ expect(result.owner).toBe(group);
37
+ expect(
38
+ result.owner.castAs(Group)._raw.roleOf(Account.getMe()._raw.id),
39
+ ).toBe("admin");
40
+ expect(result.uniqueness).toBeUndefined();
41
+ });
42
+
43
+ it("should handle options with uniqueness", () => {
44
+ const group = Group.create();
45
+ const result = parseCoValueCreateOptions({
46
+ unique: "per-group",
47
+ owner: group,
48
+ });
49
+ expect(result.owner).toBe(group);
50
+ expect(result.uniqueness?.uniqueness).toBe("per-group");
51
+ });
52
+
53
+ it("should use the account as the owner when passing an Account", async () => {
54
+ const account = await createJazzTestAccount();
55
+ const result = parseCoValueCreateOptions({
56
+ owner: account,
57
+ unique: "per-group",
58
+ });
59
+ expect(result.owner).toBe(account);
60
+ expect(result.uniqueness?.uniqueness).toBe("per-group");
61
+ });
62
+ });
63
+
64
+ describe("parseGroupCreateOptions", () => {
65
+ beforeEach(() => {
66
+ vi.clearAllMocks();
67
+ });
68
+
69
+ it("should use active account when no options provided", () => {
70
+ const result = parseGroupCreateOptions(undefined);
71
+ expect(result.owner).toBe(Account.getMe());
72
+ });
73
+
74
+ it("should use provided account when passing an Account", async () => {
75
+ const account = await createJazzTestAccount();
76
+ const result = parseGroupCreateOptions(account);
77
+ expect(result.owner).toBe(account);
78
+ });
79
+
80
+ it("should use active account when passing empty options", () => {
81
+ const result = parseGroupCreateOptions({});
82
+ expect(result.owner).toBe(Account.getMe());
83
+ });
84
+
85
+ it("should use provided account in options.owner", async () => {
86
+ const account = await createJazzTestAccount();
87
+ const result = parseGroupCreateOptions({ owner: account });
88
+ expect(result.owner).toBe(account);
89
+ });
90
+ });