applesauce-core 0.0.0-next-20250414124006 → 0.0.0-next-20250423151245
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/helpers/blossom.test.d.ts +1 -0
- package/dist/helpers/blossom.test.js +13 -0
- package/dist/helpers/bookmark.d.ts +15 -0
- package/dist/helpers/bookmark.js +27 -0
- package/dist/helpers/event.test.d.ts +1 -0
- package/dist/helpers/event.test.js +36 -0
- package/dist/helpers/media-attachment.d.ts +42 -0
- package/dist/helpers/media-attachment.js +72 -0
- package/dist/helpers/media-attachment.test.d.ts +1 -0
- package/dist/helpers/media-attachment.test.js +59 -0
- package/dist/helpers/media-post.d.ts +4 -0
- package/dist/helpers/media-post.js +6 -0
- package/dist/helpers/mute.d.ts +14 -0
- package/dist/helpers/mute.js +23 -0
- package/dist/helpers/pipe.d.ts +10 -0
- package/dist/helpers/pipe.js +3 -0
- package/dist/helpers/tags.test.js +20 -12
- package/dist/observable/{getValue.d.ts → get-value.d.ts} +1 -0
- package/dist/observable/{getValue.js → get-value.js} +1 -0
- package/dist/observable/simple-timeout.test.d.ts +1 -0
- package/dist/observable/simple-timeout.test.js +34 -0
- package/dist/queries/comment.d.ts +4 -0
- package/dist/queries/comment.js +14 -0
- package/package.json +1 -1
- package/dist/observable/share-behavior.d.ts +0 -2
- package/dist/observable/share-behavior.js +0 -7
- package/dist/observable/stateful.d.ts +0 -10
- package/dist/observable/stateful.js +0 -60
- package/dist/observable/throttle.d.ts +0 -3
- package/dist/observable/throttle.js +0 -23
- package/dist/utils/lru.d.ts +0 -32
- package/dist/utils/lru.js +0 -148
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { areBlossomServersEqual } from "./blossom.js";
|
|
3
|
+
describe("areBlossomServersEqual", () => {
|
|
4
|
+
it("should ignore path", () => {
|
|
5
|
+
expect(areBlossomServersEqual("https://cdn.server.com/pathname", "https://cdn.server.com")).toBe(true);
|
|
6
|
+
});
|
|
7
|
+
it("should not ignore protocol", () => {
|
|
8
|
+
expect(areBlossomServersEqual("http://cdn.server.com", "https://cdn.server.com")).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
it("should not ignore port", () => {
|
|
11
|
+
expect(areBlossomServersEqual("http://cdn.server.com:4658", "https://cdn.server.com")).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NostrEvent } from "nostr-tools";
|
|
2
|
+
import { AddressPointer, EventPointer } from "nostr-tools/nip19";
|
|
3
|
+
export declare const BookmarkPublicSymbol: unique symbol;
|
|
4
|
+
export declare const BookmarkHiddenSymbol: unique symbol;
|
|
5
|
+
export type Bookmarks = {
|
|
6
|
+
notes: EventPointer[];
|
|
7
|
+
articles: AddressPointer[];
|
|
8
|
+
hashtags: string[];
|
|
9
|
+
urls: string[];
|
|
10
|
+
};
|
|
11
|
+
export declare function parseBookmarkTags(tags: string[][]): Bookmarks;
|
|
12
|
+
/** Returns the public bookmarks of the event */
|
|
13
|
+
export declare function getBookmarks(bookmark: NostrEvent): Bookmarks;
|
|
14
|
+
/** Returns the bookmarks of the event if its unlocked */
|
|
15
|
+
export declare function getHiddenBookmarks(bookmark: NostrEvent): Bookmarks | undefined;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { kinds } from "nostr-tools";
|
|
2
|
+
import { getAddressPointerFromATag, getEventPointerFromETag } from "./pointers.js";
|
|
3
|
+
import { getOrComputeCachedValue } from "./cache.js";
|
|
4
|
+
import { getHiddenTags } from "./index.js";
|
|
5
|
+
export const BookmarkPublicSymbol = Symbol.for("bookmark-public");
|
|
6
|
+
export const BookmarkHiddenSymbol = Symbol.for("bookmark-hidden");
|
|
7
|
+
export function parseBookmarkTags(tags) {
|
|
8
|
+
const notes = tags.filter((t) => t[0] === "e" && t[1]).map(getEventPointerFromETag);
|
|
9
|
+
const articles = tags
|
|
10
|
+
.filter((t) => t[0] === "a" && t[1])
|
|
11
|
+
.map(getAddressPointerFromATag)
|
|
12
|
+
.filter((addr) => addr.kind === kinds.LongFormArticle);
|
|
13
|
+
const hashtags = tags.filter((t) => t[0] === "t" && t[1]).map((t) => t[1]);
|
|
14
|
+
const urls = tags.filter((t) => t[0] === "r" && t[1]).map((t) => t[1]);
|
|
15
|
+
return { notes, articles, hashtags, urls };
|
|
16
|
+
}
|
|
17
|
+
/** Returns the public bookmarks of the event */
|
|
18
|
+
export function getBookmarks(bookmark) {
|
|
19
|
+
return getOrComputeCachedValue(bookmark, BookmarkPublicSymbol, () => parseBookmarkTags(bookmark.tags));
|
|
20
|
+
}
|
|
21
|
+
/** Returns the bookmarks of the event if its unlocked */
|
|
22
|
+
export function getHiddenBookmarks(bookmark) {
|
|
23
|
+
return getOrComputeCachedValue(bookmark, BookmarkHiddenSymbol, () => {
|
|
24
|
+
const tags = getHiddenTags(bookmark);
|
|
25
|
+
return tags && parseBookmarkTags(tags);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { EventIndexableTagsSymbol, getIndexableTags, getTagValue } from "./event.js";
|
|
3
|
+
const event = {
|
|
4
|
+
content: "",
|
|
5
|
+
created_at: 1732889913,
|
|
6
|
+
id: "2d53511f321cc82dd13eedfb597c9fe834d12d271c10d8068e9d8cfb8f58d1b4",
|
|
7
|
+
kind: 30000,
|
|
8
|
+
pubkey: "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5",
|
|
9
|
+
sig: "e6a442487ef44a8a00ec1e0a852e547991fcd5cbf19aa1a4219fa65d6f41022675e0745207649f4b16fe9a6c5c7c3693dc3e13966ffa5b2891634867c874cf22",
|
|
10
|
+
tags: [
|
|
11
|
+
["d", "qRxLhBbTfRlxsvKSu0iUl"],
|
|
12
|
+
["title", "Musicians"],
|
|
13
|
+
["client", "noStrudel", "31990:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:1686066542546"],
|
|
14
|
+
["p", "2842e34860c59dfacd5df48ba7a65065e6760d08c35f779553d83c2c2310b493"],
|
|
15
|
+
["p", "28ca019b78b494c25a9da2d645975a8501c7e99b11302e5cbe748ee593fcb2cc"],
|
|
16
|
+
["p", "f46192b8b9be1b43fc30ea27c7cb16210aede17252b3aa9692fbb3f2ba153199"],
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
describe("getIndexableTags", () => {
|
|
20
|
+
it("should return a set of indexable tags for event", () => {
|
|
21
|
+
expect(Array.from(getIndexableTags(event))).toEqual(expect.arrayContaining([
|
|
22
|
+
"p:2842e34860c59dfacd5df48ba7a65065e6760d08c35f779553d83c2c2310b493",
|
|
23
|
+
"p:28ca019b78b494c25a9da2d645975a8501c7e99b11302e5cbe748ee593fcb2cc",
|
|
24
|
+
"p:f46192b8b9be1b43fc30ea27c7cb16210aede17252b3aa9692fbb3f2ba153199",
|
|
25
|
+
]));
|
|
26
|
+
});
|
|
27
|
+
it("should cache value on EventIndexableTagsSymbol", () => {
|
|
28
|
+
getIndexableTags(event);
|
|
29
|
+
expect(Reflect.has(event, EventIndexableTagsSymbol)).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe("getTagValue", () => {
|
|
33
|
+
it("should return value of tag if present", () => {
|
|
34
|
+
expect(getTagValue(event, "title")).toBe("Musicians");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { NostrEvent } from "nostr-tools";
|
|
2
|
+
export type MediaAttachment = {
|
|
3
|
+
/** URL of the file */
|
|
4
|
+
url: string;
|
|
5
|
+
/** MIME type */
|
|
6
|
+
type?: string;
|
|
7
|
+
/** sha256 hash of the file */
|
|
8
|
+
sha256?: string;
|
|
9
|
+
/**
|
|
10
|
+
* The original sha256 hash before the file was transformed
|
|
11
|
+
* @deprecated
|
|
12
|
+
*/
|
|
13
|
+
originalSha256?: string;
|
|
14
|
+
/** size of the file in bytes */
|
|
15
|
+
size?: number;
|
|
16
|
+
/** size of file in pixels in the form <width>x<height> */
|
|
17
|
+
dimensions?: string;
|
|
18
|
+
/** magnet */
|
|
19
|
+
magnet?: string;
|
|
20
|
+
/** torrent infohash */
|
|
21
|
+
infohash?: string;
|
|
22
|
+
/** URL to a thumbnail */
|
|
23
|
+
thumbnail?: string;
|
|
24
|
+
/** URL to a preview image with the same dimensions */
|
|
25
|
+
image?: string;
|
|
26
|
+
/** summary */
|
|
27
|
+
summary?: string;
|
|
28
|
+
/** description for accessability */
|
|
29
|
+
alt?: string;
|
|
30
|
+
/** blurhash */
|
|
31
|
+
blurhash?: string;
|
|
32
|
+
/** fallback URLs */
|
|
33
|
+
fallback?: string[];
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Parses a imeta tag into a {@link MediaAttachment}
|
|
37
|
+
* @throws
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseMediaAttachmentTag(tag: string[]): MediaAttachment;
|
|
40
|
+
export declare const MediaAttachmentsSymbol: unique symbol;
|
|
41
|
+
/** Gets all the media attachments on an event */
|
|
42
|
+
export declare function getMediaAttachments(event: NostrEvent): MediaAttachment[];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { getOrComputeCachedValue } from "./cache.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parses a imeta tag into a {@link MediaAttachment}
|
|
4
|
+
* @throws
|
|
5
|
+
*/
|
|
6
|
+
export function parseMediaAttachmentTag(tag) {
|
|
7
|
+
const parts = tag.slice(1);
|
|
8
|
+
const fields = {};
|
|
9
|
+
let fallback = undefined;
|
|
10
|
+
for (const part of parts) {
|
|
11
|
+
const match = part.match(/^(.+?)\s(.+)$/);
|
|
12
|
+
if (match) {
|
|
13
|
+
const [_, name, value] = match;
|
|
14
|
+
switch (name) {
|
|
15
|
+
case "fallback":
|
|
16
|
+
fallback = fallback ? [...fallback, value] : [value];
|
|
17
|
+
break;
|
|
18
|
+
default:
|
|
19
|
+
fields[name] = value;
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (!fields.url)
|
|
25
|
+
throw new Error("Missing required url in attachment");
|
|
26
|
+
const attachment = { url: fields.url, fallback };
|
|
27
|
+
// parse size
|
|
28
|
+
if (fields.size)
|
|
29
|
+
attachment.size = parseInt(fields.size);
|
|
30
|
+
// copy optional fields
|
|
31
|
+
if (fields.m)
|
|
32
|
+
attachment.type = fields.m;
|
|
33
|
+
if (fields.x)
|
|
34
|
+
attachment.sha256 = fields.x;
|
|
35
|
+
if (fields.ox)
|
|
36
|
+
attachment.originalSha256 = fields.ox;
|
|
37
|
+
if (fields.dim)
|
|
38
|
+
attachment.dimensions = fields.dim;
|
|
39
|
+
if (fields.magnet)
|
|
40
|
+
attachment.magnet = fields.magnet;
|
|
41
|
+
if (fields.i)
|
|
42
|
+
attachment.infohash = fields.i;
|
|
43
|
+
if (fields.thumb)
|
|
44
|
+
attachment.thumbnail = fields.thumb;
|
|
45
|
+
if (fields.image)
|
|
46
|
+
attachment.image = fields.image;
|
|
47
|
+
if (fields.summary)
|
|
48
|
+
attachment.summary = fields.summary;
|
|
49
|
+
if (fields.alt)
|
|
50
|
+
attachment.alt = fields.alt;
|
|
51
|
+
if (fields.blurhash)
|
|
52
|
+
attachment.blurhash = fields.blurhash;
|
|
53
|
+
return attachment;
|
|
54
|
+
}
|
|
55
|
+
export const MediaAttachmentsSymbol = Symbol.for("media-attachments");
|
|
56
|
+
/** Gets all the media attachments on an event */
|
|
57
|
+
export function getMediaAttachments(event) {
|
|
58
|
+
return getOrComputeCachedValue(event, MediaAttachmentsSymbol, () => {
|
|
59
|
+
return event.tags
|
|
60
|
+
.filter((t) => t[0] === "imeta")
|
|
61
|
+
.map((tag) => {
|
|
62
|
+
try {
|
|
63
|
+
return parseMediaAttachmentTag(tag);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
// ignore invalid attachments
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
.filter((a) => !!a);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { parseMediaAttachmentTag } from "./media-attachment.js";
|
|
3
|
+
describe("media attachment helpers", () => {
|
|
4
|
+
describe("parseMediaAttachmentTag", () => {
|
|
5
|
+
it("should parse simple imeta tag", () => {
|
|
6
|
+
expect(parseMediaAttachmentTag([
|
|
7
|
+
"imeta",
|
|
8
|
+
"url https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
|
|
9
|
+
"x 3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
|
|
10
|
+
"dim 1024x1024",
|
|
11
|
+
"m image/jpeg",
|
|
12
|
+
"blurhash ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
|
|
13
|
+
])).toEqual({
|
|
14
|
+
url: "https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
|
|
15
|
+
sha256: "3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
|
|
16
|
+
dimensions: "1024x1024",
|
|
17
|
+
type: "image/jpeg",
|
|
18
|
+
blurhash: "ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
it("should parse thumbnail url", () => {
|
|
22
|
+
expect(parseMediaAttachmentTag([
|
|
23
|
+
"imeta",
|
|
24
|
+
"url https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
|
|
25
|
+
"x 3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
|
|
26
|
+
"dim 1024x1024",
|
|
27
|
+
"m image/jpeg",
|
|
28
|
+
"blurhash ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
|
|
29
|
+
"thumb https://exmaple.com/thumb.jpg",
|
|
30
|
+
])).toEqual({
|
|
31
|
+
url: "https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
|
|
32
|
+
sha256: "3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
|
|
33
|
+
dimensions: "1024x1024",
|
|
34
|
+
type: "image/jpeg",
|
|
35
|
+
blurhash: "ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
|
|
36
|
+
thumbnail: "https://exmaple.com/thumb.jpg",
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
it("should parse multiple fallback urls", () => {
|
|
40
|
+
expect(parseMediaAttachmentTag([
|
|
41
|
+
"imeta",
|
|
42
|
+
"url https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
|
|
43
|
+
"x 3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
|
|
44
|
+
"dim 1024x1024",
|
|
45
|
+
"m image/jpeg",
|
|
46
|
+
"blurhash ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
|
|
47
|
+
"fallback https://exmaple.com/image2.jpg",
|
|
48
|
+
"fallback https://exmaple.com/image3.jpg",
|
|
49
|
+
])).toEqual({
|
|
50
|
+
url: "https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
|
|
51
|
+
sha256: "3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
|
|
52
|
+
dimensions: "1024x1024",
|
|
53
|
+
type: "image/jpeg",
|
|
54
|
+
blurhash: "ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
|
|
55
|
+
fallback: ["https://exmaple.com/image2.jpg", "https://exmaple.com/image3.jpg"],
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NostrEvent } from "nostr-tools";
|
|
2
|
+
export declare const MutePublicSymbol: unique symbol;
|
|
3
|
+
export declare const MuteHiddenSymbol: unique symbol;
|
|
4
|
+
export type Mutes = {
|
|
5
|
+
pubkeys: Set<string>;
|
|
6
|
+
threads: Set<string>;
|
|
7
|
+
hashtags: Set<string>;
|
|
8
|
+
words: Set<string>;
|
|
9
|
+
};
|
|
10
|
+
export declare function parseMutedTags(tags: string[][]): Mutes;
|
|
11
|
+
/** Returns muted things */
|
|
12
|
+
export declare function getMutedThings(mute: NostrEvent): Mutes;
|
|
13
|
+
/** Returns the hidden muted content if the event is unlocked */
|
|
14
|
+
export declare function getHiddenMutedThings(mute: NostrEvent): Mutes | undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { isETag, isPTag, isTTag } from "./tags.js";
|
|
2
|
+
import { getOrComputeCachedValue } from "./cache.js";
|
|
3
|
+
import { getHiddenTags } from "./hidden-tags.js";
|
|
4
|
+
export const MutePublicSymbol = Symbol.for("mute-public");
|
|
5
|
+
export const MuteHiddenSymbol = Symbol.for("mute-hidden");
|
|
6
|
+
export function parseMutedTags(tags) {
|
|
7
|
+
const pubkeys = new Set(tags.filter(isPTag).map((t) => t[1]));
|
|
8
|
+
const threads = new Set(tags.filter(isETag).map((t) => t[1]));
|
|
9
|
+
const hashtags = new Set(tags.filter(isTTag).map((t) => t[1].toLocaleLowerCase()));
|
|
10
|
+
const words = new Set(tags.filter((t) => t[0] === "word" && t[1]).map((t) => t[1].toLocaleLowerCase()));
|
|
11
|
+
return { pubkeys, threads, hashtags, words };
|
|
12
|
+
}
|
|
13
|
+
/** Returns muted things */
|
|
14
|
+
export function getMutedThings(mute) {
|
|
15
|
+
return getOrComputeCachedValue(mute, MutePublicSymbol, (e) => parseMutedTags(e.tags));
|
|
16
|
+
}
|
|
17
|
+
/** Returns the hidden muted content if the event is unlocked */
|
|
18
|
+
export function getHiddenMutedThings(mute) {
|
|
19
|
+
return getOrComputeCachedValue(mute, MuteHiddenSymbol, () => {
|
|
20
|
+
const tags = getHiddenTags(mute);
|
|
21
|
+
return tags && parseMutedTags(tags);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type Pipe = {
|
|
2
|
+
<A>(a: A): A;
|
|
3
|
+
<A, B>(a: A, ab: (a: A) => B): B;
|
|
4
|
+
<A, B, C>(a: A, ab: (a: A) => B, bc: (b: B) => C): C;
|
|
5
|
+
<A, B, C, D>(a: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D): D;
|
|
6
|
+
<A, B, C, D, E>(a: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E): E;
|
|
7
|
+
<A, B, C, D, E, F>(a: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F): F;
|
|
8
|
+
<A, B, C, D, E, F, G>(a: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F, fg: (f: F) => G): G;
|
|
9
|
+
};
|
|
10
|
+
export declare const pipe: Pipe;
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { isATag, processTags } from "./tags.js";
|
|
2
|
+
import { isATag, isNameValueTag, processTags } from "./tags.js";
|
|
3
3
|
import { getAddressPointerFromATag } from "./pointers.js";
|
|
4
|
-
describe("
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
describe("isNameValueTag", () => {
|
|
5
|
+
it("should return true if tag has at least two indexes", () => {
|
|
6
|
+
expect(isNameValueTag(["a", "30000:pubkey:list"])).toBe(true);
|
|
7
|
+
expect(isNameValueTag(["title", "article", "other-value"])).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
it("should ignore tags without values", () => {
|
|
10
|
+
expect(isNameValueTag(["a"])).toBe(false);
|
|
11
|
+
expect(isNameValueTag(["title"])).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
describe("processTags", () => {
|
|
15
|
+
it("should filter out errors", () => {
|
|
16
|
+
expect(processTags([["a", "bad coordinate"], ["e"], ["a", "30000:pubkey:list"]], getAddressPointerFromATag)).toEqual([{ identifier: "list", kind: 30000, pubkey: "pubkey" }]);
|
|
17
|
+
});
|
|
18
|
+
it("should filter out undefined", () => {
|
|
19
|
+
expect(processTags([["a", "bad coordinate"], ["e"], ["a", "30000:pubkey:list"]], (tag) => isATag(tag) ? tag : undefined)).toEqual([
|
|
20
|
+
["a", "bad coordinate"],
|
|
21
|
+
["a", "30000:pubkey:list"],
|
|
22
|
+
]);
|
|
15
23
|
});
|
|
16
24
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { Observable, Subject, firstValueFrom } from "rxjs";
|
|
3
|
+
import { simpleTimeout, TimeoutError } from "./simple-timeout.js";
|
|
4
|
+
describe("simpleTimeout operator", () => {
|
|
5
|
+
it("should throw TimeoutError after specified timeout period", async () => {
|
|
6
|
+
const subject = new Subject();
|
|
7
|
+
const obs = subject.pipe(simpleTimeout(10));
|
|
8
|
+
const promise = firstValueFrom(obs);
|
|
9
|
+
await expect(promise).rejects.toThrow(TimeoutError);
|
|
10
|
+
await expect(promise).rejects.toThrow("Timeout");
|
|
11
|
+
});
|
|
12
|
+
it("should throw TimeoutError with custom message", async () => {
|
|
13
|
+
const subject = new Subject();
|
|
14
|
+
const customMessage = "Custom timeout message";
|
|
15
|
+
const obs = subject.pipe(simpleTimeout(10, customMessage));
|
|
16
|
+
const promise = firstValueFrom(obs);
|
|
17
|
+
await expect(promise).rejects.toThrow(TimeoutError);
|
|
18
|
+
await expect(promise).rejects.toThrow(customMessage);
|
|
19
|
+
});
|
|
20
|
+
it("should not throw when value emitted before timeout", async () => {
|
|
21
|
+
const subject = new Subject();
|
|
22
|
+
const obs = subject.pipe(simpleTimeout(1000));
|
|
23
|
+
const promise = firstValueFrom(obs);
|
|
24
|
+
subject.next("test value");
|
|
25
|
+
await expect(promise).resolves.toBe("test value");
|
|
26
|
+
});
|
|
27
|
+
it("should complete without error when source emits non-null value before timeout", async () => {
|
|
28
|
+
const source = new Observable((subscriber) => {
|
|
29
|
+
subscriber.next("test value");
|
|
30
|
+
});
|
|
31
|
+
const result = await firstValueFrom(source.pipe(simpleTimeout(10)));
|
|
32
|
+
expect(result).toBe("test value");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { COMMENT_KIND, getEventUID } from "../helpers/index.js";
|
|
2
|
+
import { isParameterizedReplaceableKind } from "nostr-tools/kinds";
|
|
3
|
+
/** Returns all NIP-22 comment replies for the event */
|
|
4
|
+
export function CommentsQuery(parent) {
|
|
5
|
+
return {
|
|
6
|
+
key: `${getEventUID(parent)}-comments`,
|
|
7
|
+
run: (events) => {
|
|
8
|
+
const filter = { kinds: [COMMENT_KIND], "#e": [parent.id] };
|
|
9
|
+
if (isParameterizedReplaceableKind(parent.kind))
|
|
10
|
+
filter["#a"] = [getEventUID(parent)];
|
|
11
|
+
return events.timeline(filter);
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { Observable } from "rxjs";
|
|
2
|
-
export type StatefulObservable<T> = Observable<T> & {
|
|
3
|
-
_stateful?: true;
|
|
4
|
-
value?: T;
|
|
5
|
-
error?: Error;
|
|
6
|
-
complete?: boolean;
|
|
7
|
-
};
|
|
8
|
-
/** Wraps an {@link Observable} and makes it stateful */
|
|
9
|
-
export declare function stateful<T extends unknown>(observable: Observable<T>, cleanup?: boolean): StatefulObservable<T>;
|
|
10
|
-
export declare function isStateful<T extends unknown>(observable: Observable<T> | StatefulObservable<T>): observable is StatefulObservable<T>;
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { Observable } from "rxjs";
|
|
2
|
-
/** Wraps an {@link Observable} and makes it stateful */
|
|
3
|
-
export function stateful(observable, cleanup = false) {
|
|
4
|
-
let subscription = undefined;
|
|
5
|
-
let observers = [];
|
|
6
|
-
const self = new Observable((observer) => {
|
|
7
|
-
// add observer to list
|
|
8
|
-
observers.push(observer);
|
|
9
|
-
// pass any cached values
|
|
10
|
-
if (self.value)
|
|
11
|
-
observer.next(self.value);
|
|
12
|
-
if (self.error)
|
|
13
|
-
observer.error(self.error);
|
|
14
|
-
if (self.complete)
|
|
15
|
-
observer.complete();
|
|
16
|
-
// subscribe if not already
|
|
17
|
-
if (!subscription) {
|
|
18
|
-
subscription = observable.subscribe({
|
|
19
|
-
next: (v) => {
|
|
20
|
-
self.value = v;
|
|
21
|
-
for (const observer of observers)
|
|
22
|
-
observer.next(v);
|
|
23
|
-
},
|
|
24
|
-
error: (err) => {
|
|
25
|
-
self.error = err;
|
|
26
|
-
for (const observer of observers)
|
|
27
|
-
observer.error(err);
|
|
28
|
-
},
|
|
29
|
-
complete: () => {
|
|
30
|
-
self.complete = true;
|
|
31
|
-
for (const observer of observers)
|
|
32
|
-
observer.complete();
|
|
33
|
-
},
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
return () => {
|
|
37
|
-
let i = observers.indexOf(observer);
|
|
38
|
-
if (i !== -1) {
|
|
39
|
-
// remove observer from list
|
|
40
|
-
observers.splice(i, 1);
|
|
41
|
-
if (subscription && observers.length === 0) {
|
|
42
|
-
subscription.unsubscribe();
|
|
43
|
-
subscription = undefined;
|
|
44
|
-
// reset cached values
|
|
45
|
-
if (cleanup) {
|
|
46
|
-
delete self.value;
|
|
47
|
-
delete self.error;
|
|
48
|
-
delete self.complete;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
});
|
|
54
|
-
self._stateful = true;
|
|
55
|
-
return self;
|
|
56
|
-
}
|
|
57
|
-
export function isStateful(observable) {
|
|
58
|
-
// @ts-expect-error
|
|
59
|
-
return observable._stateful;
|
|
60
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import Observable from "zen-observable";
|
|
2
|
-
/** Throttles an {@link Observable} */
|
|
3
|
-
export function throttle(source, interval) {
|
|
4
|
-
return new Observable((observer) => {
|
|
5
|
-
let lastEmissionTime = 0;
|
|
6
|
-
let subscription = source.subscribe({
|
|
7
|
-
next(value) {
|
|
8
|
-
const currentTime = Date.now();
|
|
9
|
-
if (currentTime - lastEmissionTime >= interval) {
|
|
10
|
-
lastEmissionTime = currentTime;
|
|
11
|
-
observer.next(value);
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
error(err) {
|
|
15
|
-
observer.error(err);
|
|
16
|
-
},
|
|
17
|
-
complete() {
|
|
18
|
-
observer.complete();
|
|
19
|
-
},
|
|
20
|
-
});
|
|
21
|
-
return () => subscription.unsubscribe();
|
|
22
|
-
});
|
|
23
|
-
}
|
package/dist/utils/lru.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
type Item<T> = {
|
|
2
|
-
key: string;
|
|
3
|
-
prev: Item<T> | null;
|
|
4
|
-
value: T;
|
|
5
|
-
next: Item<T> | null;
|
|
6
|
-
expiry: number;
|
|
7
|
-
};
|
|
8
|
-
/**
|
|
9
|
-
* Copied from tiny-lru and modified to support typescript
|
|
10
|
-
* @see https://github.com/avoidwork/tiny-lru/blob/master/src/lru.js
|
|
11
|
-
*/
|
|
12
|
-
export declare class LRU<T extends unknown> {
|
|
13
|
-
first: Item<T> | null;
|
|
14
|
-
items: Record<string, Item<T>>;
|
|
15
|
-
last: Item<T> | null;
|
|
16
|
-
max: number;
|
|
17
|
-
resetTtl: boolean;
|
|
18
|
-
size: number;
|
|
19
|
-
ttl: number;
|
|
20
|
-
constructor(max?: number, ttl?: number, resetTtl?: boolean);
|
|
21
|
-
clear(): this;
|
|
22
|
-
delete(key: string): this;
|
|
23
|
-
entries(keys?: string[]): (string | T | undefined)[][];
|
|
24
|
-
evict(bypass?: boolean): this;
|
|
25
|
-
expiresAt(key: string): number | undefined;
|
|
26
|
-
get(key: string): T | undefined;
|
|
27
|
-
has(key: string): boolean;
|
|
28
|
-
keys(): string[];
|
|
29
|
-
set(key: string, value: T, bypass?: boolean, resetTtl?: boolean): this;
|
|
30
|
-
values(keys?: string[]): NonNullable<T>[];
|
|
31
|
-
}
|
|
32
|
-
export {};
|
package/dist/utils/lru.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copied from tiny-lru and modified to support typescript
|
|
3
|
-
* @see https://github.com/avoidwork/tiny-lru/blob/master/src/lru.js
|
|
4
|
-
*/
|
|
5
|
-
export class LRU {
|
|
6
|
-
first = null;
|
|
7
|
-
items = Object.create(null);
|
|
8
|
-
last = null;
|
|
9
|
-
max;
|
|
10
|
-
resetTtl;
|
|
11
|
-
size;
|
|
12
|
-
ttl;
|
|
13
|
-
constructor(max = 0, ttl = 0, resetTtl = false) {
|
|
14
|
-
this.first = null;
|
|
15
|
-
this.items = Object.create(null);
|
|
16
|
-
this.last = null;
|
|
17
|
-
this.max = max;
|
|
18
|
-
this.resetTtl = resetTtl;
|
|
19
|
-
this.size = 0;
|
|
20
|
-
this.ttl = ttl;
|
|
21
|
-
}
|
|
22
|
-
clear() {
|
|
23
|
-
this.first = null;
|
|
24
|
-
this.items = Object.create(null);
|
|
25
|
-
this.last = null;
|
|
26
|
-
this.size = 0;
|
|
27
|
-
return this;
|
|
28
|
-
}
|
|
29
|
-
delete(key) {
|
|
30
|
-
if (this.has(key)) {
|
|
31
|
-
const item = this.items[key];
|
|
32
|
-
delete this.items[key];
|
|
33
|
-
this.size--;
|
|
34
|
-
if (item.prev !== null) {
|
|
35
|
-
item.prev.next = item.next;
|
|
36
|
-
}
|
|
37
|
-
if (item.next !== null) {
|
|
38
|
-
item.next.prev = item.prev;
|
|
39
|
-
}
|
|
40
|
-
if (this.first === item) {
|
|
41
|
-
this.first = item.next;
|
|
42
|
-
}
|
|
43
|
-
if (this.last === item) {
|
|
44
|
-
this.last = item.prev;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return this;
|
|
48
|
-
}
|
|
49
|
-
entries(keys = this.keys()) {
|
|
50
|
-
return keys.map((key) => [key, this.get(key)]);
|
|
51
|
-
}
|
|
52
|
-
evict(bypass = false) {
|
|
53
|
-
if (bypass || this.size > 0) {
|
|
54
|
-
const item = this.first;
|
|
55
|
-
delete this.items[item.key];
|
|
56
|
-
if (--this.size === 0) {
|
|
57
|
-
this.first = null;
|
|
58
|
-
this.last = null;
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
this.first = item.next;
|
|
62
|
-
this.first.prev = null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return this;
|
|
66
|
-
}
|
|
67
|
-
expiresAt(key) {
|
|
68
|
-
let result;
|
|
69
|
-
if (this.has(key)) {
|
|
70
|
-
result = this.items[key].expiry;
|
|
71
|
-
}
|
|
72
|
-
return result;
|
|
73
|
-
}
|
|
74
|
-
get(key) {
|
|
75
|
-
let result;
|
|
76
|
-
if (this.has(key)) {
|
|
77
|
-
const item = this.items[key];
|
|
78
|
-
if (this.ttl > 0 && item.expiry <= Date.now()) {
|
|
79
|
-
this.delete(key);
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
result = item.value;
|
|
83
|
-
this.set(key, result, true);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return result;
|
|
87
|
-
}
|
|
88
|
-
has(key) {
|
|
89
|
-
return key in this.items;
|
|
90
|
-
}
|
|
91
|
-
keys() {
|
|
92
|
-
const result = [];
|
|
93
|
-
let x = this.first;
|
|
94
|
-
while (x !== null) {
|
|
95
|
-
result.push(x.key);
|
|
96
|
-
x = x.next;
|
|
97
|
-
}
|
|
98
|
-
return result;
|
|
99
|
-
}
|
|
100
|
-
set(key, value, bypass = false, resetTtl = this.resetTtl) {
|
|
101
|
-
let item;
|
|
102
|
-
if (bypass || this.has(key)) {
|
|
103
|
-
item = this.items[key];
|
|
104
|
-
item.value = value;
|
|
105
|
-
if (bypass === false && resetTtl) {
|
|
106
|
-
item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;
|
|
107
|
-
}
|
|
108
|
-
if (this.last !== item) {
|
|
109
|
-
const last = this.last, next = item.next, prev = item.prev;
|
|
110
|
-
if (this.first === item) {
|
|
111
|
-
this.first = item.next;
|
|
112
|
-
}
|
|
113
|
-
item.next = null;
|
|
114
|
-
item.prev = this.last;
|
|
115
|
-
last.next = item;
|
|
116
|
-
if (prev !== null) {
|
|
117
|
-
prev.next = next;
|
|
118
|
-
}
|
|
119
|
-
if (next !== null) {
|
|
120
|
-
next.prev = prev;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
if (this.max > 0 && this.size === this.max) {
|
|
126
|
-
this.evict(true);
|
|
127
|
-
}
|
|
128
|
-
item = this.items[key] = {
|
|
129
|
-
expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,
|
|
130
|
-
key: key,
|
|
131
|
-
prev: this.last,
|
|
132
|
-
next: null,
|
|
133
|
-
value,
|
|
134
|
-
};
|
|
135
|
-
if (++this.size === 1) {
|
|
136
|
-
this.first = item;
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
this.last.next = item;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
this.last = item;
|
|
143
|
-
return this;
|
|
144
|
-
}
|
|
145
|
-
values(keys = this.keys()) {
|
|
146
|
-
return keys.map((key) => this.get(key));
|
|
147
|
-
}
|
|
148
|
-
}
|