Files
boris/node_modules/applesauce-accounts/dist/account.js
Gigi 5d53a827e0 feat: initialize markr nostr bookmark client
- Add project structure with TypeScript, React, and Vite
- Implement nostr authentication using browser extension (NIP-07)
- Add NIP-51 compliant bookmark fetching and display
- Create minimal UI with login and bookmark components
- Integrate applesauce-core and applesauce-react libraries
- Add responsive styling with dark/light mode support
- Include comprehensive README with setup instructions

This is a minimal MVP for a nostr bookmark client that allows users to
view their bookmarks according to NIP-51 specification.
2025-10-02 07:17:07 +02:00

160 lines
5.6 KiB
JavaScript

import { nanoid } from "nanoid";
import { BehaviorSubject } from "rxjs";
import { getEventHash } from "nostr-tools";
/** Wraps a promise in an abort signal */
function wrapInSignal(promise, signal) {
return new Promise((res, rej) => {
signal.throwIfAborted();
let done = false;
// reject promise if abort signal is triggered
signal.addEventListener("abort", () => {
if (!done)
rej(signal.reason || undefined);
done = true;
});
return promise.then((v) => {
if (!done)
res(v);
done = true;
}, (err) => {
if (!done)
rej(err);
done = true;
});
});
}
/** An error thrown when a signer is used with the wrong pubkey */
export class SignerMismatchError extends Error {
}
/** A base class for all accounts */
export class BaseAccount {
pubkey;
signer;
id = nanoid(8);
get type() {
const cls = Reflect.getPrototypeOf(this).constructor;
return cls.type;
}
/** Disable request queueing */
disableQueue;
metadata$ = new BehaviorSubject(undefined);
get metadata() {
return this.metadata$.value;
}
set metadata(metadata) {
this.metadata$.next(metadata);
}
get nip04() {
if (!this.signer.nip04)
return undefined;
return {
encrypt: async (pubkey, plaintext) => this.operation(() => this.signer.nip04.encrypt(pubkey, plaintext)),
decrypt: async (pubkey, plaintext) => this.operation(() => this.signer.nip04.decrypt(pubkey, plaintext)),
};
}
get nip44() {
if (!this.signer.nip44)
return undefined;
return {
encrypt: async (pubkey, plaintext) => this.operation(() => this.signer.nip44.encrypt(pubkey, plaintext)),
decrypt: async (pubkey, plaintext) => this.operation(() => this.signer.nip44.decrypt(pubkey, plaintext)),
};
}
constructor(pubkey, signer) {
this.pubkey = pubkey;
this.signer = signer;
}
// This should be overwritten by a sub class
toJSON() {
throw new Error("Not implemented");
}
/** A method that wraps any signer interaction, this allows the account to wait for unlock or queue requests */
operation(operation) {
// If the queue is enabled, wait our turn in the queue
return this.waitForQueue(operation);
}
/** Adds the common fields to the serialized output of a toJSON method */
saveCommonFields(json) {
return { ...json, id: this.id, pubkey: this.pubkey, metadata: this.metadata, type: this.type };
}
/** Sets an accounts id and metadata. NOTE: This should only be used in fromJSON methods */
static loadCommonFields(account, json) {
if (json.id)
account.id = json.id;
if (json.metadata)
account.metadata = json.metadata;
return account;
}
/** Gets the pubkey from the signer */
getPublicKey() {
return this.operation(async () => {
const result = await this.signer.getPublicKey();
if (this.pubkey !== result)
throw new SignerMismatchError("Account signer mismatch");
return result;
});
}
/** sign the event and make sure its signed with the correct pubkey */
signEvent(template) {
// If the template does not have a pubkey, set it to the accounts pubkey
if (!Reflect.has(template, "pubkey"))
Reflect.set(template, "pubkey", this.pubkey);
return this.operation(async () => {
const id = getEventHash(template);
const result = await this.signer.signEvent(template);
if (result.pubkey !== this.pubkey)
throw new SignerMismatchError("Signer signed with wrong pubkey");
if (result.id !== id)
throw new SignerMismatchError("Signer modified event");
return result;
});
}
/** Aborts all pending requests in the queue */
abortQueue(reason) {
if (this.abort)
this.abort.abort(reason);
}
/** internal queue */
queueLength = 0;
lock = null;
abort = null;
reduceQueue() {
// shorten the queue
this.queueLength--;
// if this was the last request, remove the lock
if (this.queueLength === 0) {
this.lock = null;
this.abort = null;
}
}
waitForQueue(operation) {
if (this.disableQueue)
return operation();
// if there is already a pending request, wait for it
if (this.lock && this.abort) {
// create a new promise that runs after the lock
const p = wrapInSignal(this.lock.then(() => {
// if the abort signal is triggered, don't call the signer
this.abort?.signal.throwIfAborted();
return operation();
}), this.abort.signal);
// set the lock the new promise that ignores errors
this.lock = p.catch(() => { }).finally(this.reduceQueue.bind(this));
this.queueLength++;
return p;
}
else {
const result = operation();
// if the result is async, set the new lock
if (result instanceof Promise) {
this.abort = new AbortController();
const p = wrapInSignal(result, this.abort.signal);
// set the lock the new promise that ignores errors
this.lock = p.catch(() => { }).finally(this.reduceQueue.bind(this));
this.queueLength = 1;
}
return result;
}
}
}