applesauce-core 0.0.0-next-20250212001252 → 0.0.0-next-20250213170222

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.
Files changed (40) hide show
  1. package/dist/event-store/__tests__/event-store.test.js +252 -0
  2. package/dist/event-store/database.d.ts +4 -4
  3. package/dist/event-store/database.js +9 -6
  4. package/dist/event-store/event-store.d.ts +19 -10
  5. package/dist/event-store/event-store.js +211 -311
  6. package/dist/helpers/{blossom.test.js → __tests__/blossom.test.js} +1 -1
  7. package/dist/helpers/{event.test.js → __tests__/event.test.js} +1 -1
  8. package/dist/helpers/{file-metadata.test.js → __tests__/file-metadata.test.js} +1 -1
  9. package/dist/helpers/{hidden-tags.test.js → __tests__/hidden-tags.test.js} +2 -2
  10. package/dist/helpers/{mailboxes.test.js → __tests__/mailboxes.test.js} +1 -1
  11. package/dist/helpers/{tags.test.js → __tests__/tags.test.js} +2 -2
  12. package/dist/helpers/{threading.test.js → __tests__/threading.test.js} +1 -1
  13. package/dist/helpers/relays.js +2 -1
  14. package/dist/observable/__tests__/claim-events.test.js +23 -0
  15. package/dist/observable/__tests__/claim-latest.test.d.ts +1 -0
  16. package/dist/observable/__tests__/claim-latest.test.js +37 -0
  17. package/dist/observable/__tests__/simple-timeout.test.d.ts +1 -0
  18. package/dist/observable/{simple-timeout.test.js → __tests__/simple-timeout.test.js} +1 -1
  19. package/dist/observable/claim-events.d.ts +5 -0
  20. package/dist/observable/claim-events.js +20 -0
  21. package/dist/observable/claim-latest.d.ts +4 -0
  22. package/dist/observable/claim-latest.js +20 -0
  23. package/dist/observable/share-latest-value.d.ts +2 -4
  24. package/dist/observable/share-latest-value.js +19 -16
  25. package/dist/queries/channels.js +3 -3
  26. package/dist/queries/contacts.js +1 -1
  27. package/dist/queries/thread.js +1 -1
  28. package/dist/query-store/query-store.d.ts +0 -2
  29. package/dist/query-store/query-store.js +5 -5
  30. package/package.json +2 -1
  31. package/dist/event-store/event-store.test.js +0 -74
  32. /package/dist/event-store/{event-store.test.d.ts → __tests__/event-store.test.d.ts} +0 -0
  33. /package/dist/helpers/{blossom.test.d.ts → __tests__/blossom.test.d.ts} +0 -0
  34. /package/dist/helpers/{event.test.d.ts → __tests__/event.test.d.ts} +0 -0
  35. /package/dist/helpers/{file-metadata.test.d.ts → __tests__/file-metadata.test.d.ts} +0 -0
  36. /package/dist/helpers/{hidden-tags.test.d.ts → __tests__/hidden-tags.test.d.ts} +0 -0
  37. /package/dist/helpers/{mailboxes.test.d.ts → __tests__/mailboxes.test.d.ts} +0 -0
  38. /package/dist/helpers/{tags.test.d.ts → __tests__/tags.test.d.ts} +0 -0
  39. /package/dist/helpers/{threading.test.d.ts → __tests__/threading.test.d.ts} +0 -0
  40. /package/dist/observable/{simple-timeout.test.d.ts → __tests__/claim-events.test.d.ts} +0 -0
@@ -0,0 +1,252 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { kinds } from "nostr-tools";
3
+ import { subscribeSpyTo } from "@hirez_io/observer-spy";
4
+ import { EventStore } from "../event-store.js";
5
+ import { addSeenRelay, getSeenRelays } from "../../helpers/relays.js";
6
+ import { getEventUID } from "../../helpers/event.js";
7
+ let eventStore;
8
+ beforeEach(() => {
9
+ eventStore = new EventStore();
10
+ });
11
+ const event = {
12
+ 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":""}',
13
+ created_at: 1738362529,
14
+ id: "e9df8d5898c4ccfbd21fcd59f3f48abb3ff0ab7259b19570e2f1756de1e9306b",
15
+ kind: 0,
16
+ pubkey: "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5",
17
+ sig: "465a47b93626a587bf81dadc2b306b8f713a62db31d6ce1533198e9ae1e665a6eaf376a03250bf9ffbb02eb9059c8eafbd37ae1092d05d215757575bd8357586",
18
+ tags: [],
19
+ };
20
+ const kind1 = {
21
+ content: "Except for Substack, what is the simplest way to make a mailing list that can also be browsed on the web?\n\nNo one is doing the Nostr Weekly Report, so maybe I'll start it.",
22
+ created_at: 1739384412,
23
+ id: "00006c0cbf210d920242ab472337e690f21db94116bd905259eebfad5ade7232",
24
+ kind: 1,
25
+ pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
26
+ sig: "6ebd9c34fcdad8ced3ddd9e6eef6e0d0fa77d15d86fe02af33b48dc79714f0f92d3eec77f300ccc8f237217838f9d802bbd65ac7f477705d236dbd99a45c6c34",
27
+ tags: [["nonce", "13835058055282193107", "16"]],
28
+ };
29
+ describe("add", () => {
30
+ it("should return original event in case of duplicates", () => {
31
+ const a = { ...event };
32
+ expect(eventStore.add(a)).toBe(a);
33
+ const b = { ...event };
34
+ expect(eventStore.add(b)).toBe(a);
35
+ const c = { ...event };
36
+ expect(eventStore.add(c)).toBe(a);
37
+ });
38
+ it("should merge seen relays on duplicate events", () => {
39
+ const a = { ...event };
40
+ addSeenRelay(a, "wss://relay.a.com");
41
+ eventStore.add(a);
42
+ const b = { ...event };
43
+ addSeenRelay(b, "wss://relay.b.com");
44
+ eventStore.add(b);
45
+ expect(eventStore.getEvent(event.id)).toBeDefined();
46
+ expect([...getSeenRelays(eventStore.getEvent(event.id))]).toEqual(expect.arrayContaining(["wss://relay.a.com", "wss://relay.b.com"]));
47
+ });
48
+ it("should ignore deleted events", () => {
49
+ const deleteEvent = {
50
+ id: "delete event id",
51
+ kind: kinds.EventDeletion,
52
+ created_at: event.created_at + 100,
53
+ pubkey: event.pubkey,
54
+ tags: [["e", event.id]],
55
+ sig: "this should be ignored for the test",
56
+ content: "test",
57
+ };
58
+ // add delete event first
59
+ eventStore.add(deleteEvent);
60
+ // now event should be ignored
61
+ eventStore.add(event);
62
+ expect(eventStore.getEvent(event.id)).toBeUndefined();
63
+ });
64
+ });
65
+ describe("verifyEvent", () => {
66
+ it("should be called for all events added", () => {
67
+ const verifyEvent = vi.fn().mockReturnValue(true);
68
+ eventStore.verifyEvent = verifyEvent;
69
+ eventStore.add(event);
70
+ expect(verifyEvent).toHaveBeenCalledWith(event);
71
+ });
72
+ it("should not be called for duplicate events", () => {
73
+ const verifyEvent = vi.fn().mockReturnValue(true);
74
+ eventStore.verifyEvent = verifyEvent;
75
+ const a = { ...event };
76
+ eventStore.add(a);
77
+ expect(verifyEvent).toHaveBeenCalledWith(a);
78
+ const b = { ...event };
79
+ eventStore.add(b);
80
+ expect(verifyEvent).toHaveBeenCalledTimes(1);
81
+ const c = { ...event };
82
+ eventStore.add(c);
83
+ expect(verifyEvent).toHaveBeenCalledTimes(1);
84
+ });
85
+ });
86
+ describe("deleted", () => {
87
+ it("should complete when event is removed", () => {
88
+ eventStore.add(event);
89
+ const spy = subscribeSpyTo(eventStore.removed(event.id));
90
+ eventStore.remove(event);
91
+ expect(spy.getValues()).toEqual([]);
92
+ expect(spy.receivedComplete()).toBe(true);
93
+ });
94
+ });
95
+ describe("event", () => {
96
+ it("should emit existing event", () => {
97
+ eventStore.add(event);
98
+ const spy = subscribeSpyTo(eventStore.event(event.id));
99
+ expect(spy.getValues()).toEqual([event]);
100
+ });
101
+ it("should emit then event when its added", () => {
102
+ const spy = subscribeSpyTo(eventStore.event(event.id));
103
+ expect(spy.getValues()).toEqual([]);
104
+ eventStore.add(event);
105
+ expect(spy.getValues()).toEqual([event]);
106
+ });
107
+ it("should emit undefined when event is removed", () => {
108
+ eventStore.add(event);
109
+ const spy = subscribeSpyTo(eventStore.event(event.id));
110
+ expect(spy.getValues()).toEqual([event]);
111
+ eventStore.remove(event);
112
+ expect(spy.getValues()).toEqual([event, undefined]);
113
+ });
114
+ it("should emit new value if event is re-added", () => {
115
+ eventStore.add(event);
116
+ const spy = subscribeSpyTo(eventStore.event(event.id));
117
+ eventStore.remove(event);
118
+ eventStore.add(event);
119
+ expect(spy.getValues()).toEqual([event, undefined, event]);
120
+ });
121
+ it("should not complete when event is removed", () => {
122
+ eventStore.add(event);
123
+ const spy = subscribeSpyTo(eventStore.event(event.id));
124
+ eventStore.remove(event);
125
+ expect(spy.receivedComplete()).toBe(false);
126
+ });
127
+ it("should not emit any values if there are no events", () => {
128
+ const spy = subscribeSpyTo(eventStore.event(event.id));
129
+ expect(spy.receivedNext()).toBe(false);
130
+ });
131
+ });
132
+ describe("events", () => {
133
+ it("should emit existing events", () => {
134
+ eventStore.add(event);
135
+ const spy = subscribeSpyTo(eventStore.events([event.id]));
136
+ expect(spy.getValues()).toEqual([{ [event.id]: event }]);
137
+ });
138
+ it("should remove events when they are removed", () => {
139
+ eventStore.add(event);
140
+ const spy = subscribeSpyTo(eventStore.events([event.id]));
141
+ expect(spy.getValues()).toEqual([{ [event.id]: event }]);
142
+ eventStore.remove(event);
143
+ expect(spy.getValues()).toEqual([{ [event.id]: event }, {}]);
144
+ });
145
+ it("should add events back if then are re-added", () => {
146
+ eventStore.add(event);
147
+ const spy = subscribeSpyTo(eventStore.events([event.id]));
148
+ eventStore.remove(event);
149
+ eventStore.add(event);
150
+ expect(spy.getValues()).toEqual([{ [event.id]: event }, {}, { [event.id]: event }]);
151
+ });
152
+ it("should not emit any values if there are no events", () => {
153
+ const spy = subscribeSpyTo(eventStore.events([event.id]));
154
+ expect(spy.receivedNext()).toBe(false);
155
+ });
156
+ });
157
+ describe("replaceable", () => {
158
+ it("should not emit till there is an event", () => {
159
+ const spy = subscribeSpyTo(eventStore.replaceable(0, event.pubkey));
160
+ expect(spy.receivedNext()).toBe(false);
161
+ });
162
+ it("should emit existing events", () => {
163
+ eventStore.add(event);
164
+ const spy = subscribeSpyTo(eventStore.replaceable(0, event.pubkey));
165
+ expect(spy.getValues()).toEqual([event]);
166
+ });
167
+ it("should emit undefined when event is removed", () => {
168
+ eventStore.add(event);
169
+ const spy = subscribeSpyTo(eventStore.replaceable(0, event.pubkey));
170
+ eventStore.remove(event);
171
+ expect(spy.getValues()).toEqual([event, undefined]);
172
+ });
173
+ it("should not complete when event is removed", () => {
174
+ eventStore.add(event);
175
+ const spy = subscribeSpyTo(eventStore.replaceable(0, event.pubkey));
176
+ eventStore.remove(event);
177
+ expect(spy.receivedComplete()).toBe(false);
178
+ });
179
+ it("should emit event when re-added", () => {
180
+ eventStore.add(event);
181
+ const spy = subscribeSpyTo(eventStore.replaceable(0, event.pubkey));
182
+ eventStore.remove(event);
183
+ eventStore.add(event);
184
+ expect(spy.getValues()).toEqual([event, undefined, event]);
185
+ });
186
+ it("should claim event", () => {
187
+ eventStore.add(event);
188
+ eventStore.replaceable(0, event.pubkey).subscribe();
189
+ expect(eventStore.database.isClaimed(event)).toBe(true);
190
+ });
191
+ it("should remove claim when event is removed", () => {
192
+ eventStore.add(event);
193
+ eventStore.replaceable(0, event.pubkey).subscribe();
194
+ eventStore.remove(event);
195
+ expect(eventStore.database.isClaimed(event)).toBe(false);
196
+ });
197
+ });
198
+ describe("timeline", () => {
199
+ it("should not emit if there are not events", () => {
200
+ const spy = subscribeSpyTo(eventStore.timeline({ kinds: [1] }));
201
+ expect(spy.receivedNext()).toBe(false);
202
+ });
203
+ it("should emit existing events", () => {
204
+ eventStore.add(event);
205
+ const spy = subscribeSpyTo(eventStore.timeline({ kinds: [0] }));
206
+ expect(spy.getValues()).toEqual([[event]]);
207
+ });
208
+ it("should emit new events", () => {
209
+ const spy = subscribeSpyTo(eventStore.timeline({ kinds: [0, 1] }));
210
+ eventStore.add(event);
211
+ eventStore.add(kind1);
212
+ expect(spy.getValues()).toEqual([[event], [kind1, event]]);
213
+ });
214
+ it("should remove event when its removed", () => {
215
+ eventStore.add(event);
216
+ const spy = subscribeSpyTo(eventStore.timeline({ kinds: [0] }));
217
+ eventStore.remove(event);
218
+ expect(spy.getValues()).toEqual([[event], []]);
219
+ });
220
+ it("should not emit when other events are removed", () => {
221
+ eventStore.add(event);
222
+ const spy = subscribeSpyTo(eventStore.timeline({ kinds: [0] }));
223
+ eventStore.add(kind1);
224
+ eventStore.remove(kind1);
225
+ expect(spy.getValues()).toEqual([[event]]);
226
+ });
227
+ });
228
+ describe("replaceableSet", () => {
229
+ it("should not emit if there are not events", () => {
230
+ const spy = subscribeSpyTo(eventStore.replaceableSet([{ kind: 0, pubkey: event.pubkey }]));
231
+ expect(spy.receivedNext()).toBe(false);
232
+ });
233
+ it("should emit existing events", () => {
234
+ eventStore.add(event);
235
+ const spy = subscribeSpyTo(eventStore.replaceableSet([{ kind: 0, pubkey: event.pubkey }]));
236
+ expect(spy.getValues()).toEqual([{ [getEventUID(event)]: event }]);
237
+ });
238
+ it("should remove event when removed", () => {
239
+ eventStore.add(event);
240
+ const spy = subscribeSpyTo(eventStore.replaceableSet([{ kind: 0, pubkey: event.pubkey }]));
241
+ eventStore.remove(event);
242
+ expect(spy.getValues()).toEqual([{ [getEventUID(event)]: event }, {}]);
243
+ });
244
+ it("should replace older events", () => {
245
+ const event2 = { ...event, created_at: event.created_at + 100, id: "newer-event" };
246
+ const uid = getEventUID(event);
247
+ eventStore.add(event);
248
+ const spy = subscribeSpyTo(eventStore.replaceableSet([{ kind: 0, pubkey: event.pubkey }]));
249
+ eventStore.add(event2);
250
+ expect(spy.getValues()).toEqual([{ [uid]: event }, { [uid]: event2 }]);
251
+ });
252
+ });
@@ -19,8 +19,8 @@ export declare class Database {
19
19
  inserted: Subject<import("nostr-tools").Event>;
20
20
  /** A stream of events that have been updated */
21
21
  updated: Subject<import("nostr-tools").Event>;
22
- /** A stream of events removed of the database */
23
- deleted: Subject<import("nostr-tools").Event>;
22
+ /** A stream of events removed from the database */
23
+ removed: Subject<import("nostr-tools").Event>;
24
24
  /** A method thats called before a new event is inserted */
25
25
  onBeforeInsert?: (event: NostrEvent) => void;
26
26
  get size(): number;
@@ -43,8 +43,8 @@ export declare class Database {
43
43
  addEvent(event: NostrEvent): NostrEvent;
44
44
  /** Inserts and event into the database and notifies all subscriptions that the event has updated */
45
45
  updateEvent(event: NostrEvent): NostrEvent;
46
- /** Deletes an event from the database and notifies all subscriptions */
47
- deleteEvent(eventOrId: string | NostrEvent): boolean;
46
+ /** Removes an event from the database and notifies all subscriptions */
47
+ removeEvent(eventOrId: string | NostrEvent): boolean;
48
48
  /** Sets the claim on the event and touches it */
49
49
  claimEvent(event: NostrEvent, claim: any): void;
50
50
  /** Checks if an event is claimed by anything */
@@ -22,8 +22,8 @@ export class Database {
22
22
  inserted = new Subject();
23
23
  /** A stream of events that have been updated */
24
24
  updated = new Subject();
25
- /** A stream of events removed of the database */
26
- deleted = new Subject();
25
+ /** A stream of events removed from the database */
26
+ removed = new Subject();
27
27
  /** A method thats called before a new event is inserted */
28
28
  onBeforeInsert;
29
29
  get size() {
@@ -119,8 +119,8 @@ export class Database {
119
119
  this.updated.next(inserted);
120
120
  return inserted;
121
121
  }
122
- /** Deletes an event from the database and notifies all subscriptions */
123
- deleteEvent(eventOrId) {
122
+ /** Removes an event from the database and notifies all subscriptions */
123
+ removeEvent(eventOrId) {
124
124
  let event = typeof eventOrId === "string" ? this.events.get(eventOrId) : eventOrId;
125
125
  if (!event)
126
126
  throw new Error("Missing event");
@@ -148,7 +148,10 @@ export class Database {
148
148
  array.splice(idx, 1);
149
149
  }
150
150
  }
151
- this.deleted.next(event);
151
+ // remove any claims this event has
152
+ this.claims.delete(event);
153
+ // notify subscribers this event was removed
154
+ this.removed.next(event);
152
155
  return true;
153
156
  }
154
157
  /** Sets the claim on the event and touches it */
@@ -302,7 +305,7 @@ export class Database {
302
305
  while (cursor) {
303
306
  const event = cursor.value;
304
307
  if (!this.isClaimed(event)) {
305
- this.deleteEvent(event);
308
+ this.removeEvent(event);
306
309
  removed++;
307
310
  if (removed >= limit)
308
311
  break;
@@ -8,6 +8,12 @@ export declare class EventStore {
8
8
  /** A method used to verify new events before added them */
9
9
  verifyEvent?: (event: NostrEvent) => boolean;
10
10
  constructor();
11
+ protected deletedIds: Set<string>;
12
+ protected deletedCoords: Map<string, number>;
13
+ protected checkDeleted(event: string | NostrEvent): boolean;
14
+ protected handleDeleteEvent(deleteEvent: NostrEvent): void;
15
+ /** Copies important metadata from and identical event to another */
16
+ static mergeDuplicateEvent(source: NostrEvent, dest: NostrEvent): void;
11
17
  /**
12
18
  * Adds an event to the database and update subscriptions
13
19
  * @throws
@@ -15,22 +21,31 @@ export declare class EventStore {
15
21
  add(event: NostrEvent, fromRelay?: string): NostrEvent;
16
22
  /** Removes an event from the database and updates subscriptions */
17
23
  remove(event: string | NostrEvent): boolean;
18
- protected deletedIds: Set<string>;
19
- protected deletedCoords: Map<string, number>;
20
- protected handleDeleteEvent(deleteEvent: NostrEvent): void;
21
- protected checkDeleted(event: NostrEvent): boolean;
22
24
  /** Removes any event that is not being used by a subscription */
23
25
  prune(max?: number): number;
24
26
  /** Add an event to the store and notifies all subscribes it has updated */
25
27
  update(event: NostrEvent): NostrEvent;
28
+ /** Get all events matching a filter */
26
29
  getAll(filters: Filter[]): Set<NostrEvent>;
30
+ /** Check if the store has an event */
27
31
  hasEvent(uid: string): boolean;
28
32
  getEvent(uid: string): NostrEvent | undefined;
33
+ /** Check if the store has a replaceable event */
29
34
  hasReplaceable(kind: number, pubkey: string, d?: string): boolean;
30
35
  /** Gets the latest version of a replaceable event */
31
36
  getReplaceable(kind: number, pubkey: string, d?: string): NostrEvent | undefined;
32
37
  /** Returns all versions of a replaceable event */
33
38
  getReplaceableHistory(kind: number, pubkey: string, d?: string): NostrEvent[] | undefined;
39
+ /**
40
+ * Creates an observable that streams all events that match the filter and remains open
41
+ * @param filters
42
+ * @param [onlyNew=false] Only subscribe to new events
43
+ */
44
+ filters(filters: Filter | Filter[], onlyNew?: boolean): Observable<NostrEvent>;
45
+ /** Returns an observable that completes when an event is removed */
46
+ removed(id: string): Observable<never>;
47
+ /** Creates an observable that emits when event is updated */
48
+ updated(id: string): Observable<NostrEvent>;
34
49
  /** Creates an observable that subscribes to a single event */
35
50
  event(id: string): Observable<NostrEvent | undefined>;
36
51
  /** Creates an observable that subscribes to multiple events */
@@ -43,12 +58,6 @@ export declare class EventStore {
43
58
  pubkey: string;
44
59
  identifier?: string;
45
60
  }[]): Observable<Record<string, NostrEvent>>;
46
- /**
47
- * Creates an observable that streams all events that match the filter
48
- * @param filters
49
- * @param [onlyNew=false] Only subscribe to new events
50
- */
51
- stream(filters: Filter | Filter[], onlyNew?: boolean): Observable<NostrEvent>;
52
61
  /** Creates an observable that updates with an array of sorted events */
53
62
  timeline(filters: Filter | Filter[], keepOldVersions?: boolean): Observable<NostrEvent[]>;
54
63
  }