applesauce-core 0.0.0-next-20250726160247 → 0.0.0-next-20250806165639
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/__tests__/exports.test.d.ts +1 -0
- package/dist/__tests__/exports.test.js +27 -0
- package/dist/__tests__/fixtures.d.ts +17 -0
- package/dist/__tests__/fixtures.js +28 -0
- package/dist/event-store/__tests__/event-store.test.d.ts +1 -0
- package/dist/event-store/__tests__/event-store.test.js +386 -0
- package/dist/event-store/common.d.ts +1 -0
- package/dist/event-store/common.js +2 -0
- package/dist/event-store/event-set.js +5 -3
- package/dist/event-store/event-store.d.ts +13 -3
- package/dist/event-store/event-store.js +58 -6
- package/dist/event-store/interface.d.ts +7 -3
- package/dist/helpers/__tests__/app-handlers.test.d.ts +1 -0
- package/dist/helpers/__tests__/app-handlers.test.js +184 -0
- package/dist/helpers/__tests__/blossom.test.d.ts +1 -0
- package/dist/helpers/__tests__/blossom.test.js +13 -0
- package/dist/helpers/__tests__/bookmarks.test.d.ts +1 -0
- package/dist/helpers/__tests__/bookmarks.test.js +88 -0
- package/dist/helpers/__tests__/comment.test.d.ts +1 -0
- package/dist/helpers/__tests__/comment.test.js +249 -0
- package/dist/helpers/__tests__/contacts.test.d.ts +1 -0
- package/dist/helpers/__tests__/contacts.test.js +34 -0
- package/dist/helpers/__tests__/emoji.test.d.ts +1 -0
- package/dist/helpers/__tests__/emoji.test.js +110 -0
- package/dist/helpers/__tests__/encrypted-content-cache.test.d.ts +1 -0
- package/dist/helpers/__tests__/encrypted-content-cache.test.js +65 -0
- package/dist/helpers/__tests__/encryption.test.d.ts +1 -0
- package/dist/helpers/__tests__/encryption.test.js +21 -0
- package/dist/helpers/__tests__/event.test.d.ts +1 -0
- package/dist/helpers/__tests__/event.test.js +36 -0
- package/dist/helpers/__tests__/events.test.d.ts +1 -0
- package/dist/helpers/__tests__/events.test.js +32 -0
- package/dist/helpers/__tests__/exports.test.d.ts +1 -0
- package/dist/helpers/__tests__/exports.test.js +295 -0
- package/dist/helpers/__tests__/file-metadata.test.d.ts +1 -0
- package/dist/helpers/__tests__/file-metadata.test.js +103 -0
- package/dist/helpers/__tests__/groups.test.d.ts +1 -0
- package/dist/helpers/__tests__/groups.test.js +61 -0
- package/dist/helpers/__tests__/hidden-tags.test.d.ts +1 -0
- package/dist/helpers/__tests__/hidden-tags.test.js +29 -0
- package/dist/helpers/__tests__/mailboxes.test.d.ts +1 -0
- package/dist/helpers/__tests__/mailboxes.test.js +81 -0
- package/dist/helpers/__tests__/messages.test.d.ts +1 -0
- package/dist/helpers/__tests__/messages.test.js +91 -0
- package/dist/helpers/__tests__/mutes.test.d.ts +1 -0
- package/dist/helpers/__tests__/mutes.test.js +55 -0
- package/dist/helpers/__tests__/pointers.test.d.ts +1 -0
- package/dist/helpers/__tests__/pointers.test.js +118 -0
- package/dist/helpers/__tests__/profile.test.d.ts +1 -0
- package/dist/helpers/__tests__/profile.test.js +72 -0
- package/dist/helpers/__tests__/reactions.test.d.ts +1 -0
- package/dist/helpers/__tests__/reactions.test.js +88 -0
- package/dist/helpers/__tests__/relays.test.d.ts +1 -0
- package/dist/helpers/__tests__/relays.test.js +21 -0
- package/dist/helpers/__tests__/tags.test.d.ts +1 -0
- package/dist/helpers/__tests__/tags.test.js +24 -0
- package/dist/helpers/__tests__/threading.test.d.ts +1 -0
- package/dist/helpers/__tests__/threading.test.js +41 -0
- package/dist/helpers/event-cache.d.ts +15 -0
- package/dist/helpers/event-cache.js +32 -0
- package/dist/helpers/event.d.ts +1 -1
- package/dist/helpers/event.js +1 -1
- package/dist/helpers/expiration.js +1 -2
- package/dist/helpers/index.d.ts +3 -0
- package/dist/helpers/index.js +3 -0
- package/dist/helpers/stream-chat.d.ts +4 -0
- package/dist/helpers/stream-chat.js +9 -0
- package/dist/helpers/stream.d.ts +31 -0
- package/dist/helpers/stream.js +81 -0
- package/dist/models/__tests__/comments.test.d.ts +1 -0
- package/dist/models/__tests__/comments.test.js +36 -0
- package/dist/models/__tests__/exports.test.d.ts +1 -0
- package/dist/models/__tests__/exports.test.js +53 -0
- package/dist/models/common.js +14 -6
- package/dist/observable/__tests__/claim-events.test.d.ts +1 -0
- package/dist/observable/__tests__/claim-events.test.js +23 -0
- package/dist/observable/__tests__/claim-latest.test.d.ts +1 -0
- package/dist/observable/__tests__/claim-latest.test.js +37 -0
- package/dist/observable/__tests__/exports.test.d.ts +1 -0
- package/dist/observable/__tests__/exports.test.js +21 -0
- package/dist/observable/__tests__/map-events-to-store.test.d.ts +1 -0
- package/dist/observable/__tests__/map-events-to-store.test.js +38 -0
- package/dist/observable/__tests__/simple-timeout.test.d.ts +1 -0
- package/dist/observable/__tests__/simple-timeout.test.js +34 -0
- package/dist/observable/__tests__/watch-event-updates.test.d.ts +1 -0
- package/dist/observable/__tests__/watch-event-updates.test.js +55 -0
- package/dist/promise/__tests__/exports.test.d.ts +1 -0
- package/dist/promise/__tests__/exports.test.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import * as exports from "../index.js";
|
|
3
|
+
describe("exports", () => {
|
|
4
|
+
it("should export the expected functions", () => {
|
|
5
|
+
expect(Object.keys(exports).sort()).toMatchInlineSnapshot(`
|
|
6
|
+
[
|
|
7
|
+
"EventSet",
|
|
8
|
+
"EventStore",
|
|
9
|
+
"EventStoreSymbol",
|
|
10
|
+
"Helpers",
|
|
11
|
+
"Models",
|
|
12
|
+
"TimeoutError",
|
|
13
|
+
"defined",
|
|
14
|
+
"firstValueFrom",
|
|
15
|
+
"getObservableValue",
|
|
16
|
+
"lastValueFrom",
|
|
17
|
+
"logger",
|
|
18
|
+
"mapEventsToStore",
|
|
19
|
+
"mapEventsToTimeline",
|
|
20
|
+
"simpleTimeout",
|
|
21
|
+
"watchEventUpdates",
|
|
22
|
+
"watchEventsUpdates",
|
|
23
|
+
"withImmediateValueOrDefault",
|
|
24
|
+
]
|
|
25
|
+
`);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { NostrEvent } from "nostr-tools";
|
|
2
|
+
import { EncryptedContentSigner } from "../helpers/encrypted-content.js";
|
|
3
|
+
export declare class FakeUser implements EncryptedContentSigner {
|
|
4
|
+
key: Uint8Array<ArrayBufferLike>;
|
|
5
|
+
pubkey: string;
|
|
6
|
+
nip04: {
|
|
7
|
+
encrypt: (pubkey: string, plaintext: string) => string;
|
|
8
|
+
decrypt: (pubkey: string, ciphertext: string) => string;
|
|
9
|
+
};
|
|
10
|
+
nip44: {
|
|
11
|
+
encrypt: (pubkey: string, plaintext: string) => string;
|
|
12
|
+
decrypt: (pubkey: string, ciphertext: string) => string;
|
|
13
|
+
};
|
|
14
|
+
event(data?: Partial<NostrEvent>): NostrEvent;
|
|
15
|
+
note(content?: string, extra?: Partial<NostrEvent>): import("nostr-tools").Event;
|
|
16
|
+
profile(profile: any, extra?: Partial<NostrEvent>): import("nostr-tools").Event;
|
|
17
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { finalizeEvent, generateSecretKey, getPublicKey, kinds, nip04, nip44 } from "nostr-tools";
|
|
2
|
+
import { unixNow } from "../helpers/time.js";
|
|
3
|
+
export class FakeUser {
|
|
4
|
+
key = generateSecretKey();
|
|
5
|
+
pubkey = getPublicKey(this.key);
|
|
6
|
+
nip04 = {
|
|
7
|
+
encrypt: (pubkey, plaintext) => nip04.encrypt(this.key, pubkey, plaintext),
|
|
8
|
+
decrypt: (pubkey, ciphertext) => nip04.decrypt(this.key, pubkey, ciphertext),
|
|
9
|
+
};
|
|
10
|
+
nip44 = {
|
|
11
|
+
encrypt: (pubkey, plaintext) => nip44.encrypt(plaintext, nip44.getConversationKey(this.key, pubkey)),
|
|
12
|
+
decrypt: (pubkey, ciphertext) => nip44.decrypt(ciphertext, nip44.getConversationKey(this.key, pubkey)),
|
|
13
|
+
};
|
|
14
|
+
event(data) {
|
|
15
|
+
return finalizeEvent({
|
|
16
|
+
kind: data?.kind ?? kinds.ShortTextNote,
|
|
17
|
+
content: data?.content || "",
|
|
18
|
+
created_at: data?.created_at ?? unixNow(),
|
|
19
|
+
tags: data?.tags || [],
|
|
20
|
+
}, this.key);
|
|
21
|
+
}
|
|
22
|
+
note(content = "Hello World", extra) {
|
|
23
|
+
return this.event({ kind: kinds.ShortTextNote, content, ...extra });
|
|
24
|
+
}
|
|
25
|
+
profile(profile, extra) {
|
|
26
|
+
return this.event({ kind: kinds.Metadata, content: JSON.stringify({ ...profile }), ...extra });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { subscribeSpyTo } from "@hirez_io/observer-spy";
|
|
2
|
+
import { kinds } from "nostr-tools";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { FakeUser } from "../../__tests__/fixtures.js";
|
|
5
|
+
import { getEventUID } from "../../helpers/event.js";
|
|
6
|
+
import { addSeenRelay, getSeenRelays } from "../../helpers/relays.js";
|
|
7
|
+
import { EventModel } from "../../models/common.js";
|
|
8
|
+
import { ProfileModel } from "../../models/profile.js";
|
|
9
|
+
import { EventStore } from "../event-store.js";
|
|
10
|
+
let eventStore;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
eventStore = new EventStore();
|
|
13
|
+
});
|
|
14
|
+
const user = new FakeUser();
|
|
15
|
+
const profile = user.profile({ name: "fake user" });
|
|
16
|
+
const note = user.note();
|
|
17
|
+
describe("add", () => {
|
|
18
|
+
it("should return original event in case of duplicates", () => {
|
|
19
|
+
const a = { ...profile };
|
|
20
|
+
expect(eventStore.add(a)).toBe(a);
|
|
21
|
+
const b = { ...profile };
|
|
22
|
+
expect(eventStore.add(b)).toBe(a);
|
|
23
|
+
const c = { ...profile };
|
|
24
|
+
expect(eventStore.add(c)).toBe(a);
|
|
25
|
+
});
|
|
26
|
+
it("should merge seen relays on duplicate events", () => {
|
|
27
|
+
const a = { ...profile };
|
|
28
|
+
addSeenRelay(a, "wss://relay.a.com");
|
|
29
|
+
eventStore.add(a);
|
|
30
|
+
const b = { ...profile };
|
|
31
|
+
addSeenRelay(b, "wss://relay.b.com");
|
|
32
|
+
eventStore.add(b);
|
|
33
|
+
expect(eventStore.getEvent(profile.id)).toBeDefined();
|
|
34
|
+
expect([...getSeenRelays(eventStore.getEvent(profile.id))]).toEqual(expect.arrayContaining(["wss://relay.a.com", "wss://relay.b.com"]));
|
|
35
|
+
});
|
|
36
|
+
it("should ignore old deleted events but not newer ones", () => {
|
|
37
|
+
const deleteEvent = {
|
|
38
|
+
id: "delete event id",
|
|
39
|
+
kind: kinds.EventDeletion,
|
|
40
|
+
created_at: profile.created_at + 100,
|
|
41
|
+
pubkey: user.pubkey,
|
|
42
|
+
tags: [["e", profile.id]],
|
|
43
|
+
sig: "this should be ignored for the test",
|
|
44
|
+
content: "test",
|
|
45
|
+
};
|
|
46
|
+
// add delete event first
|
|
47
|
+
eventStore.add(deleteEvent);
|
|
48
|
+
// now event should be ignored
|
|
49
|
+
eventStore.add(profile);
|
|
50
|
+
const newProfile = user.profile({ name: "new name" }, { created_at: profile.created_at + 1000 });
|
|
51
|
+
eventStore.add(newProfile);
|
|
52
|
+
expect(eventStore.getEvent(profile.id)).toBeUndefined();
|
|
53
|
+
expect(eventStore.getEvent(newProfile.id)).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
it("should remove profile events when delete event is added", () => {
|
|
56
|
+
// Add initial replaceable event
|
|
57
|
+
eventStore.add(profile);
|
|
58
|
+
expect(eventStore.getEvent(profile.id)).toBeDefined();
|
|
59
|
+
const newProfile = user.profile({ name: "new name" }, { created_at: profile.created_at + 1000 });
|
|
60
|
+
eventStore.add(newProfile);
|
|
61
|
+
const deleteEvent = {
|
|
62
|
+
id: "delete event id",
|
|
63
|
+
kind: kinds.EventDeletion,
|
|
64
|
+
created_at: profile.created_at + 100,
|
|
65
|
+
pubkey: user.pubkey,
|
|
66
|
+
tags: [["a", `${profile.kind}:${profile.pubkey}`]],
|
|
67
|
+
sig: "this should be ignored for the test",
|
|
68
|
+
content: "test",
|
|
69
|
+
};
|
|
70
|
+
// Add delete event with coordinate
|
|
71
|
+
eventStore.add(deleteEvent);
|
|
72
|
+
// Profile should be removed since delete event is newer
|
|
73
|
+
expect(eventStore.getEvent(profile.id)).toBeUndefined();
|
|
74
|
+
expect(eventStore.getEvent(newProfile.id)).toBeDefined();
|
|
75
|
+
expect(eventStore.getReplaceable(profile.kind, profile.pubkey)).toBe(newProfile);
|
|
76
|
+
});
|
|
77
|
+
it("should remove addressable replaceable events when delete event is added", () => {
|
|
78
|
+
// Add initial replaceable event
|
|
79
|
+
const event = user.event({ content: "test", kind: 30000, tags: [["d", "test"]] });
|
|
80
|
+
eventStore.add(event);
|
|
81
|
+
expect(eventStore.getEvent(event.id)).toBeDefined();
|
|
82
|
+
const newEvent = user.event({
|
|
83
|
+
...event,
|
|
84
|
+
created_at: event.created_at + 500,
|
|
85
|
+
});
|
|
86
|
+
eventStore.add(newEvent);
|
|
87
|
+
const deleteEvent = {
|
|
88
|
+
id: "delete event id",
|
|
89
|
+
kind: kinds.EventDeletion,
|
|
90
|
+
created_at: event.created_at + 100,
|
|
91
|
+
pubkey: user.pubkey,
|
|
92
|
+
tags: [["a", `${event.kind}:${event.pubkey}:test`]],
|
|
93
|
+
sig: "this should be ignored for the test",
|
|
94
|
+
content: "test",
|
|
95
|
+
};
|
|
96
|
+
// Add delete event with coordinate
|
|
97
|
+
eventStore.add(deleteEvent);
|
|
98
|
+
// Profile should be removed since delete event is newer
|
|
99
|
+
expect(eventStore.getEvent(event.id)).toBeUndefined();
|
|
100
|
+
expect(eventStore.getEvent(newEvent.id)).toBeDefined();
|
|
101
|
+
expect(eventStore.getReplaceable(event.kind, event.pubkey, "test")).toBe(newEvent);
|
|
102
|
+
});
|
|
103
|
+
it("should return null when event is invalid and there isn't an existing event", () => {
|
|
104
|
+
const verifyEvent = vi.fn().mockReturnValue(false);
|
|
105
|
+
eventStore.verifyEvent = verifyEvent;
|
|
106
|
+
expect(eventStore.add(profile)).toBeNull();
|
|
107
|
+
expect(verifyEvent).toHaveBeenCalledWith(profile);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe("inserts", () => {
|
|
111
|
+
it("should emit newer replaceable events", () => {
|
|
112
|
+
const spy = subscribeSpyTo(eventStore.insert$);
|
|
113
|
+
eventStore.add(profile);
|
|
114
|
+
const newer = user.profile({ name: "new name" }, { created_at: profile.created_at + 100 });
|
|
115
|
+
eventStore.add(newer);
|
|
116
|
+
expect(spy.getValues()).toEqual([profile, newer]);
|
|
117
|
+
});
|
|
118
|
+
it("should not emit when older replaceable event is added", () => {
|
|
119
|
+
const spy = subscribeSpyTo(eventStore.insert$);
|
|
120
|
+
eventStore.add(profile);
|
|
121
|
+
eventStore.add(user.profile({ name: "new name" }, { created_at: profile.created_at - 1000 }));
|
|
122
|
+
expect(spy.getValues()).toEqual([profile]);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
describe("removes", () => {
|
|
126
|
+
it("should emit older replaceable events when the newest replaceable event is added", () => {
|
|
127
|
+
const spy = subscribeSpyTo(eventStore.remove$);
|
|
128
|
+
eventStore.add(profile);
|
|
129
|
+
const newer = user.profile({ name: "new name" }, { created_at: profile.created_at + 1000 });
|
|
130
|
+
eventStore.add(newer);
|
|
131
|
+
expect(spy.getValues()).toEqual([profile]);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe("verifyEvent", () => {
|
|
135
|
+
it("should be called for all events added", () => {
|
|
136
|
+
const verifyEvent = vi.fn().mockReturnValue(true);
|
|
137
|
+
eventStore.verifyEvent = verifyEvent;
|
|
138
|
+
eventStore.add(profile);
|
|
139
|
+
expect(verifyEvent).toHaveBeenCalledWith(profile);
|
|
140
|
+
});
|
|
141
|
+
it("should not be called for duplicate events", () => {
|
|
142
|
+
const verifyEvent = vi.fn().mockReturnValue(true);
|
|
143
|
+
eventStore.verifyEvent = verifyEvent;
|
|
144
|
+
const a = { ...profile };
|
|
145
|
+
eventStore.add(a);
|
|
146
|
+
expect(verifyEvent).toHaveBeenCalledWith(a);
|
|
147
|
+
const b = { ...profile };
|
|
148
|
+
eventStore.add(b);
|
|
149
|
+
expect(verifyEvent).toHaveBeenCalledTimes(1);
|
|
150
|
+
const c = { ...profile };
|
|
151
|
+
eventStore.add(c);
|
|
152
|
+
expect(verifyEvent).toHaveBeenCalledTimes(1);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
describe("removed", () => {
|
|
156
|
+
it("should complete when event is removed", () => {
|
|
157
|
+
eventStore.add(profile);
|
|
158
|
+
const spy = subscribeSpyTo(eventStore.removed(profile.id));
|
|
159
|
+
eventStore.remove(profile);
|
|
160
|
+
expect(spy.getValues()).toEqual([]);
|
|
161
|
+
expect(spy.receivedComplete()).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
describe("model", () => {
|
|
165
|
+
it("should emit synchronous value if it exists", () => {
|
|
166
|
+
let value = undefined;
|
|
167
|
+
eventStore.add(profile);
|
|
168
|
+
eventStore.model(ProfileModel, user.pubkey).subscribe((v) => (value = v));
|
|
169
|
+
expect(value).not.toBe(undefined);
|
|
170
|
+
});
|
|
171
|
+
it("should not emit undefined if value exists", () => {
|
|
172
|
+
eventStore.add(profile);
|
|
173
|
+
const spy = subscribeSpyTo(eventStore.model(EventModel, profile.id));
|
|
174
|
+
expect(spy.getValues()).toEqual([profile]);
|
|
175
|
+
});
|
|
176
|
+
it("should emit synchronous undefined if value does not exists", () => {
|
|
177
|
+
let value = 0;
|
|
178
|
+
eventStore.model(ProfileModel, user.pubkey).subscribe((v) => {
|
|
179
|
+
value = v;
|
|
180
|
+
});
|
|
181
|
+
expect(value).not.toBe(0);
|
|
182
|
+
expect(value).toBe(undefined);
|
|
183
|
+
});
|
|
184
|
+
it("should share latest value", () => {
|
|
185
|
+
eventStore.add(profile);
|
|
186
|
+
const spy = subscribeSpyTo(eventStore.model(EventModel, profile.id));
|
|
187
|
+
const spy2 = subscribeSpyTo(eventStore.model(EventModel, profile.id));
|
|
188
|
+
expect(spy.getValues()).toEqual([profile]);
|
|
189
|
+
expect(spy2.getValues()).toEqual([profile]);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
describe("event", () => {
|
|
193
|
+
it("should emit existing event", () => {
|
|
194
|
+
eventStore.add(profile);
|
|
195
|
+
const spy = subscribeSpyTo(eventStore.event(profile.id));
|
|
196
|
+
expect(spy.getValues()).toEqual([profile]);
|
|
197
|
+
});
|
|
198
|
+
it("should emit then event when its added", () => {
|
|
199
|
+
const spy = subscribeSpyTo(eventStore.event(profile.id));
|
|
200
|
+
expect(spy.getValues()).toEqual([undefined]);
|
|
201
|
+
eventStore.add(profile);
|
|
202
|
+
expect(spy.getValues()).toEqual([undefined, profile]);
|
|
203
|
+
});
|
|
204
|
+
it("should emit undefined when event is removed", () => {
|
|
205
|
+
eventStore.add(profile);
|
|
206
|
+
const spy = subscribeSpyTo(eventStore.event(profile.id));
|
|
207
|
+
expect(spy.getValues()).toEqual([profile]);
|
|
208
|
+
eventStore.remove(profile);
|
|
209
|
+
expect(spy.getValues()).toEqual([profile, undefined]);
|
|
210
|
+
});
|
|
211
|
+
it("should emit new value if event is re-added", () => {
|
|
212
|
+
eventStore.add(profile);
|
|
213
|
+
const spy = subscribeSpyTo(eventStore.event(profile.id));
|
|
214
|
+
eventStore.remove(profile);
|
|
215
|
+
eventStore.add(profile);
|
|
216
|
+
expect(spy.getValues()).toEqual([profile, undefined, profile]);
|
|
217
|
+
});
|
|
218
|
+
it("should not complete when event is removed", () => {
|
|
219
|
+
eventStore.add(profile);
|
|
220
|
+
const spy = subscribeSpyTo(eventStore.event(profile.id));
|
|
221
|
+
eventStore.remove(profile);
|
|
222
|
+
expect(spy.receivedComplete()).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
it("should emit undefined if event is not found", () => {
|
|
225
|
+
const spy = subscribeSpyTo(eventStore.event(profile.id));
|
|
226
|
+
expect(spy.getValues()).toEqual([undefined]);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
describe("events", () => {
|
|
230
|
+
it("should emit existing events", () => {
|
|
231
|
+
eventStore.add(profile);
|
|
232
|
+
const spy = subscribeSpyTo(eventStore.events([profile.id]));
|
|
233
|
+
expect(spy.getValues()).toEqual([{ [profile.id]: profile }]);
|
|
234
|
+
});
|
|
235
|
+
it("should remove events when they are removed", () => {
|
|
236
|
+
eventStore.add(profile);
|
|
237
|
+
const spy = subscribeSpyTo(eventStore.events([profile.id]));
|
|
238
|
+
expect(spy.getValues()).toEqual([{ [profile.id]: profile }]);
|
|
239
|
+
eventStore.remove(profile);
|
|
240
|
+
expect(spy.getValues()).toEqual([{ [profile.id]: profile }, {}]);
|
|
241
|
+
});
|
|
242
|
+
it("should add events back if then are re-added", () => {
|
|
243
|
+
eventStore.add(profile);
|
|
244
|
+
const spy = subscribeSpyTo(eventStore.events([profile.id]));
|
|
245
|
+
eventStore.remove(profile);
|
|
246
|
+
eventStore.add(profile);
|
|
247
|
+
expect(spy.getValues()).toEqual([{ [profile.id]: profile }, {}, { [profile.id]: profile }]);
|
|
248
|
+
});
|
|
249
|
+
it("should not emit any values if there are no events", () => {
|
|
250
|
+
const spy = subscribeSpyTo(eventStore.events([profile.id]));
|
|
251
|
+
expect(spy.receivedNext()).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
describe("replaceable", () => {
|
|
255
|
+
it("should emit existing events", () => {
|
|
256
|
+
eventStore.add(profile);
|
|
257
|
+
const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
|
|
258
|
+
expect(spy.getValues()).toEqual([profile]);
|
|
259
|
+
});
|
|
260
|
+
it("should emit undefined when event is removed", () => {
|
|
261
|
+
eventStore.add(profile);
|
|
262
|
+
const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
|
|
263
|
+
eventStore.remove(profile);
|
|
264
|
+
expect(spy.getValues()).toEqual([profile, undefined]);
|
|
265
|
+
});
|
|
266
|
+
it("should not complete when event is removed", () => {
|
|
267
|
+
eventStore.add(profile);
|
|
268
|
+
const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
|
|
269
|
+
eventStore.remove(profile);
|
|
270
|
+
expect(spy.receivedComplete()).toBe(false);
|
|
271
|
+
});
|
|
272
|
+
it("should emit event when re-added", () => {
|
|
273
|
+
eventStore.add(profile);
|
|
274
|
+
const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
|
|
275
|
+
eventStore.remove(profile);
|
|
276
|
+
eventStore.add(profile);
|
|
277
|
+
expect(spy.getValues()).toEqual([profile, undefined, profile]);
|
|
278
|
+
});
|
|
279
|
+
it("should claim event", () => {
|
|
280
|
+
eventStore.add(profile);
|
|
281
|
+
eventStore.replaceable(0, user.pubkey).subscribe();
|
|
282
|
+
expect(eventStore.database.isClaimed(profile)).toBe(true);
|
|
283
|
+
});
|
|
284
|
+
it("should remove claim when event is removed", () => {
|
|
285
|
+
eventStore.add(profile);
|
|
286
|
+
eventStore.replaceable(0, user.pubkey).subscribe();
|
|
287
|
+
eventStore.remove(profile);
|
|
288
|
+
expect(eventStore.database.isClaimed(profile)).toBe(false);
|
|
289
|
+
});
|
|
290
|
+
it("should ignore older events added later", () => {
|
|
291
|
+
eventStore.add(profile);
|
|
292
|
+
const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
|
|
293
|
+
eventStore.add(user.profile({ name: "old name" }, { created_at: profile.created_at - 500 }));
|
|
294
|
+
eventStore.add(user.profile({ name: "really old name" }, { created_at: profile.created_at - 1000 }));
|
|
295
|
+
expect(spy.getValues()).toEqual([profile]);
|
|
296
|
+
});
|
|
297
|
+
it("should emit newer events", () => {
|
|
298
|
+
eventStore.add(profile);
|
|
299
|
+
const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
|
|
300
|
+
const newProfile = user.profile({ name: "new name" }, { created_at: profile.created_at + 500 });
|
|
301
|
+
eventStore.add(newProfile);
|
|
302
|
+
expect(spy.getValues()).toEqual([profile, newProfile]);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
describe("timeline", () => {
|
|
306
|
+
it("should emit an empty array if there are not events", () => {
|
|
307
|
+
const spy = subscribeSpyTo(eventStore.timeline({ kinds: [1] }));
|
|
308
|
+
expect(spy.getValues()).toEqual([[]]);
|
|
309
|
+
});
|
|
310
|
+
it("should emit existing events", () => {
|
|
311
|
+
eventStore.add(profile);
|
|
312
|
+
const spy = subscribeSpyTo(eventStore.timeline({ kinds: [0] }));
|
|
313
|
+
expect(spy.getValues()).toEqual([[profile]]);
|
|
314
|
+
});
|
|
315
|
+
it("should emit new events", () => {
|
|
316
|
+
const spy = subscribeSpyTo(eventStore.timeline({ kinds: [0, 1] }));
|
|
317
|
+
eventStore.add(profile);
|
|
318
|
+
eventStore.add(note);
|
|
319
|
+
expect(spy.getValues()).toEqual([[], [profile], [note, profile]]);
|
|
320
|
+
});
|
|
321
|
+
it("should remove event when its removed", () => {
|
|
322
|
+
eventStore.add(profile);
|
|
323
|
+
const spy = subscribeSpyTo(eventStore.timeline({ kinds: [0] }));
|
|
324
|
+
eventStore.remove(profile);
|
|
325
|
+
expect(spy.getValues()).toEqual([[profile], []]);
|
|
326
|
+
});
|
|
327
|
+
it("should not emit when other events are removed", () => {
|
|
328
|
+
eventStore.add(profile);
|
|
329
|
+
const spy = subscribeSpyTo(eventStore.timeline({ kinds: [0] }));
|
|
330
|
+
eventStore.add(note);
|
|
331
|
+
eventStore.remove(note);
|
|
332
|
+
expect(spy.getValues()).toEqual([[profile]]);
|
|
333
|
+
});
|
|
334
|
+
it("should ignore older events added later", () => {
|
|
335
|
+
eventStore.add(profile);
|
|
336
|
+
const spy = subscribeSpyTo(eventStore.timeline({ kinds: [0] }));
|
|
337
|
+
eventStore.add(user.profile({ name: "old-name" }, { created_at: profile.created_at - 1000 }));
|
|
338
|
+
expect(spy.getValues()).toEqual([[profile]]);
|
|
339
|
+
});
|
|
340
|
+
it("should return new array for every value", () => {
|
|
341
|
+
const first = user.note("first note");
|
|
342
|
+
const second = user.note("second note");
|
|
343
|
+
const third = user.note("third note");
|
|
344
|
+
eventStore.add(first);
|
|
345
|
+
const spy = subscribeSpyTo(eventStore.timeline({ kinds: [0] }));
|
|
346
|
+
eventStore.add(second);
|
|
347
|
+
eventStore.add(third);
|
|
348
|
+
const hasDuplicates = (arr) => {
|
|
349
|
+
return new Set(arr).size !== arr.length;
|
|
350
|
+
};
|
|
351
|
+
expect(hasDuplicates(spy.getValues())).toBe(false);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
describe("replaceableSet", () => {
|
|
355
|
+
it("should not emit if there are not events", () => {
|
|
356
|
+
const spy = subscribeSpyTo(eventStore.replaceableSet([{ kind: 0, pubkey: user.pubkey }]));
|
|
357
|
+
expect(spy.receivedNext()).toBe(false);
|
|
358
|
+
});
|
|
359
|
+
it("should emit existing events", () => {
|
|
360
|
+
eventStore.add(profile);
|
|
361
|
+
const spy = subscribeSpyTo(eventStore.replaceableSet([{ kind: 0, pubkey: user.pubkey }]));
|
|
362
|
+
expect(spy.getValues()).toEqual([{ [getEventUID(profile)]: profile }]);
|
|
363
|
+
});
|
|
364
|
+
it("should remove event when removed", () => {
|
|
365
|
+
eventStore.add(profile);
|
|
366
|
+
const spy = subscribeSpyTo(eventStore.replaceableSet([{ kind: 0, pubkey: user.pubkey }]));
|
|
367
|
+
eventStore.remove(profile);
|
|
368
|
+
expect(spy.getValues()).toEqual([{ [getEventUID(profile)]: profile }, {}]);
|
|
369
|
+
});
|
|
370
|
+
it("should replace older events", () => {
|
|
371
|
+
const event2 = { ...profile, created_at: profile.created_at + 100, id: "newer-event" };
|
|
372
|
+
const uid = getEventUID(profile);
|
|
373
|
+
eventStore.add(profile);
|
|
374
|
+
const spy = subscribeSpyTo(eventStore.replaceableSet([{ kind: 0, pubkey: user.pubkey }]));
|
|
375
|
+
eventStore.add(event2);
|
|
376
|
+
expect(spy.getValues()).toEqual([{ [uid]: profile }, { [uid]: event2 }]);
|
|
377
|
+
});
|
|
378
|
+
it("should ignore old events added later", () => {
|
|
379
|
+
const old = user.profile({ name: "old-name" }, { created_at: profile.created_at - 1000 });
|
|
380
|
+
const uid = getEventUID(profile);
|
|
381
|
+
eventStore.add(profile);
|
|
382
|
+
const spy = subscribeSpyTo(eventStore.replaceableSet([{ kind: 0, pubkey: user.pubkey }]));
|
|
383
|
+
eventStore.add(old);
|
|
384
|
+
expect(spy.getValues()).toEqual([{ [uid]: profile }]);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const INDEXABLE_TAGS: Set<string>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { binarySearch, insertEventIntoDescendingList } from "nostr-tools/utils";
|
|
2
2
|
import { Subject } from "rxjs";
|
|
3
3
|
import { getIndexableTags, INDEXABLE_TAGS } from "../helpers/event-tags.js";
|
|
4
|
-
import { createReplaceableAddress,
|
|
4
|
+
import { createReplaceableAddress, isReplaceable } from "../helpers/event.js";
|
|
5
5
|
import { LRU } from "../helpers/lru.js";
|
|
6
6
|
import { logger } from "../logger.js";
|
|
7
7
|
/**
|
|
@@ -92,7 +92,8 @@ export class EventSet {
|
|
|
92
92
|
insertEventIntoDescendingList(this.created_at, event);
|
|
93
93
|
// Insert into replaceable index
|
|
94
94
|
if (isReplaceable(event.kind)) {
|
|
95
|
-
const
|
|
95
|
+
const identifier = event.tags.find((t) => t[0] === "d")?.[1];
|
|
96
|
+
const address = createReplaceableAddress(event.kind, event.pubkey, identifier);
|
|
96
97
|
let array = this.replaceable.get(address);
|
|
97
98
|
if (!this.replaceable.has(address)) {
|
|
98
99
|
// add an empty array if there is no array
|
|
@@ -135,7 +136,8 @@ export class EventSet {
|
|
|
135
136
|
this.events.delete(id);
|
|
136
137
|
// remove from replaceable index
|
|
137
138
|
if (isReplaceable(event.kind)) {
|
|
138
|
-
const
|
|
139
|
+
const identifier = event.tags.find((t) => t[0] === "d")?.[1];
|
|
140
|
+
const address = createReplaceableAddress(event.kind, event.pubkey, identifier);
|
|
139
141
|
const array = this.replaceable.get(address);
|
|
140
142
|
if (array && array.includes(event)) {
|
|
141
143
|
const idx = array.indexOf(event);
|
|
@@ -9,6 +9,8 @@ export declare class EventStore implements IEventStore {
|
|
|
9
9
|
database: EventSet;
|
|
10
10
|
/** Enable this to keep old versions of replaceable events */
|
|
11
11
|
keepOldVersions: boolean;
|
|
12
|
+
/** Enable this to keep expired events */
|
|
13
|
+
keepExpired: boolean;
|
|
12
14
|
/**
|
|
13
15
|
* A method used to verify new events before added them
|
|
14
16
|
* @returns true if the event is valid, false if it should be ignored
|
|
@@ -24,21 +26,29 @@ export declare class EventStore implements IEventStore {
|
|
|
24
26
|
* A method that will be called when an event isn't found in the store
|
|
25
27
|
* @experimental
|
|
26
28
|
*/
|
|
27
|
-
eventLoader?: (pointer: EventPointer) => Observable<NostrEvent>;
|
|
29
|
+
eventLoader?: (pointer: EventPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
|
|
28
30
|
/**
|
|
29
31
|
* A method that will be called when a replaceable event isn't found in the store
|
|
30
32
|
* @experimental
|
|
31
33
|
*/
|
|
32
|
-
replaceableLoader?: (pointer: AddressPointerWithoutD) => Observable<NostrEvent>;
|
|
34
|
+
replaceableLoader?: (pointer: AddressPointerWithoutD) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
|
|
33
35
|
/**
|
|
34
36
|
* A method that will be called when an addressable event isn't found in the store
|
|
35
37
|
* @experimental
|
|
36
38
|
*/
|
|
37
|
-
addressableLoader?: (pointer: AddressPointer) => Observable<NostrEvent>;
|
|
39
|
+
addressableLoader?: (pointer: AddressPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
|
|
38
40
|
constructor();
|
|
39
41
|
protected deletedIds: Set<string>;
|
|
40
42
|
protected deletedCoords: Map<string, number>;
|
|
41
43
|
protected checkDeleted(event: string | NostrEvent): boolean;
|
|
44
|
+
protected expirations: Map<string, number>;
|
|
45
|
+
/** Adds an event to the expiration map */
|
|
46
|
+
protected addExpiration(event: NostrEvent): void;
|
|
47
|
+
protected expirationTimeout: number | null;
|
|
48
|
+
protected nextExpirationCheck: number | null;
|
|
49
|
+
protected handleExpiringEvent(event: NostrEvent): void;
|
|
50
|
+
/** Remove expired events from the store */
|
|
51
|
+
protected pruneExpired(): void;
|
|
42
52
|
protected handleDeleteEvent(deleteEvent: NostrEvent): void;
|
|
43
53
|
/** Copies important metadata from and identical event to another */
|
|
44
54
|
static mergeDuplicateEvent(source: NostrEvent, dest: NostrEvent): void;
|
|
@@ -3,10 +3,12 @@ import { isAddressableKind } from "nostr-tools/kinds";
|
|
|
3
3
|
import { EMPTY, filter, finalize, from, merge, mergeMap, ReplaySubject, share, take, timer } from "rxjs";
|
|
4
4
|
import hash_sum from "hash-sum";
|
|
5
5
|
import { getDeleteCoordinates, getDeleteIds } from "../helpers/delete.js";
|
|
6
|
-
import { EventStoreSymbol, FromCacheSymbol,
|
|
6
|
+
import { createReplaceableAddress, EventStoreSymbol, FromCacheSymbol, isReplaceable } from "../helpers/event.js";
|
|
7
|
+
import { getExpirationTimestamp } from "../helpers/expiration.js";
|
|
7
8
|
import { matchFilters } from "../helpers/filter.js";
|
|
8
9
|
import { parseCoordinate } from "../helpers/pointers.js";
|
|
9
10
|
import { addSeenRelay, getSeenRelays } from "../helpers/relays.js";
|
|
11
|
+
import { unixNow } from "../helpers/time.js";
|
|
10
12
|
import { UserBlossomServersModel } from "../models/blossom.js";
|
|
11
13
|
import { EventModel, EventsModel, ReplaceableModel, ReplaceableSetModel, TimelineModel } from "../models/common.js";
|
|
12
14
|
import { ContactsModel } from "../models/contacts.js";
|
|
@@ -21,6 +23,8 @@ export class EventStore {
|
|
|
21
23
|
database;
|
|
22
24
|
/** Enable this to keep old versions of replaceable events */
|
|
23
25
|
keepOldVersions = false;
|
|
26
|
+
/** Enable this to keep expired events */
|
|
27
|
+
keepExpired = false;
|
|
24
28
|
/**
|
|
25
29
|
* A method used to verify new events before added them
|
|
26
30
|
* @returns true if the event is valid, false if it should be ignored
|
|
@@ -79,12 +83,53 @@ export class EventStore {
|
|
|
79
83
|
if (this.deletedIds.has(event.id))
|
|
80
84
|
return true;
|
|
81
85
|
if (isAddressableKind(event.kind)) {
|
|
82
|
-
const
|
|
86
|
+
const identifier = event.tags.find((t) => t[0] === "d")?.[1];
|
|
87
|
+
const deleted = this.deletedCoords.get(createReplaceableAddress(event.kind, event.pubkey, identifier));
|
|
83
88
|
if (deleted)
|
|
84
89
|
return deleted > event.created_at;
|
|
85
90
|
}
|
|
86
|
-
return false;
|
|
87
91
|
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
expirations = new Map();
|
|
95
|
+
/** Adds an event to the expiration map */
|
|
96
|
+
addExpiration(event) {
|
|
97
|
+
const expiration = getExpirationTimestamp(event);
|
|
98
|
+
if (expiration && Number.isFinite(expiration))
|
|
99
|
+
this.expirations.set(event.id, expiration);
|
|
100
|
+
}
|
|
101
|
+
expirationTimeout = null;
|
|
102
|
+
nextExpirationCheck = null;
|
|
103
|
+
handleExpiringEvent(event) {
|
|
104
|
+
const expiration = getExpirationTimestamp(event);
|
|
105
|
+
if (!expiration)
|
|
106
|
+
return;
|
|
107
|
+
// Add event to expiration map
|
|
108
|
+
this.expirations.set(event.id, expiration);
|
|
109
|
+
// Exit if the next check is already less than the next expiration
|
|
110
|
+
if (this.expirationTimeout && this.nextExpirationCheck && this.nextExpirationCheck < expiration)
|
|
111
|
+
return;
|
|
112
|
+
// Set timeout to prune expired events
|
|
113
|
+
if (this.expirationTimeout)
|
|
114
|
+
clearTimeout(this.expirationTimeout);
|
|
115
|
+
const timeout = expiration - unixNow();
|
|
116
|
+
this.expirationTimeout = setTimeout(this.pruneExpired.bind(this), timeout * 1000 + 10);
|
|
117
|
+
this.nextExpirationCheck = expiration;
|
|
118
|
+
}
|
|
119
|
+
/** Remove expired events from the store */
|
|
120
|
+
pruneExpired() {
|
|
121
|
+
const now = unixNow();
|
|
122
|
+
for (const [id, expiration] of this.expirations) {
|
|
123
|
+
if (expiration <= now) {
|
|
124
|
+
this.expirations.delete(id);
|
|
125
|
+
this.remove(id);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Cleanup timers
|
|
129
|
+
if (this.expirationTimeout)
|
|
130
|
+
clearTimeout(this.expirationTimeout);
|
|
131
|
+
this.nextExpirationCheck = null;
|
|
132
|
+
this.expirationTimeout = null;
|
|
88
133
|
}
|
|
89
134
|
// handling delete events
|
|
90
135
|
handleDeleteEvent(deleteEvent) {
|
|
@@ -133,11 +178,15 @@ export class EventStore {
|
|
|
133
178
|
// Ignore if the event was deleted
|
|
134
179
|
if (this.checkDeleted(event))
|
|
135
180
|
return event;
|
|
181
|
+
// Reject expired events if keepExpired is false
|
|
182
|
+
const expiration = getExpirationTimestamp(event);
|
|
183
|
+
if (this.keepExpired === false && expiration && expiration <= unixNow())
|
|
184
|
+
return null;
|
|
136
185
|
// Get the replaceable identifier
|
|
137
|
-
const
|
|
186
|
+
const identifier = isReplaceable(event.kind) ? event.tags.find((t) => t[0] === "d")?.[1] : undefined;
|
|
138
187
|
// Don't insert the event if there is already a newer version
|
|
139
188
|
if (!this.keepOldVersions && isReplaceable(event.kind)) {
|
|
140
|
-
const existing = this.database.getReplaceableHistory(event.kind, event.pubkey,
|
|
189
|
+
const existing = this.database.getReplaceableHistory(event.kind, event.pubkey, identifier);
|
|
141
190
|
// If there is already a newer version, copy cached symbols and return existing event
|
|
142
191
|
if (existing && existing.length > 0 && existing[0].created_at >= event.created_at) {
|
|
143
192
|
EventStore.mergeDuplicateEvent(event, existing[0]);
|
|
@@ -165,7 +214,7 @@ export class EventStore {
|
|
|
165
214
|
addSeenRelay(inserted, fromRelay);
|
|
166
215
|
// remove all old version of the replaceable event
|
|
167
216
|
if (!this.keepOldVersions && isReplaceable(event.kind)) {
|
|
168
|
-
const existing = this.database.getReplaceableHistory(event.kind, event.pubkey,
|
|
217
|
+
const existing = this.database.getReplaceableHistory(event.kind, event.pubkey, identifier);
|
|
169
218
|
if (existing) {
|
|
170
219
|
const older = Array.from(existing).filter((e) => e.created_at < event.created_at);
|
|
171
220
|
for (const old of older)
|
|
@@ -176,6 +225,9 @@ export class EventStore {
|
|
|
176
225
|
return existing[0];
|
|
177
226
|
}
|
|
178
227
|
}
|
|
228
|
+
// Add event to expiration map
|
|
229
|
+
if (this.keepExpired === false && expiration)
|
|
230
|
+
this.handleExpiringEvent(inserted);
|
|
179
231
|
return inserted;
|
|
180
232
|
}
|
|
181
233
|
/** Removes an event from the database and updates subscriptions */
|