applesauce-accounts 0.0.0-next-20250125174815 → 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,15 +1,15 @@
1
1
  import { Nip07Interface } from "applesauce-signer";
2
- import { EventTemplate, IAccount, SerializedAccount } from "./types.js";
3
2
  import { BehaviorSubject } from "rxjs";
4
3
  import { NostrEvent } from "nostr-tools";
4
+ import { EventTemplate, IAccount, SerializedAccount } from "./types.js";
5
5
  export declare class SignerMismatchError extends Error {
6
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
- /** Use a queue for sign and encryption/decryption requests so that there is only one request at a time */
12
- queueRequests: boolean;
11
+ /** Disable request queueing */
12
+ disableQueue?: boolean;
13
13
  metadata$: BehaviorSubject<Metadata | undefined>;
14
14
  get metadata(): Metadata | undefined;
15
15
  set metadata(metadata: Metadata);
@@ -27,10 +27,12 @@ export declare class BaseAccount<Signer extends Nip07Interface, SignerData, Meta
27
27
  getPublicKey(): string | Promise<string>;
28
28
  /** sign the event and make sure its signed with the correct pubkey */
29
29
  signEvent(template: EventTemplate): Promise<NostrEvent> | NostrEvent;
30
- /** Resets the request queue */
31
- resetQueue(): void;
30
+ /** Aborts all pending requests in the queue */
31
+ abortQueue(reason: Error): void;
32
32
  /** internal queue */
33
33
  protected queueLength: number;
34
34
  protected lock: Promise<any> | null;
35
+ protected abort: AbortController | null;
36
+ protected reduceQueue(): void;
35
37
  protected waitForLock<T>(fn: () => Promise<T> | T): Promise<T> | T;
36
38
  }
package/dist/account.js CHANGED
@@ -1,13 +1,34 @@
1
1
  import { nanoid } from "nanoid";
2
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
+ });
23
+ }
3
24
  export class SignerMismatchError extends Error {
4
25
  }
5
26
  export class BaseAccount {
6
27
  pubkey;
7
28
  signer;
8
29
  id = nanoid(8);
9
- /** Use a queue for sign and encryption/decryption requests so that there is only one request at a time */
10
- queueRequests = true;
30
+ /** Disable request queueing */
31
+ disableQueue;
11
32
  metadata$ = new BehaviorSubject(undefined);
12
33
  get metadata() {
13
34
  return this.metadata$.value;
@@ -81,31 +102,37 @@ export class BaseAccount {
81
102
  }
82
103
  });
83
104
  }
84
- /** Resets the request queue */
85
- resetQueue() {
86
- this.lock = null;
87
- this.queueLength = 0;
105
+ /** Aborts all pending requests in the queue */
106
+ abortQueue(reason) {
107
+ if (this.abort)
108
+ this.abort.abort(reason);
88
109
  }
89
110
  /** internal queue */
90
111
  queueLength = 0;
91
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
+ }
92
123
  waitForLock(fn) {
93
- if (!this.queueRequests)
124
+ if (this.disableQueue)
94
125
  return fn();
95
126
  // if there is already a pending request, wait for it
96
- if (this.lock) {
127
+ if (this.lock && this.abort) {
97
128
  // create a new promise that runs after the lock
98
- const p = this.lock
99
- .then(() => fn())
100
- .finally(() => {
101
- // shorten the queue
102
- this.queueLength--;
103
- // if this was the last request, remove the lock
104
- if (this.queueLength === 0)
105
- this.lock = null;
106
- });
107
- // set the lock the new promise
108
- this.lock = p;
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));
109
136
  this.queueLength++;
110
137
  return p;
111
138
  }
@@ -113,7 +140,10 @@ export class BaseAccount {
113
140
  const result = fn();
114
141
  // if the result is async, set the new lock
115
142
  if (result instanceof Promise) {
116
- this.lock = result;
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));
117
147
  this.queueLength = 1;
118
148
  }
119
149
  return result;
@@ -17,8 +17,8 @@ describe("BaseAccount", () => {
17
17
  });
18
18
  });
19
19
  // make two signing requests
20
- account.signEvent({ kind: 1, content: "first", created_at: 0, tags: [] });
21
- account.signEvent({ kind: 1, content: "second", created_at: 0, tags: [] });
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
22
  expect(signer.signEvent).toHaveBeenCalledOnce();
23
23
  expect(signer.signEvent).toHaveBeenCalledWith(expect.objectContaining({ content: "first" }));
24
24
  // resolve first
@@ -29,10 +29,15 @@ describe("BaseAccount", () => {
29
29
  expect(signer.signEvent).toHaveBeenCalledWith(expect.objectContaining({ content: "second" }));
30
30
  // resolve second
31
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();
32
36
  });
37
+ it("should cancel queue if request throws", () => { });
33
38
  it("should not use queueing if its disabled", async () => {
34
39
  const account = new BaseAccount(await signer.getPublicKey(), signer);
35
- account.queueRequests = false;
40
+ account.disableQueue = false;
36
41
  let resolve = [];
37
42
  vi.spyOn(signer, "signEvent").mockImplementation(() => {
38
43
  return new Promise((res) => {
@@ -42,14 +47,12 @@ describe("BaseAccount", () => {
42
47
  // make two signing requests
43
48
  account.signEvent({ kind: 1, content: "first", created_at: 0, tags: [] });
44
49
  account.signEvent({ kind: 1, content: "second", created_at: 0, tags: [] });
50
+ expect(Reflect.get(account, "lock")).toBeNull();
45
51
  expect(signer.signEvent).toHaveBeenCalledTimes(2);
46
52
  expect(signer.signEvent).toHaveBeenCalledWith(expect.objectContaining({ content: "first" }));
47
53
  expect(signer.signEvent).toHaveBeenCalledWith(expect.objectContaining({ content: "second" }));
48
- // resolve first
54
+ // resolve both
49
55
  resolve.shift()?.();
50
- // wait next tick
51
- await new Promise((res) => setTimeout(res, 0));
52
- // resolve second
53
56
  resolve.shift()?.();
54
57
  });
55
58
  });
package/dist/manager.d.ts CHANGED
@@ -7,6 +7,8 @@ export declare class AccountManager<Metadata extends unknown = any> {
7
7
  get active(): IAccount<any, any, Metadata> | null;
8
8
  accounts$: BehaviorSubject<IAccount<any, any, Metadata>[]>;
9
9
  get accounts(): IAccount<any, any, Metadata>[];
10
+ /** Disable request queueing for any accounts added to this manager */
11
+ disableQueue?: boolean;
10
12
  /** Add account type class */
11
13
  registerType<S extends Nip07Interface>(accountType: IAccountConstructor<S, any, Metadata>): void;
12
14
  /** Remove account type */
package/dist/manager.js CHANGED
@@ -9,6 +9,8 @@ export class AccountManager {
9
9
  get accounts() {
10
10
  return this.accounts$.value;
11
11
  }
12
+ /** Disable request queueing for any accounts added to this manager */
13
+ disableQueue;
12
14
  // Account type CRUD
13
15
  /** Add account type class */
14
16
  registerType(accountType) {
@@ -44,6 +46,10 @@ export class AccountManager {
44
46
  addAccount(account) {
45
47
  if (this.getAccount(account.id))
46
48
  return;
49
+ // copy the disableQueue flag only if its set
50
+ if (this.disableQueue !== undefined && account.disableQueue !== undefined) {
51
+ account.disableQueue = this.disableQueue;
52
+ }
47
53
  this.accounts$.next({
48
54
  ...this.accounts$.value,
49
55
  [account.id]: account,
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-20250125174815",
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-20250125174815",
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"