applesauce-core 0.0.0-next-20250312111321 → 0.0.0-next-20250312113207

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,74 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { kinds } from "nostr-tools";
3
+ import { EventStore } from "./event-store.js";
4
+ import { addSeenRelay, getSeenRelays } from "../helpers/relays.js";
5
+ let eventStore;
6
+ beforeEach(() => {
7
+ eventStore = new EventStore();
8
+ });
9
+ const event = {
10
+ content: '{"name":"hzrd149","picture":"https://cdn.hzrd149.com/5ed3fe5df09a74e8c126831eac999364f9eb7624e2b86d521521b8021de20bdc.png","about":"JavaScript developer working on some nostr stuff\\n- noStrudel https://nostrudel.ninja/ \\n- Blossom https://github.com/hzrd149/blossom \\n- Applesauce https://hzrd149.github.io/applesauce/","website":"https://hzrd149.com","nip05":"_@hzrd149.com","lud16":"hzrd1499@minibits.cash","pubkey":"266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5","display_name":"hzrd149","displayName":"hzrd149","banner":""}',
11
+ created_at: 1738362529,
12
+ id: "e9df8d5898c4ccfbd21fcd59f3f48abb3ff0ab7259b19570e2f1756de1e9306b",
13
+ kind: 0,
14
+ pubkey: "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5",
15
+ sig: "465a47b93626a587bf81dadc2b306b8f713a62db31d6ce1533198e9ae1e665a6eaf376a03250bf9ffbb02eb9059c8eafbd37ae1092d05d215757575bd8357586",
16
+ tags: [],
17
+ };
18
+ describe("add", () => {
19
+ it("should return original event in case of duplicates", () => {
20
+ const a = { ...event };
21
+ expect(eventStore.add(a)).toBe(a);
22
+ const b = { ...event };
23
+ expect(eventStore.add(b)).toBe(a);
24
+ const c = { ...event };
25
+ expect(eventStore.add(c)).toBe(a);
26
+ });
27
+ it("should merge seen relays on duplicate events", () => {
28
+ const a = { ...event };
29
+ addSeenRelay(a, "wss://relay.a.com");
30
+ eventStore.add(a);
31
+ const b = { ...event };
32
+ addSeenRelay(b, "wss://relay.b.com");
33
+ eventStore.add(b);
34
+ expect(eventStore.getEvent(event.id)).toBeDefined();
35
+ expect([...getSeenRelays(eventStore.getEvent(event.id))]).toEqual(expect.arrayContaining(["wss://relay.a.com", "wss://relay.b.com"]));
36
+ });
37
+ it("should ignore deleted events", () => {
38
+ const deleteEvent = {
39
+ id: "delete event id",
40
+ kind: kinds.EventDeletion,
41
+ created_at: event.created_at + 100,
42
+ pubkey: event.pubkey,
43
+ tags: [["e", event.id]],
44
+ sig: "this should be ignored for the test",
45
+ content: "test",
46
+ };
47
+ // add delete event first
48
+ eventStore.add(deleteEvent);
49
+ // now event should be ignored
50
+ eventStore.add(event);
51
+ expect(eventStore.getEvent(event.id)).toBeUndefined();
52
+ });
53
+ });
54
+ describe("verifyEvent", () => {
55
+ it("should be called for all events added", () => {
56
+ const verifyEvent = vi.fn().mockReturnValue(true);
57
+ eventStore.verifyEvent = verifyEvent;
58
+ eventStore.add(event);
59
+ expect(verifyEvent).toHaveBeenCalledWith(event);
60
+ });
61
+ it("should not be called for duplicate events", () => {
62
+ const verifyEvent = vi.fn().mockReturnValue(true);
63
+ eventStore.verifyEvent = verifyEvent;
64
+ const a = { ...event };
65
+ eventStore.add(a);
66
+ expect(verifyEvent).toHaveBeenCalledWith(a);
67
+ const b = { ...event };
68
+ eventStore.add(b);
69
+ expect(verifyEvent).toHaveBeenCalledTimes(1);
70
+ const c = { ...event };
71
+ eventStore.add(c);
72
+ expect(verifyEvent).toHaveBeenCalledTimes(1);
73
+ });
74
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,103 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { getFileMetadataFromImetaTag, parseFileMetadataTags } from "./file-metadata.js";
3
+ describe("file metadata helpers", () => {
4
+ describe("parseFileMetadataTags", () => {
5
+ it("should parse a simple 1060 event", () => {
6
+ const tags = [
7
+ ["url", "https://image.nostr.build/30696696e57a2732d4e9f1b15ff4d4d4eaa64b759df6876863f436ff5d736eae.gif"],
8
+ ["ox", "30696696e57a2732d4e9f1b15ff4d4d4eaa64b759df6876863f436ff5d736eae"],
9
+ ["fallback", "https://media.tenor.com/wpvrkjn192gAAAAC/daenerys-targaryen.gif"],
10
+ ["x", "77fcf42b2b720babcdbe686eff67273d8a68862d74a2672db672bc48439a3ea5"],
11
+ ["m", "image/gif"],
12
+ ["dim", "360x306"],
13
+ ["bh", "L38zleNL00~W^kRj0L-p0KM_^kx]"],
14
+ ["blurhash", "L38zleNL00~W^kRj0L-p0KM_^kx]"],
15
+ [
16
+ "thumb",
17
+ "https://image.nostr.build/thumb/30696696e57a2732d4e9f1b15ff4d4d4eaa64b759df6876863f436ff5d736eae.gif",
18
+ ],
19
+ ["t", "gifbuddy"],
20
+ ["summary", "Khaleesi call dragons Daenerys Targaryen"],
21
+ ["alt", "a woman with blonde hair and a brooch on her shoulder"],
22
+ [
23
+ "thumb",
24
+ "https://media.tenor.com/wpvrkjn192gAAAAx/daenerys-targaryen.webp",
25
+ "5d92423664fc15874b1d26c70a05a541ec09b5c438bf157977a87c8e64b31463",
26
+ ],
27
+ [
28
+ "image",
29
+ "https://media.tenor.com/wpvrkjn192gAAAAe/daenerys-targaryen.png",
30
+ "5d92423664fc15874b1d26c70a05a541ec09b5c438bf157977a87c8e64b31463",
31
+ ],
32
+ ];
33
+ expect(parseFileMetadataTags(tags)).toEqual({
34
+ url: "https://image.nostr.build/30696696e57a2732d4e9f1b15ff4d4d4eaa64b759df6876863f436ff5d736eae.gif",
35
+ type: "image/gif",
36
+ dimensions: "360x306",
37
+ blurhash: "L38zleNL00~W^kRj0L-p0KM_^kx]",
38
+ sha256: "77fcf42b2b720babcdbe686eff67273d8a68862d74a2672db672bc48439a3ea5",
39
+ originalSha256: "30696696e57a2732d4e9f1b15ff4d4d4eaa64b759df6876863f436ff5d736eae",
40
+ thumbnail: "https://media.tenor.com/wpvrkjn192gAAAAx/daenerys-targaryen.webp",
41
+ image: "https://media.tenor.com/wpvrkjn192gAAAAe/daenerys-targaryen.png",
42
+ summary: "Khaleesi call dragons Daenerys Targaryen",
43
+ fallback: ["https://media.tenor.com/wpvrkjn192gAAAAC/daenerys-targaryen.gif"],
44
+ alt: "a woman with blonde hair and a brooch on her shoulder",
45
+ });
46
+ });
47
+ });
48
+ describe("getFileMetadataFromImetaTag", () => {
49
+ it("should parse simple imeta tag", () => {
50
+ expect(getFileMetadataFromImetaTag([
51
+ "imeta",
52
+ "url https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
53
+ "x 3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
54
+ "dim 1024x1024",
55
+ "m image/jpeg",
56
+ "blurhash ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
57
+ ])).toEqual({
58
+ url: "https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
59
+ sha256: "3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
60
+ dimensions: "1024x1024",
61
+ type: "image/jpeg",
62
+ blurhash: "ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
63
+ });
64
+ });
65
+ it("should parse thumbnail url", () => {
66
+ expect(getFileMetadataFromImetaTag([
67
+ "imeta",
68
+ "url https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
69
+ "x 3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
70
+ "dim 1024x1024",
71
+ "m image/jpeg",
72
+ "blurhash ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
73
+ "thumb https://exmaple.com/thumb.jpg",
74
+ ])).toEqual({
75
+ url: "https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
76
+ sha256: "3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
77
+ dimensions: "1024x1024",
78
+ type: "image/jpeg",
79
+ blurhash: "ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
80
+ thumbnail: "https://exmaple.com/thumb.jpg",
81
+ });
82
+ });
83
+ it("should parse multiple fallback urls", () => {
84
+ expect(getFileMetadataFromImetaTag([
85
+ "imeta",
86
+ "url https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
87
+ "x 3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
88
+ "dim 1024x1024",
89
+ "m image/jpeg",
90
+ "blurhash ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
91
+ "fallback https://exmaple.com/image2.jpg",
92
+ "fallback https://exmaple.com/image3.jpg",
93
+ ])).toEqual({
94
+ url: "https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
95
+ sha256: "3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
96
+ dimensions: "1024x1024",
97
+ type: "image/jpeg",
98
+ blurhash: "ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
99
+ fallback: ["https://exmaple.com/image2.jpg", "https://exmaple.com/image3.jpg"],
100
+ });
101
+ });
102
+ });
103
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import { describe, beforeEach, it, expect } from "vitest";
2
+ import { finalizeEvent, generateSecretKey, getPublicKey, kinds, nip04 } from "nostr-tools";
3
+ import { getHiddenTags, unlockHiddenTags } from "./hidden-tags.js";
4
+ import { unixNow } from "./time.js";
5
+ const key = generateSecretKey();
6
+ const pubkey = getPublicKey(key);
7
+ const signer = {
8
+ nip04: {
9
+ encrypt: (pubkey, plaintext) => nip04.encrypt(key, pubkey, plaintext),
10
+ decrypt: (pubkey, ciphertext) => nip04.decrypt(key, pubkey, ciphertext),
11
+ },
12
+ };
13
+ describe("Private Lists", () => {
14
+ describe("unlockHiddenTags", () => {
15
+ let list;
16
+ beforeEach(async () => {
17
+ list = finalizeEvent({
18
+ kind: kinds.Mutelist,
19
+ created_at: unixNow(),
20
+ content: await nip04.encrypt(key, pubkey, JSON.stringify([["p", "npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr"]])),
21
+ tags: [],
22
+ }, key);
23
+ });
24
+ it("should unlock hidden tags", async () => {
25
+ await unlockHiddenTags(list, signer);
26
+ expect(getHiddenTags(list)).toEqual(expect.arrayContaining([["p", "npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr"]]));
27
+ });
28
+ });
29
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,81 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { getInboxes, getOutboxes } from "./mailboxes.js";
3
+ const emptyEvent = {
4
+ kind: 10002,
5
+ content: "",
6
+ tags: [],
7
+ created_at: 0,
8
+ sig: "",
9
+ id: "",
10
+ pubkey: "",
11
+ };
12
+ describe("Mailboxes", () => {
13
+ describe("getInboxes", () => {
14
+ test("should transform urls", () => {
15
+ expect(Array.from(getInboxes({
16
+ ...emptyEvent,
17
+ tags: [["r", "wss://inbox.com"]],
18
+ }))).toEqual(expect.arrayContaining(["wss://inbox.com/"]));
19
+ });
20
+ test("should remove bad urls", () => {
21
+ expect(Array.from(getInboxes({
22
+ ...emptyEvent,
23
+ tags: [["r", "bad://inbox.com"]],
24
+ }))).toHaveLength(0);
25
+ expect(Array.from(getInboxes({
26
+ ...emptyEvent,
27
+ tags: [["r", "something that is not a url"]],
28
+ }))).toHaveLength(0);
29
+ expect(Array.from(getInboxes({
30
+ ...emptyEvent,
31
+ tags: [["r", "wss://inbox.com,wss://inbox.org"]],
32
+ }))).toHaveLength(0);
33
+ });
34
+ test("without marker", () => {
35
+ expect(Array.from(getInboxes({
36
+ ...emptyEvent,
37
+ tags: [["r", "wss://inbox.com/"]],
38
+ }))).toEqual(expect.arrayContaining(["wss://inbox.com/"]));
39
+ });
40
+ test("with marker", () => {
41
+ expect(Array.from(getInboxes({
42
+ ...emptyEvent,
43
+ tags: [["r", "wss://inbox.com/", "read"]],
44
+ }))).toEqual(expect.arrayContaining(["wss://inbox.com/"]));
45
+ });
46
+ });
47
+ describe("getOutboxes", () => {
48
+ test("should transform urls", () => {
49
+ expect(Array.from(getOutboxes({
50
+ ...emptyEvent,
51
+ tags: [["r", "wss://outbox.com"]],
52
+ }))).toEqual(expect.arrayContaining(["wss://outbox.com/"]));
53
+ });
54
+ test("should remove bad urls", () => {
55
+ expect(Array.from(getOutboxes({
56
+ ...emptyEvent,
57
+ tags: [["r", "bad://inbox.com"]],
58
+ }))).toHaveLength(0);
59
+ expect(Array.from(getOutboxes({
60
+ ...emptyEvent,
61
+ tags: [["r", "something that is not a url"]],
62
+ }))).toHaveLength(0);
63
+ expect(Array.from(getOutboxes({
64
+ ...emptyEvent,
65
+ tags: [["r", "wss://outbox.com,wss://inbox.org"]],
66
+ }))).toHaveLength(0);
67
+ });
68
+ test("without marker", () => {
69
+ expect(Array.from(getOutboxes({
70
+ ...emptyEvent,
71
+ tags: [["r", "wss://outbox.com/"]],
72
+ }))).toEqual(expect.arrayContaining(["wss://outbox.com/"]));
73
+ });
74
+ test("with marker", () => {
75
+ expect(Array.from(getOutboxes({
76
+ ...emptyEvent,
77
+ tags: [["r", "wss://outbox.com/", "write"]],
78
+ }))).toEqual(expect.arrayContaining(["wss://outbox.com/"]));
79
+ });
80
+ });
81
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { isATag, processTags } from "./tags.js";
3
+ import { getAddressPointerFromATag } from "./pointers.js";
4
+ describe("tag helpers", () => {
5
+ describe("processTags", () => {
6
+ it("should filter out errors", () => {
7
+ expect(processTags([["a", "bad coordinate"], ["e"], ["a", "30000:pubkey:list"]], getAddressPointerFromATag)).toEqual([{ identifier: "list", kind: 30000, pubkey: "pubkey" }]);
8
+ });
9
+ it("should filter out undefined", () => {
10
+ expect(processTags([["a", "bad coordinate"], ["e"], ["a", "30000:pubkey:list"]], (tag) => isATag(tag) ? tag : undefined)).toEqual([
11
+ ["a", "bad coordinate"],
12
+ ["a", "30000:pubkey:list"],
13
+ ]);
14
+ });
15
+ });
16
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { interpretThreadTags } from "./threading.js";
3
+ describe("threading helpers", () => {
4
+ describe("interpretThreadTags", () => {
5
+ it("should handle legacy tags", () => {
6
+ expect(interpretThreadTags([
7
+ ["e", "root-id"],
8
+ ["e", "reply-id"],
9
+ ])).toEqual({ root: { a: undefined, e: ["e", "root-id"] }, reply: { a: undefined, e: ["e", "reply-id"] } });
10
+ });
11
+ it("should handle nip-10 tags", () => {
12
+ expect(interpretThreadTags([
13
+ ["e", "root-id", "relay", "root"],
14
+ ["e", "reply-id", "relay", "reply"],
15
+ ])).toEqual({
16
+ root: { a: undefined, e: ["e", "root-id", "relay", "root"] },
17
+ reply: { a: undefined, e: ["e", "reply-id", "relay", "reply"] },
18
+ });
19
+ });
20
+ it("should ignore mention nip-10 tags", () => {
21
+ expect(interpretThreadTags([
22
+ ["e", "root-id", "relay", "root"],
23
+ ["e", "mention-id", "relay", "mention"],
24
+ ["e", "reply-id", "relay", "reply"],
25
+ ])).toEqual({
26
+ root: { a: undefined, e: ["e", "root-id", "relay", "root"] },
27
+ reply: { a: undefined, e: ["e", "reply-id", "relay", "reply"] },
28
+ });
29
+ });
30
+ it("should handle single nip-10 tags", () => {
31
+ expect(interpretThreadTags([["e", "root-id", "relay", "root"]])).toEqual({
32
+ root: { a: undefined, e: ["e", "root-id", "relay", "root"] },
33
+ reply: { a: undefined, e: ["e", "root-id", "relay", "root"] },
34
+ });
35
+ expect(interpretThreadTags([["e", "reply-id", "relay", "reply"]])).toEqual({
36
+ root: { a: undefined, e: ["e", "reply-id", "relay", "reply"] },
37
+ reply: { a: undefined, e: ["e", "reply-id", "relay", "reply"] },
38
+ });
39
+ });
40
+ });
41
+ });
@@ -0,0 +1,2 @@
1
+ import { Observable } from "rxjs";
2
+ export declare function getValue<T>(observable: Observable<T>): T | Promise<T>;
@@ -0,0 +1,13 @@
1
+ import { BehaviorSubject } from "rxjs";
2
+ export function getValue(observable) {
3
+ if (observable instanceof BehaviorSubject)
4
+ return observable.value;
5
+ if (Reflect.has(observable, "value"))
6
+ return Reflect.get(observable, "value");
7
+ return new Promise((res) => {
8
+ const sub = observable.subscribe((v) => {
9
+ res(v);
10
+ sub.unsubscribe();
11
+ });
12
+ });
13
+ }
@@ -0,0 +1,2 @@
1
+ import { BehaviorSubject, Observable, UnaryFunction } from "rxjs";
2
+ export declare function shareBehavior<T>(): UnaryFunction<Observable<T>, BehaviorSubject<T | undefined>>;
@@ -0,0 +1,7 @@
1
+ import { BehaviorSubject, share } from "rxjs";
2
+ export function shareBehavior() {
3
+ return share({
4
+ connector: () => new BehaviorSubject(undefined),
5
+ resetOnRefCountZero: false,
6
+ });
7
+ }
@@ -0,0 +1,10 @@
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>;
@@ -0,0 +1,60 @@
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
+ }
@@ -0,0 +1,3 @@
1
+ import Observable from "zen-observable";
2
+ /** Throttles an {@link Observable} */
3
+ export declare function throttle<T>(source: Observable<T>, interval: number): Observable<T>;
@@ -0,0 +1,23 @@
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
+ }
@@ -0,0 +1,32 @@
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 {};
@@ -0,0 +1,148 @@
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-core",
3
- "version": "0.0.0-next-20250312111321",
3
+ "version": "0.0.0-next-20250312113207",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",