applesauce-accounts 0.0.0-next-20250124230519 → 0.0.0-next-20250125183855

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/dist/account.d.ts CHANGED
@@ -1,14 +1,18 @@
1
1
  import { Nip07Interface } from "applesauce-signer";
2
+ import { BehaviorSubject } from "rxjs";
3
+ import { NostrEvent } from "nostr-tools";
2
4
  import { EventTemplate, IAccount, SerializedAccount } from "./types.js";
3
5
  export declare class SignerMismatchError extends Error {
4
6
  }
5
- export declare class AccountLockedError extends Error {
6
- }
7
7
  export declare class BaseAccount<Signer extends Nip07Interface, SignerData, Metadata extends unknown> implements IAccount<Signer, SignerData, Metadata> {
8
8
  pubkey: string;
9
9
  signer: Signer;
10
10
  id: string;
11
- metadata?: Metadata;
11
+ /** Disable request queueing */
12
+ disableQueue?: boolean;
13
+ metadata$: BehaviorSubject<Metadata | undefined>;
14
+ get metadata(): Metadata | undefined;
15
+ set metadata(metadata: Metadata);
12
16
  nip04?: {
13
17
  encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
14
18
  decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
@@ -20,7 +24,15 @@ export declare class BaseAccount<Signer extends Nip07Interface, SignerData, Meta
20
24
  constructor(pubkey: string, signer: Signer);
21
25
  toJSON(): SerializedAccount<SignerData, Metadata>;
22
26
  /** Gets the pubkey from the signer */
23
- getPublicKey(): Promise<string>;
27
+ getPublicKey(): string | Promise<string>;
24
28
  /** sign the event and make sure its signed with the correct pubkey */
25
- signEvent(template: EventTemplate): Promise<import("nostr-tools").Event>;
29
+ signEvent(template: EventTemplate): Promise<NostrEvent> | NostrEvent;
30
+ /** Aborts all pending requests in the queue */
31
+ abortQueue(reason: Error): void;
32
+ /** internal queue */
33
+ protected queueLength: number;
34
+ protected lock: Promise<any> | null;
35
+ protected abort: AbortController | null;
36
+ protected reduceQueue(): void;
37
+ protected waitForLock<T>(fn: () => Promise<T> | T): Promise<T> | T;
26
38
  }
package/dist/account.js CHANGED
@@ -1,14 +1,41 @@
1
1
  import { nanoid } from "nanoid";
2
- // errors
3
- export class SignerMismatchError extends Error {
2
+ import { BehaviorSubject } from "rxjs";
3
+ function wrapInSignal(promise, signal) {
4
+ return new Promise((res, rej) => {
5
+ signal.throwIfAborted();
6
+ let done = false;
7
+ // reject promise if abort signal is triggered
8
+ signal.addEventListener("abort", () => {
9
+ if (!done)
10
+ rej(signal.reason || undefined);
11
+ done = true;
12
+ });
13
+ return promise.then((v) => {
14
+ if (!done)
15
+ res(v);
16
+ done = true;
17
+ }, (err) => {
18
+ if (!done)
19
+ rej(err);
20
+ done = true;
21
+ });
22
+ });
4
23
  }
5
- export class AccountLockedError extends Error {
24
+ export class SignerMismatchError extends Error {
6
25
  }
7
26
  export class BaseAccount {
8
27
  pubkey;
9
28
  signer;
10
29
  id = nanoid(8);
11
- metadata;
30
+ /** Disable request queueing */
31
+ disableQueue;
32
+ metadata$ = new BehaviorSubject(undefined);
33
+ get metadata() {
34
+ return this.metadata$.value;
35
+ }
36
+ set metadata(metadata) {
37
+ this.metadata$.next(metadata);
38
+ }
12
39
  // encryption interfaces
13
40
  nip04;
14
41
  nip44;
@@ -19,20 +46,20 @@ export class BaseAccount {
19
46
  if (this.signer.nip04) {
20
47
  this.nip04 = {
21
48
  encrypt: (pubkey, plaintext) => {
22
- return this.signer.nip04.encrypt(pubkey, plaintext);
49
+ return this.waitForLock(() => this.signer.nip04.encrypt(pubkey, plaintext));
23
50
  },
24
51
  decrypt: (pubkey, plaintext) => {
25
- return this.signer.nip04.decrypt(pubkey, plaintext);
52
+ return this.waitForLock(() => this.signer.nip04.decrypt(pubkey, plaintext));
26
53
  },
27
54
  };
28
55
  }
29
56
  if (this.signer.nip44) {
30
57
  this.nip44 = {
31
58
  encrypt: (pubkey, plaintext) => {
32
- return this.signer.nip44.encrypt(pubkey, plaintext);
59
+ return this.waitForLock(() => this.signer.nip44.encrypt(pubkey, plaintext));
33
60
  },
34
61
  decrypt: (pubkey, plaintext) => {
35
- return this.signer.nip44.decrypt(pubkey, plaintext);
62
+ return this.waitForLock(() => this.signer.nip44.decrypt(pubkey, plaintext));
36
63
  },
37
64
  };
38
65
  }
@@ -42,21 +69,84 @@ export class BaseAccount {
42
69
  throw new Error("Not implemented");
43
70
  }
44
71
  /** Gets the pubkey from the signer */
45
- async getPublicKey() {
46
- // this.checkLocked();
47
- const signerKey = await this.signer.getPublicKey();
48
- if (this.pubkey !== signerKey)
49
- throw new Error("Account signer mismatch");
50
- return this.pubkey;
72
+ getPublicKey() {
73
+ const result = this.signer.getPublicKey();
74
+ if (result instanceof Promise)
75
+ return result.then((pubkey) => {
76
+ if (this.pubkey !== pubkey)
77
+ throw new SignerMismatchError("Account signer mismatch");
78
+ return pubkey;
79
+ });
80
+ else {
81
+ if (this.pubkey !== result)
82
+ throw new SignerMismatchError("Account signer mismatch");
83
+ return result;
84
+ }
51
85
  }
52
86
  /** sign the event and make sure its signed with the correct pubkey */
53
- async signEvent(template) {
54
- // this.checkLocked();
87
+ signEvent(template) {
55
88
  if (!Reflect.has(template, "pubkey"))
56
89
  Reflect.set(template, "pubkey", this.pubkey);
57
- const signed = await this.signer.signEvent(template);
58
- if (signed.pubkey !== this.pubkey)
59
- throw new SignerMismatchError("Signer signed with wrong pubkey");
60
- return signed;
90
+ return this.waitForLock(() => {
91
+ const result = this.signer.signEvent(template);
92
+ if (result instanceof Promise)
93
+ return result.then((signed) => {
94
+ if (signed.pubkey !== this.pubkey)
95
+ throw new SignerMismatchError("Signer signed with wrong pubkey");
96
+ return signed;
97
+ });
98
+ else {
99
+ if (result.pubkey !== this.pubkey)
100
+ throw new SignerMismatchError("Signer signed with wrong pubkey");
101
+ return result;
102
+ }
103
+ });
104
+ }
105
+ /** Aborts all pending requests in the queue */
106
+ abortQueue(reason) {
107
+ if (this.abort)
108
+ this.abort.abort(reason);
109
+ }
110
+ /** internal queue */
111
+ queueLength = 0;
112
+ lock = null;
113
+ abort = null;
114
+ reduceQueue() {
115
+ // shorten the queue
116
+ this.queueLength--;
117
+ // if this was the last request, remove the lock
118
+ if (this.queueLength === 0) {
119
+ this.lock = null;
120
+ this.abort = null;
121
+ }
122
+ }
123
+ waitForLock(fn) {
124
+ if (this.disableQueue)
125
+ return fn();
126
+ // if there is already a pending request, wait for it
127
+ if (this.lock && this.abort) {
128
+ // create a new promise that runs after the lock
129
+ const p = wrapInSignal(this.lock.then(() => {
130
+ // if the abort signal is triggered, don't call the signer
131
+ this.abort?.signal.throwIfAborted();
132
+ return fn();
133
+ }), this.abort.signal);
134
+ // set the lock the new promise that ignores errors
135
+ this.lock = p.catch(() => { }).finally(this.reduceQueue.bind(this));
136
+ this.queueLength++;
137
+ return p;
138
+ }
139
+ else {
140
+ const result = fn();
141
+ // if the result is async, set the new lock
142
+ if (result instanceof Promise) {
143
+ this.abort = new AbortController();
144
+ const p = wrapInSignal(result, this.abort.signal);
145
+ // set the lock the new promise that ignores errors
146
+ this.lock = p.catch(() => { }).finally(this.reduceQueue.bind(this));
147
+ this.queueLength = 1;
148
+ }
149
+ return result;
150
+ }
61
151
  }
62
152
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import { BaseAccount } from "./account.js";
3
+ import { SimpleSigner } from "applesauce-signer";
4
+ import { finalizeEvent } from "nostr-tools";
5
+ describe("BaseAccount", () => {
6
+ let signer;
7
+ beforeEach(() => {
8
+ signer = new SimpleSigner();
9
+ });
10
+ describe("request queue", () => {
11
+ it("should queue signing requests by default", async () => {
12
+ const account = new BaseAccount(await signer.getPublicKey(), signer);
13
+ let resolve = [];
14
+ vi.spyOn(signer, "signEvent").mockImplementation(() => {
15
+ return new Promise((res) => {
16
+ resolve.push(() => res(finalizeEvent({ kind: 1, content: "mock", created_at: 0, tags: [] }, signer.key)));
17
+ });
18
+ });
19
+ // make two signing requests
20
+ expect(account.signEvent({ kind: 1, content: "first", created_at: 0, tags: [] })).toEqual(expect.any(Promise));
21
+ expect(account.signEvent({ kind: 1, content: "second", created_at: 0, tags: [] })).toEqual(expect.any(Promise));
22
+ expect(signer.signEvent).toHaveBeenCalledOnce();
23
+ expect(signer.signEvent).toHaveBeenCalledWith(expect.objectContaining({ content: "first" }));
24
+ // resolve first
25
+ resolve.shift()?.();
26
+ // wait next tick
27
+ await new Promise((res) => setTimeout(res, 0));
28
+ expect(signer.signEvent).toHaveBeenCalledTimes(2);
29
+ expect(signer.signEvent).toHaveBeenCalledWith(expect.objectContaining({ content: "second" }));
30
+ // resolve second
31
+ resolve.shift()?.();
32
+ // wait next tick
33
+ await new Promise((res) => setTimeout(res, 0));
34
+ expect(Reflect.get(account, "queueLength")).toBe(0);
35
+ expect(Reflect.get(account, "lock")).toBeNull();
36
+ });
37
+ it("should cancel queue if request throws", () => { });
38
+ it("should not use queueing if its disabled", async () => {
39
+ const account = new BaseAccount(await signer.getPublicKey(), signer);
40
+ account.disableQueue = false;
41
+ let resolve = [];
42
+ vi.spyOn(signer, "signEvent").mockImplementation(() => {
43
+ return new Promise((res) => {
44
+ resolve.push(() => res(finalizeEvent({ kind: 1, content: "mock", created_at: 0, tags: [] }, signer.key)));
45
+ });
46
+ });
47
+ // make two signing requests
48
+ account.signEvent({ kind: 1, content: "first", created_at: 0, tags: [] });
49
+ account.signEvent({ kind: 1, content: "second", created_at: 0, tags: [] });
50
+ expect(Reflect.get(account, "lock")).toBeNull();
51
+ expect(signer.signEvent).toHaveBeenCalledTimes(2);
52
+ expect(signer.signEvent).toHaveBeenCalledWith(expect.objectContaining({ content: "first" }));
53
+ expect(signer.signEvent).toHaveBeenCalledWith(expect.objectContaining({ content: "second" }));
54
+ // resolve both
55
+ resolve.shift()?.();
56
+ resolve.shift()?.();
57
+ });
58
+ });
59
+ });
package/dist/manager.d.ts CHANGED
@@ -2,9 +2,13 @@ import { Nip07Interface } from "applesauce-signer";
2
2
  import { BehaviorSubject } from "rxjs";
3
3
  import { IAccount, IAccountConstructor, SerializedAccount } from "./types.js";
4
4
  export declare class AccountManager<Metadata extends unknown = any> {
5
- active: BehaviorSubject<IAccount<any, any, Metadata> | null>;
6
- accounts: BehaviorSubject<Record<string, IAccount<any, any, Metadata>>>;
7
5
  types: Map<string, IAccountConstructor<any, any, Metadata>>;
6
+ active$: BehaviorSubject<IAccount<any, any, Metadata> | null>;
7
+ get active(): IAccount<any, any, Metadata> | null;
8
+ accounts$: BehaviorSubject<IAccount<any, any, Metadata>[]>;
9
+ get accounts(): IAccount<any, any, Metadata>[];
10
+ /** Disable request queueing for any accounts added to this manager */
11
+ disableQueue?: boolean;
8
12
  /** Add account type class */
9
13
  registerType<S extends Nip07Interface>(accountType: IAccountConstructor<S, any, Metadata>): void;
10
14
  /** Remove account type */
package/dist/manager.js CHANGED
@@ -1,8 +1,16 @@
1
1
  import { BehaviorSubject } from "rxjs";
2
2
  export class AccountManager {
3
- active = new BehaviorSubject(null);
4
- accounts = new BehaviorSubject({});
5
3
  types = new Map();
4
+ active$ = new BehaviorSubject(null);
5
+ get active() {
6
+ return this.active$.value;
7
+ }
8
+ accounts$ = new BehaviorSubject([]);
9
+ get accounts() {
10
+ return this.accounts$.value;
11
+ }
12
+ /** Disable request queueing for any accounts added to this manager */
13
+ disableQueue;
6
14
  // Account type CRUD
7
15
  /** Add account type class */
8
16
  registerType(accountType) {
@@ -20,62 +28,64 @@ export class AccountManager {
20
28
  /** gets an account in the manager */
21
29
  getAccount(id) {
22
30
  if (typeof id === "string")
23
- return this.accounts.value[id];
24
- else if (this.accounts.value[id.id])
31
+ return this.accounts$.value.find((a) => a.id === id);
32
+ else if (this.accounts$.value.includes(id))
25
33
  return id;
26
34
  else
27
35
  return undefined;
28
36
  }
29
37
  /** Return the first account for a pubkey */
30
38
  getAccountForPubkey(pubkey) {
31
- return Object.values(this.accounts.value).find((account) => account.pubkey === pubkey);
39
+ return Object.values(this.accounts$.value).find((account) => account.pubkey === pubkey);
32
40
  }
33
41
  /** Returns all accounts for a pubkey */
34
42
  getAccountsForPubkey(pubkey) {
35
- return Object.values(this.accounts.value).filter((account) => account.pubkey === pubkey);
43
+ return Object.values(this.accounts$.value).filter((account) => account.pubkey === pubkey);
36
44
  }
37
45
  /** adds an account to the manager */
38
46
  addAccount(account) {
39
47
  if (this.getAccount(account.id))
40
48
  return;
41
- this.accounts.next({
42
- ...this.accounts.value,
49
+ // copy the disableQueue flag only if its set
50
+ if (this.disableQueue !== undefined && account.disableQueue !== undefined) {
51
+ account.disableQueue = this.disableQueue;
52
+ }
53
+ this.accounts$.next({
54
+ ...this.accounts$.value,
43
55
  [account.id]: account,
44
56
  });
45
57
  }
46
58
  /** Removes an account from the manager */
47
59
  removeAccount(account) {
48
60
  const id = typeof account === "string" ? account : account.id;
49
- const next = { ...this.accounts.value };
50
- delete next[id];
51
- this.accounts.next(next);
61
+ this.accounts$.next(this.accounts$.value.filter((a) => a.id !== id));
52
62
  }
53
63
  /** Replaces an account with another */
54
64
  replaceAccount(old, account) {
55
65
  this.addAccount(account);
56
66
  // if the old account was active, switch to the new one
57
67
  const id = typeof account === "string" ? account : account.id;
58
- if (this.active.value?.id === id)
68
+ if (this.active$.value?.id === id)
59
69
  this.setActive(account);
60
70
  this.removeAccount(old);
61
71
  }
62
72
  // Active account methods
63
73
  /** Returns the currently active account */
64
74
  getActive() {
65
- return this.active.value;
75
+ return this.active$.value;
66
76
  }
67
77
  /** Sets the currently active account */
68
78
  setActive(id) {
69
79
  const account = this.getAccount(id);
70
80
  if (!account)
71
81
  throw new Error("Cant find account with that ID");
72
- if (this.active.value?.id !== account.id) {
73
- this.active.next(account);
82
+ if (this.active$.value?.id !== account.id) {
83
+ this.active$.next(account);
74
84
  }
75
85
  }
76
86
  /** Clears the currently active account */
77
87
  clearActive() {
78
- this.active.next(null);
88
+ this.active$.next(null);
79
89
  }
80
90
  // Metadata CRUD
81
91
  /** sets the metadata on an account */
@@ -95,7 +105,7 @@ export class AccountManager {
95
105
  // Serialize / Deserialize
96
106
  /** Returns an array of serialized accounts */
97
107
  toJSON() {
98
- return Array.from(Object.values(this.accounts)).map((account) => account.toJSON());
108
+ return Array.from(Object.values(this.accounts$)).map((account) => account.toJSON());
99
109
  }
100
110
  /**
101
111
  * Restores all accounts from an array of serialized accounts
package/dist/types.d.ts CHANGED
@@ -25,6 +25,7 @@ export interface IAccount<Signer extends Nip07Interface, SignerData, Metadata ex
25
25
  pubkey: string;
26
26
  metadata?: Metadata;
27
27
  signer: Signer;
28
+ disableQueue?: boolean;
28
29
  toJSON(): SerializedAccount<SignerData, Metadata>;
29
30
  }
30
31
  export interface IAccountConstructor<Signer extends Nip07Interface, SignerData, Metadata extends unknown> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-accounts",
3
- "version": "0.0.0-next-20250124230519",
3
+ "version": "0.0.0-next-20250125183855",
4
4
  "description": "A simple nostr account management system",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@noble/hashes": "^1.5.0",
35
- "applesauce-signer": "0.0.0-next-20250124230519",
35
+ "applesauce-signer": "0.0.0-next-20250125183855",
36
36
  "nanoid": "^5.0.9",
37
37
  "nostr-tools": "^2.10.3",
38
38
  "rxjs": "^7.8.1"