@nostrwatch/memory-relay 1.3.0
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/LICENSE +21 -0
- package/README.md +275 -0
- package/dist/abstract.d.ts +38 -0
- package/dist/esm/abstract.mjs +7 -0
- package/dist/esm/abstract.mjs.map +7 -0
- package/dist/esm/chunk-QC3J2K4S.mjs +225 -0
- package/dist/esm/chunk-QC3J2K4S.mjs.map +7 -0
- package/dist/esm/chunk-WJKCFUCB.mjs +104 -0
- package/dist/esm/chunk-WJKCFUCB.mjs.map +7 -0
- package/dist/esm/index.mjs +21 -0
- package/dist/esm/index.mjs.map +7 -0
- package/dist/esm/svelte.mjs +8 -0
- package/dist/esm/svelte.mjs.map +7 -0
- package/dist/index.d.ts +4 -0
- package/dist/svelte.d.ts +20 -0
- package/dist/types.d.ts +17 -0
- package/dist/utils.d.ts +8 -0
- package/package.json +54 -0
- package/src/abstract.ts +187 -0
- package/src/index.ts +4 -0
- package/src/svelte.ts +118 -0
- package/src/types.ts +23 -0
- package/src/utils.ts +88 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Sandwich Farm LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# @nostrwatch/memory-relay
|
|
2
|
+
|
|
3
|
+
In-memory Nostr relay implementation for testing and SvelteKit application state.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@nostrwatch/memory-relay)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://github.com/sandwichfarm/nostr-watch)
|
|
8
|
+
[](https://github.com/sandwichfarm/nostr-watch)
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
`@nostrwatch/memory-relay` provides an in-memory store for Nostr events (signed JSON objects that form the protocol's fundamental data unit) without any network layer or disk persistence. It is used in tests to simulate relay behavior and in SvelteKit applications to hold reactive event collections without a backend relay connection.
|
|
13
|
+
|
|
14
|
+
The package exports two classes: `AbstractMemoryRelay` — a generic base class with filter-aware query, insert, count, and delete operations — and `SvelteMemoryRelay`, which extends `AbstractMemoryRelay` to back all event storage with a Svelte `Writable` store, making the event collection reactive and compatible with Svelte's `$` syntax and `derived` stores.
|
|
15
|
+
|
|
16
|
+
A **relay** in Nostr is a WebSocket server that stores and forwards events. This package emulates the data model of a relay in memory without the network transport.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
pnpm add @nostrwatch/memory-relay
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or with npm:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npm install @nostrwatch/memory-relay
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import {AbstractMemoryRelay} from '@nostrwatch/memory-relay'
|
|
34
|
+
import type {Filter} from 'nostr-tools'
|
|
35
|
+
|
|
36
|
+
class MyRelay extends AbstractMemoryRelay {
|
|
37
|
+
async init(): Promise<void> {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const relay = new MyRelay()
|
|
41
|
+
|
|
42
|
+
// Insert a Nostr event
|
|
43
|
+
relay.event({id: 'abc123', kind: 1, created_at: 1700000000, content: 'hello', tags: [], pubkey: 'pub1'})
|
|
44
|
+
|
|
45
|
+
// Query with a filter
|
|
46
|
+
const results = relay.req('sub1', [{kinds: [1]}])
|
|
47
|
+
console.log(relay.count([{kinds: [1]}]))
|
|
48
|
+
// 1
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## API
|
|
52
|
+
|
|
53
|
+
### `AbstractMemoryRelay`
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
abstract class AbstractMemoryRelay<
|
|
57
|
+
Input extends BaseEvent = any,
|
|
58
|
+
Output extends BaseEvent = any,
|
|
59
|
+
OutputSingle = Output,
|
|
60
|
+
OutputCollection = any,
|
|
61
|
+
OutputCount = any
|
|
62
|
+
>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Abstract base class for in-memory event storage. Extend this class and implement `init()` to create a concrete relay. Subclasses may override `formatCollection()` to shape query results, and register `qualify` and `instantiate` callbacks to control which events are stored and how they are transformed on insert.
|
|
66
|
+
|
|
67
|
+
#### `abstract init(): Promise<void>`
|
|
68
|
+
|
|
69
|
+
Called once before the relay is used. Implement to perform any async setup (e.g. loading seed events from storage). Must return a resolved promise if no setup is needed.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
#### Event writes
|
|
74
|
+
|
|
75
|
+
**`event(ev: Input): boolean`**
|
|
76
|
+
|
|
77
|
+
Inserts a single event. Returns `true` if the event was inserted, `false` if it was rejected (by the qualify callback or because a newer replaceable event already exists).
|
|
78
|
+
|
|
79
|
+
**`eventBatch(evs: Input[]): number`**
|
|
80
|
+
|
|
81
|
+
Inserts multiple events. Returns the count of events actually inserted.
|
|
82
|
+
|
|
83
|
+
**`maybeInsert(event: Input): number`**
|
|
84
|
+
|
|
85
|
+
Lower-level insert: checks `shouldInsert()`, then stores the event. Returns `1` if inserted, `0` otherwise.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
#### Event reads
|
|
90
|
+
|
|
91
|
+
**`req(id: string, filters: Filter[], format?: boolean): OutputCollection | Output[]`**
|
|
92
|
+
|
|
93
|
+
Queries the store against one or more Nostr filters (a filter is a JSON object with fields like `kinds`, `authors`, `since`, `until`, `#e`, etc.). Returns all matching events. When `format` is `true` (default), passes results through `formatCollection()`.
|
|
94
|
+
|
|
95
|
+
**`get(key: string): Output | undefined`**
|
|
96
|
+
|
|
97
|
+
Returns a single event by its internal storage key (event ID or `kind:pubkey` for replaceable events).
|
|
98
|
+
|
|
99
|
+
**`count(filters: Filter[]): OutputCount`**
|
|
100
|
+
|
|
101
|
+
Returns the number of events that match the given filters.
|
|
102
|
+
|
|
103
|
+
**`summary(): Record<string, number>`**
|
|
104
|
+
|
|
105
|
+
Returns a map of `{ kind: count }` for all stored events. Useful for debugging and test assertions.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
#### Event deletes
|
|
110
|
+
|
|
111
|
+
**`delete(filters: Filter[]): string[]`**
|
|
112
|
+
|
|
113
|
+
Deletes all events matching the filters. Returns the keys of deleted events.
|
|
114
|
+
|
|
115
|
+
**`wipe(): Promise<void>`**
|
|
116
|
+
|
|
117
|
+
Clears all stored events.
|
|
118
|
+
|
|
119
|
+
**`destroy(): void`**
|
|
120
|
+
|
|
121
|
+
Calls `wipe()` and releases resources.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
#### Existence checks
|
|
126
|
+
|
|
127
|
+
**`exists(note: Input | string): boolean`**
|
|
128
|
+
|
|
129
|
+
Returns `true` if the given event (or event ID string) is present in the store.
|
|
130
|
+
|
|
131
|
+
**`noteExists(note: Input): boolean`**
|
|
132
|
+
|
|
133
|
+
Returns `true` if the given event object is in the store.
|
|
134
|
+
|
|
135
|
+
**`idExists(id: string): boolean`**
|
|
136
|
+
|
|
137
|
+
Returns `true` if the given event ID string is in the store.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
#### Lifecycle callbacks
|
|
142
|
+
|
|
143
|
+
**`on(event: 'qualify' | 'instantiate', listener): void`**
|
|
144
|
+
|
|
145
|
+
Registers a callback for event lifecycle hooks:
|
|
146
|
+
|
|
147
|
+
- `qualify(event, key, relay)` — return `false` to reject an event before insert; `true` to accept
|
|
148
|
+
- `instantiate(event, key, relay)` — transform an input event into the stored output type; return the transformed event
|
|
149
|
+
|
|
150
|
+
**`off(event: 'qualify' | 'instantiate'): void`**
|
|
151
|
+
|
|
152
|
+
Removes the registered callback for the given lifecycle hook.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
#### Serialization
|
|
157
|
+
|
|
158
|
+
**`dump(): Promise<Uint8Array>`**
|
|
159
|
+
|
|
160
|
+
Serializes all stored events to a UTF-8 JSON byte array. Useful for snapshotting relay state in tests.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
#### Properties
|
|
165
|
+
|
|
166
|
+
| Property | Type | Description |
|
|
167
|
+
|----------|------|-------------|
|
|
168
|
+
| `events` | `Map<string, Output>` | Direct access to the event store |
|
|
169
|
+
| `nip11s` | `Map<string, any>` | NIP-11 info document cache keyed by relay URL |
|
|
170
|
+
| `highestTimestamp` | `number[]` | Per-filter highest `created_at` seen during `req()` |
|
|
171
|
+
| `lowestTimestamp` | `number[]` | Per-filter lowest `created_at` seen during `req()` |
|
|
172
|
+
| `debounceMS` | `number` | Debounce interval in milliseconds (default `1000`) |
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
### `SvelteMemoryRelay`
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
class SvelteMemoryRelay<
|
|
180
|
+
InputEvent extends BaseEvent,
|
|
181
|
+
OutputEvent extends BaseEvent,
|
|
182
|
+
OutputSingle extends Readable<OutputEvent> = Readable<OutputEvent>,
|
|
183
|
+
OutputCollection extends Readable<OutputEvent[]> = Readable<OutputEvent[]>,
|
|
184
|
+
OutputCount extends Readable<number> = Readable<number>
|
|
185
|
+
> extends AbstractMemoryRelay<InputEvent, OutputEvent, OutputSingle, OutputCollection, OutputCount>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Extends `AbstractMemoryRelay` to back event storage with a Svelte `Writable<Map<string, OutputEvent>>` store. All mutations (`maybeInsert`, `wipe`, `_delete`) update the store atomically, triggering reactive updates in subscribing components.
|
|
189
|
+
|
|
190
|
+
Import from the `svelte` subpath:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import {SvelteMemoryRelay} from '@nostrwatch/memory-relay/svelte'
|
|
194
|
+
import {writable} from 'svelte/store'
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### Constructor
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
new SvelteMemoryRelay(store: Writable<Map<string, OutputEvent>>)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Pass an existing `Writable` store. The relay reads and writes through the store reference, so external subscribers see changes immediately.
|
|
204
|
+
|
|
205
|
+
**Example (SvelteKit component):**
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import {SvelteMemoryRelay} from '@nostrwatch/memory-relay/svelte'
|
|
209
|
+
import {writable, derived} from 'svelte/store'
|
|
210
|
+
|
|
211
|
+
const eventStore = writable(new Map())
|
|
212
|
+
const relay = new SvelteMemoryRelay(eventStore)
|
|
213
|
+
await relay.init()
|
|
214
|
+
|
|
215
|
+
// Reactive query — updates whenever events are inserted or deleted
|
|
216
|
+
const kind1Events = relay.$req('sub1', [{kinds: [1]}])
|
|
217
|
+
|
|
218
|
+
// In Svelte template: {#each $kind1Events as event}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### Reactive query methods
|
|
222
|
+
|
|
223
|
+
**`$req(id: string, filters: Filter[], format?: boolean): OutputCollection`**
|
|
224
|
+
|
|
225
|
+
Returns a `Readable<OutputEvent[]>` derived from the store. Updates reactively whenever the store changes.
|
|
226
|
+
|
|
227
|
+
**`$get(key: string): OutputSingle | undefined`**
|
|
228
|
+
|
|
229
|
+
Returns a `Readable<OutputEvent>` for a single event by key. Updates reactively.
|
|
230
|
+
|
|
231
|
+
**`$count(filters: Filter[]): OutputCount`**
|
|
232
|
+
|
|
233
|
+
Returns a `Readable<number>` that emits the count of matching events. Updates reactively.
|
|
234
|
+
|
|
235
|
+
#### Properties
|
|
236
|
+
|
|
237
|
+
**`store: Writable<Map<string, OutputEvent>>`**
|
|
238
|
+
|
|
239
|
+
Direct access to the underlying Svelte store. Subscribe to it to react to any event store change.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
### `BaseEvent`
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
interface BaseEvent {
|
|
247
|
+
id?: string
|
|
248
|
+
kind?: number
|
|
249
|
+
created_at?: number
|
|
250
|
+
tags?: string[][]
|
|
251
|
+
pubkey?: string
|
|
252
|
+
content?: string
|
|
253
|
+
sig?: string
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Minimum shape required by the relay internals. Use `nostr-tools`' `Event` type for full Nostr event compliance.
|
|
258
|
+
|
|
259
|
+
## Known Limitations
|
|
260
|
+
|
|
261
|
+
No known limitations at this time.
|
|
262
|
+
|
|
263
|
+
## Agent Skills
|
|
264
|
+
|
|
265
|
+
No agent skills defined yet for this package.
|
|
266
|
+
|
|
267
|
+
## Related Packages
|
|
268
|
+
|
|
269
|
+
- [`@nostrwatch/worker-relay`](../worker-relay/README.md) — persistent web worker relay using sqlite-wasm and OPFS; use this when you need durability across page reloads
|
|
270
|
+
- [`@nostrwatch/websocket`](../websocket/README.md) — cross-platform WebSocket client; pairs with memory-relay in integration tests that simulate a real relay connection
|
|
271
|
+
- [`apps/gui`](../../apps/gui/README.md) — SvelteKit app that uses `SvelteMemoryRelay` for local event caching and reactive UI updates
|
|
272
|
+
|
|
273
|
+
## License
|
|
274
|
+
|
|
275
|
+
[MIT](../../LICENSE)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Filter } from "nostr-tools";
|
|
2
|
+
import { AbstractMemoryRelayCallback } from "./types.js";
|
|
3
|
+
import { BaseEvent } from "./types.js";
|
|
4
|
+
export declare abstract class AbstractMemoryRelay<Input extends BaseEvent = any, Output extends BaseEvent = any, OutputSingle = Output, OutputCollection = any | any[], OutputCount = any> {
|
|
5
|
+
private listeners;
|
|
6
|
+
protected _events: Map<string, Output>;
|
|
7
|
+
protected _nip11s: Map<string, any>;
|
|
8
|
+
highestTimestamp: number[];
|
|
9
|
+
lowestTimestamp: number[];
|
|
10
|
+
debounceMS: number;
|
|
11
|
+
protected set events(e: Map<string, Output>);
|
|
12
|
+
get events(): Map<string, Output>;
|
|
13
|
+
get nip11s(): Map<string, any>;
|
|
14
|
+
abstract init(): Promise<void>;
|
|
15
|
+
on(event: "qualify" | "instantiate", listener: AbstractMemoryRelayCallback<this, Output>): void;
|
|
16
|
+
off(event: "qualify" | "instantiate"): void;
|
|
17
|
+
protected instantiateEvent(event: Input, key: string): Output;
|
|
18
|
+
protected qualifyEvent(event: Input, key: string): boolean;
|
|
19
|
+
exists(note: Input | string): boolean;
|
|
20
|
+
noteExists(note: Input): boolean;
|
|
21
|
+
idExists(id: string): boolean;
|
|
22
|
+
count(filters: Filter[]): OutputCount;
|
|
23
|
+
summary(): Record<string, number>;
|
|
24
|
+
dump(): Promise<Uint8Array>;
|
|
25
|
+
close(): void;
|
|
26
|
+
wipe(): Promise<void>;
|
|
27
|
+
maybeInsert(event: Input): number;
|
|
28
|
+
event(ev: Input): boolean;
|
|
29
|
+
eventBatch(evs: Input[]): number;
|
|
30
|
+
setTimestampRange(event: Input | Output | BaseEvent, index?: number): void;
|
|
31
|
+
formatCollection(collection: Output[]): OutputCollection;
|
|
32
|
+
req(id: string, filters: Filter[], format?: boolean): OutputCollection | Output[];
|
|
33
|
+
get(key: string): Output | undefined;
|
|
34
|
+
delete(filters: Filter[]): string[];
|
|
35
|
+
_delete(keys: string[]): string[];
|
|
36
|
+
destroy(): void;
|
|
37
|
+
protected shouldInsert(event: Input): boolean;
|
|
38
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// src/utils.ts
|
|
2
|
+
import { isParameterizedReplaceableKind, isReplaceableKind } from "nostr-tools/kinds";
|
|
3
|
+
var eventType = (event) => {
|
|
4
|
+
if (isReplaceableKind(event.kind)) {
|
|
5
|
+
return "replaceable";
|
|
6
|
+
} else if (isParameterizedReplaceableKind(event.kind)) {
|
|
7
|
+
return "parameterized";
|
|
8
|
+
} else {
|
|
9
|
+
return "event";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var eventAddr = (event, type) => {
|
|
13
|
+
let { pubkey, kind } = event;
|
|
14
|
+
type = type ?? eventType(event);
|
|
15
|
+
pubkey = pubkey.slice(0, 16);
|
|
16
|
+
if (type === "parameterized") {
|
|
17
|
+
const dtag = event.tags.find((t) => t[0] === "d")?.[1];
|
|
18
|
+
const key = `${pubkey}:${kind}:${dtag}`;
|
|
19
|
+
return key;
|
|
20
|
+
} else if (type === "replaceable") {
|
|
21
|
+
const key = `${pubkey}:${kind}`;
|
|
22
|
+
return key;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var eventKey = (event, type) => {
|
|
26
|
+
type = type ?? eventType(event);
|
|
27
|
+
if (type === "replaceable") {
|
|
28
|
+
return eventAddr(event, type);
|
|
29
|
+
} else if (type === "parameterized") {
|
|
30
|
+
return eventAddr(event, type);
|
|
31
|
+
} else {
|
|
32
|
+
return event.id;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var eventIdData = (event) => {
|
|
36
|
+
const type = eventType(event);
|
|
37
|
+
return {
|
|
38
|
+
type,
|
|
39
|
+
key: eventKey(event, type)
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
function eventMatchesFilter(ev, filter) {
|
|
43
|
+
if (filter.since && ev.created_at < filter.since) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (filter.until && ev.created_at > filter.until) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (!(filter.ids?.includes(ev.id) ?? true)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (!(filter.authors?.includes(ev.pubkey) ?? true)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (!(filter.kinds?.includes(ev.kind) ?? true)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const orTags = Object.entries(filter).filter(([k]) => k.startsWith("#"));
|
|
59
|
+
for (const [k, v] of orTags) {
|
|
60
|
+
const vargs = v;
|
|
61
|
+
for (const x of vargs) {
|
|
62
|
+
if (!ev.tags.find((a) => a[0] === k.slice(1) && a[1] === x)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const andTags = Object.entries(filter).filter(([k]) => k.startsWith("&"));
|
|
68
|
+
for (const [k, v] of andTags) {
|
|
69
|
+
const vargs = v;
|
|
70
|
+
const allMatch = vargs.every((x) => ev.tags.some((tag) => tag[0] === k.slice(1) && tag[1] === x));
|
|
71
|
+
if (!allMatch) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/abstract.ts
|
|
79
|
+
var AbstractMemoryRelay = class {
|
|
80
|
+
constructor() {
|
|
81
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
82
|
+
this._events = /* @__PURE__ */ new Map();
|
|
83
|
+
this._nip11s = /* @__PURE__ */ new Map();
|
|
84
|
+
this.highestTimestamp = new Array(0);
|
|
85
|
+
this.lowestTimestamp = new Array(0);
|
|
86
|
+
this.debounceMS = 1e3;
|
|
87
|
+
}
|
|
88
|
+
set events(e) {
|
|
89
|
+
this._events = e;
|
|
90
|
+
}
|
|
91
|
+
get events() {
|
|
92
|
+
return this._events;
|
|
93
|
+
}
|
|
94
|
+
get nip11s() {
|
|
95
|
+
return this._nip11s;
|
|
96
|
+
}
|
|
97
|
+
on(event, listener) {
|
|
98
|
+
this.listeners.set(event, listener);
|
|
99
|
+
}
|
|
100
|
+
off(event) {
|
|
101
|
+
this.listeners.delete(event);
|
|
102
|
+
}
|
|
103
|
+
instantiateEvent(event, key) {
|
|
104
|
+
return this.listeners.get("instantiate")?.(event, key, this) ?? event;
|
|
105
|
+
}
|
|
106
|
+
qualifyEvent(event, key) {
|
|
107
|
+
return this.listeners.get("qualify")?.(event, key, this) ?? true;
|
|
108
|
+
}
|
|
109
|
+
exists(note) {
|
|
110
|
+
return typeof note === "string" ? this.idExists(note) : this.noteExists(note);
|
|
111
|
+
}
|
|
112
|
+
noteExists(note) {
|
|
113
|
+
return this.events.has(eventKey(note));
|
|
114
|
+
}
|
|
115
|
+
idExists(id) {
|
|
116
|
+
return this.events.has(id);
|
|
117
|
+
}
|
|
118
|
+
count(filters) {
|
|
119
|
+
let ret = 0;
|
|
120
|
+
for (const [, e] of this.events) {
|
|
121
|
+
for (const filter of filters) {
|
|
122
|
+
if (eventMatchesFilter(e, filter)) {
|
|
123
|
+
ret++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return ret;
|
|
128
|
+
}
|
|
129
|
+
summary() {
|
|
130
|
+
const ret = {};
|
|
131
|
+
for (const [, event] of this.events) {
|
|
132
|
+
if (!event.kind) continue;
|
|
133
|
+
ret[event.kind.toString()] ??= 0;
|
|
134
|
+
ret[event.kind.toString()]++;
|
|
135
|
+
}
|
|
136
|
+
return ret;
|
|
137
|
+
}
|
|
138
|
+
async dump() {
|
|
139
|
+
const enc = new TextEncoder();
|
|
140
|
+
return enc.encode(JSON.stringify(Array.from(this.events.values())));
|
|
141
|
+
}
|
|
142
|
+
close() {
|
|
143
|
+
}
|
|
144
|
+
async wipe() {
|
|
145
|
+
this.events = /* @__PURE__ */ new Map();
|
|
146
|
+
}
|
|
147
|
+
maybeInsert(event) {
|
|
148
|
+
if (!this.shouldInsert(event)) return 0;
|
|
149
|
+
const key = eventKey(event);
|
|
150
|
+
this.events.set(key, this.instantiateEvent(event, key));
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
event(ev) {
|
|
154
|
+
return this.maybeInsert(ev) ? true : false;
|
|
155
|
+
}
|
|
156
|
+
eventBatch(evs) {
|
|
157
|
+
const inserted = [];
|
|
158
|
+
for (const ev of evs) {
|
|
159
|
+
if (ev.tags?.find((tag) => tag[1] === "tor")) {
|
|
160
|
+
console.log("TOR RELAY", ev.id);
|
|
161
|
+
}
|
|
162
|
+
const inserts = this.maybeInsert(ev);
|
|
163
|
+
if (!inserts) continue;
|
|
164
|
+
inserted.push(ev);
|
|
165
|
+
}
|
|
166
|
+
return inserted.length;
|
|
167
|
+
}
|
|
168
|
+
setTimestampRange(event, index = 0) {
|
|
169
|
+
const ts = event.created_at;
|
|
170
|
+
if (!this.highestTimestamp?.[index] || ts > this.highestTimestamp[index]) this.highestTimestamp[index] = ts;
|
|
171
|
+
if (!this.lowestTimestamp?.[index] || ts < this.lowestTimestamp[index]) this.lowestTimestamp[index] = ts;
|
|
172
|
+
}
|
|
173
|
+
formatCollection(collection) {
|
|
174
|
+
return collection;
|
|
175
|
+
}
|
|
176
|
+
req(id, filters, format = true) {
|
|
177
|
+
const ret = [];
|
|
178
|
+
for (const [, e] of this.events) {
|
|
179
|
+
for (const [index, filter] of filters.entries()) {
|
|
180
|
+
if (eventMatchesFilter(e, filter)) {
|
|
181
|
+
ret.push(e);
|
|
182
|
+
this.setTimestampRange(e, index);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return format ? this.formatCollection(ret) : ret;
|
|
187
|
+
}
|
|
188
|
+
get(key) {
|
|
189
|
+
return this.events.get(key);
|
|
190
|
+
}
|
|
191
|
+
delete(filters) {
|
|
192
|
+
const forDelete = this.req("ids-for-delete", filters, false);
|
|
193
|
+
const keys = forDelete.map((e) => eventKey(e)).filter((a) => typeof a === "string");
|
|
194
|
+
if (!keys?.length) return [];
|
|
195
|
+
return this._delete(keys);
|
|
196
|
+
}
|
|
197
|
+
_delete(keys) {
|
|
198
|
+
keys.forEach((k) => this.events.delete(k));
|
|
199
|
+
return keys;
|
|
200
|
+
}
|
|
201
|
+
destroy() {
|
|
202
|
+
this.wipe();
|
|
203
|
+
}
|
|
204
|
+
shouldInsert(event) {
|
|
205
|
+
const { type, key } = eventIdData(event);
|
|
206
|
+
const existing = this.events.get(key);
|
|
207
|
+
const qualified = this.qualifyEvent(event, key);
|
|
208
|
+
if (!qualified) return false;
|
|
209
|
+
if (!existing) return true;
|
|
210
|
+
if (type === "replaceable" || type === "parameterized") {
|
|
211
|
+
return event.created_at > existing.created_at;
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export {
|
|
218
|
+
eventType,
|
|
219
|
+
eventAddr,
|
|
220
|
+
eventKey,
|
|
221
|
+
eventIdData,
|
|
222
|
+
eventMatchesFilter,
|
|
223
|
+
AbstractMemoryRelay
|
|
224
|
+
};
|
|
225
|
+
//# sourceMappingURL=chunk-QC3J2K4S.mjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/utils.ts", "../../src/abstract.ts"],
|
|
4
|
+
"sourcesContent": ["import { isParameterizedReplaceableKind, isReplaceableKind } from \"nostr-tools/kinds\";\nimport { EventKeyDataType, EventType } from \"./types.js\";\nimport { Filter, NostrEvent } from \"nostr-tools\";\nimport { IEvent } from \"@nostrwatch/route66/models/Event\";\n\nexport const eventType = ( event: any ): EventType => {\n if(isReplaceableKind(event.kind)) {\n return 'replaceable';\n }\n else if(isParameterizedReplaceableKind(event.kind)){\n return 'parameterized';\n }\n else {\n return 'event'\n }\n}\n\nexport const eventAddr = ( event: any, type?: EventType ) => {\n let { pubkey, kind } = event;\n type = type ?? eventType(event);\n pubkey = pubkey.slice(0,16);\n if(type === 'parameterized') {\n const dtag = event.tags.find((t: string[]) => t[0] === 'd')?.[1]\n const key = `${pubkey}:${kind}:${dtag}`;\n return key;\n }\n else if(type === 'replaceable') {\n const key = `${pubkey}:${kind}`\n return key;\n }\n}\n\nexport const eventKey = (event: any, type?: EventType) =>{\n type = type ?? eventType(event);\n if(type === 'replaceable') {\n return eventAddr(event, type);\n }\n else if(type === 'parameterized') {\n return eventAddr(event, type);\n }\n else {\n return event.id\n }\n}\n\nexport const eventIdData = (event: any): EventKeyDataType => {\n const type = eventType(event)\n return {\n type,\n key: eventKey(event, type)\n }\n}\n\nexport function eventMatchesFilter(ev: IEvent | NostrEvent | any, filter: Filter): boolean {\n if (filter.since && ev.created_at < filter.since) {\n return false;\n }\n if (filter.until && ev.created_at > filter.until) {\n return false;\n }\n if (!(filter.ids?.includes(ev.id) ?? true)) {\n return false;\n }\n if (!(filter.authors?.includes(ev.pubkey) ?? true)) {\n return false;\n }\n if (!(filter.kinds?.includes(ev.kind) ?? true)) {\n return false;\n }\n const orTags = Object.entries(filter).filter(([k]) => k.startsWith(\"#\"));\n for (const [k, v] of orTags) {\n const vargs = v as Array<string>;\n for (const x of vargs) {\n if (!ev.tags.find((a: string[]) => a[0] === k.slice(1) && a[1] === x)) {\n return false;\n }\n }\n }\n const andTags = Object.entries(filter).filter(([k]) => k.startsWith(\"&\"));\n for (const [k, v] of andTags) {\n const vargs = v as Array<string>;\n const allMatch = vargs.every(x => ev.tags.some((tag: string[]) => tag[0] === k.slice(1) && tag[1] === x));\n if (!allMatch) {\n return false;\n }\n }\n return true;\n }", "// abstract.ts\n\nimport { Filter } from \"nostr-tools\";\nimport { eventIdData, eventKey, eventMatchesFilter } from \"./utils.js\";\nimport { AbstractMemoryRelayCallback, AbstractMemoryRelayCallbackInstatiate, AbstractMemoryRelayCallbackQualify } from \"./types.js\";\nimport { BaseEvent } from \"./types.js\";\n\nexport abstract class AbstractMemoryRelay<\n Input extends BaseEvent = any,\n Output extends BaseEvent = any,\n OutputSingle = Output,\n OutputCollection = any | any[],\n OutputCount = any\n> {\n private listeners: Map<string, AbstractMemoryRelayCallback<this, Output>> = new Map();\n\n protected _events: Map<string, Output> = new Map(); \n protected _nip11s: Map<string, any> = new Map();\n\n highestTimestamp: number[] = new Array(0);\n lowestTimestamp: number[] = new Array(0);\n debounceMS: number = 1000;\n\n protected set events(e: Map<string, Output>) {\n this._events = e;\n }\n\n get events() {\n return this._events;\n }\n\n get nip11s() {\n return this._nip11s;\n }\n\n abstract init(): Promise<void>;\n\n on(event: \"qualify\" | \"instantiate\", listener: AbstractMemoryRelayCallback<this, Output>): void {\n this.listeners.set(event, listener);\n }\n\n off(event: \"qualify\" | \"instantiate\"): void {\n this.listeners.delete(event);\n }\n\n protected instantiateEvent(event: Input, key: string): Output {\n return (\n (this.listeners.get(\"instantiate\") as AbstractMemoryRelayCallbackInstatiate<this, Output>)?.(event, key, this)\n ?? (event as unknown as Output)\n );\n }\n\n protected qualifyEvent(event: Input, key: string): boolean {\n return (\n (this.listeners.get(\"qualify\") as AbstractMemoryRelayCallbackQualify<this>)?.(event, key, this)\n ?? true\n );\n }\n\n exists(note: Input | string): boolean {\n return typeof note === \"string\" ? this.idExists(note) : this.noteExists(note);\n }\n\n noteExists(note: Input): boolean {\n return this.events.has(eventKey(note));\n }\n\n idExists(id: string): boolean {\n return this.events.has(id);\n }\n\n count(filters: Filter[]): OutputCount {\n let ret = 0;\n for (const [, e] of this.events) {\n for (const filter of filters) {\n if (eventMatchesFilter(e, filter)) {\n ret++;\n }\n }\n }\n return ret as unknown as OutputCount;\n }\n\n summary(): Record<string, number> {\n const ret: Record<string, number> = {};\n for (const [, event] of this.events) {\n if (!event.kind) continue;\n ret[event.kind.toString()] ??= 0;\n ret[event.kind.toString()]++;\n }\n return ret;\n }\n\n async dump(): Promise<Uint8Array> {\n const enc = new TextEncoder();\n return enc.encode(JSON.stringify(Array.from(this.events.values())));\n }\n\n close(): void {\n // no-op\n }\n\n async wipe() {\n this.events = new Map();\n }\n\n maybeInsert(event: Input): number {\n if (!this.shouldInsert(event)) return 0;\n const key = eventKey(event);\n this.events.set(key, this.instantiateEvent(event, key));\n return 1;\n }\n\n event(ev: Input): boolean {\n return this.maybeInsert(ev) ? true : false;\n }\n\n eventBatch(evs: Input[]): number {\n const inserted = [];\n for (const ev of evs) {\n if(ev.tags?.find(tag => tag[1] === \"tor\")){\n console.log('TOR RELAY', ev.id)\n }\n const inserts: number = this.maybeInsert(ev);\n if (!inserts) continue;\n inserted.push(ev);\n }\n return inserted.length;\n }\n\n setTimestampRange(event: Input | Output | BaseEvent, index: number = 0) {\n const ts = event.created_at as number;\n if (!this.highestTimestamp?.[index] || ts > this.highestTimestamp[index]) this.highestTimestamp[index] = ts;\n if (!this.lowestTimestamp?.[index] || ts < this.lowestTimestamp[index]) this.lowestTimestamp[index] = ts;\n }\n\n formatCollection(collection: Output[]): OutputCollection {\n return collection as unknown as OutputCollection;\n }\n\n req(id: string, filters: Filter[], format: boolean = true): OutputCollection | Output[] {\n const ret: Output[] = [];\n for (const [, e] of this.events) {\n for (const [index, filter] of filters.entries()) {\n if (eventMatchesFilter(e, filter)) {\n ret.push(e);\n this.setTimestampRange(e, index);\n }\n }\n }\n return format ? this.formatCollection(ret) : ret;\n }\n\n get(key: string): Output | undefined {\n return this.events.get(key) as Output | undefined;\n }\n\n delete(filters: Filter[]): string[] {\n const forDelete: Output[] = this.req(\"ids-for-delete\", filters, false) as Output[];\n const keys = forDelete\n .map((e) => eventKey(e))\n .filter((a) => typeof a === \"string\");\n if (!keys?.length) return [];\n return this._delete(keys);\n }\n\n _delete(keys: string[]): string[] {\n keys.forEach((k) => this.events.delete(k));\n return keys;\n }\n\n destroy() {\n this.wipe();\n }\n\n protected shouldInsert(event: Input) {\n const { type, key } = eventIdData(event);\n const existing = this.events.get(key);\n const qualified = this.qualifyEvent(event, key)\n if (!qualified) return false;\n if (!existing) return true;\n if (type === \"replaceable\" || type === \"parameterized\") {\n return (event.created_at as number) > (existing.created_at as number)\n }\n return true;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AbstractMemoryRelay,
|
|
3
|
+
eventKey,
|
|
4
|
+
eventMatchesFilter
|
|
5
|
+
} from "./chunk-QC3J2K4S.mjs";
|
|
6
|
+
|
|
7
|
+
// src/svelte.ts
|
|
8
|
+
import { derived, get } from "svelte/store";
|
|
9
|
+
var SvelteMemoryRelay = class extends AbstractMemoryRelay {
|
|
10
|
+
constructor(_store) {
|
|
11
|
+
super();
|
|
12
|
+
this._store = _store;
|
|
13
|
+
}
|
|
14
|
+
get events() {
|
|
15
|
+
return get(this._store);
|
|
16
|
+
}
|
|
17
|
+
get store() {
|
|
18
|
+
return this._store;
|
|
19
|
+
}
|
|
20
|
+
set events(e) {
|
|
21
|
+
this.store.set(e);
|
|
22
|
+
}
|
|
23
|
+
async init() {
|
|
24
|
+
return Promise.resolve();
|
|
25
|
+
}
|
|
26
|
+
maybeInsert(payload) {
|
|
27
|
+
let added = 0;
|
|
28
|
+
this._store.update((current) => {
|
|
29
|
+
let newMap;
|
|
30
|
+
if (Array.isArray(payload)) {
|
|
31
|
+
newMap = new Map(current);
|
|
32
|
+
for (const ev of payload) {
|
|
33
|
+
if (!this.shouldInsert(ev)) continue;
|
|
34
|
+
const key = eventKey(ev);
|
|
35
|
+
newMap.set(key, this.instantiateEvent(ev, key));
|
|
36
|
+
added++;
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
if (this.shouldInsert(payload)) {
|
|
40
|
+
const key = eventKey(payload);
|
|
41
|
+
current.set(key, this.instantiateEvent(payload, key));
|
|
42
|
+
added++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return newMap ? newMap : current;
|
|
46
|
+
});
|
|
47
|
+
return added;
|
|
48
|
+
}
|
|
49
|
+
$req(id, filters, format = true) {
|
|
50
|
+
const store = derived(
|
|
51
|
+
this.store,
|
|
52
|
+
($events) => {
|
|
53
|
+
const results = [];
|
|
54
|
+
for (const [index, filter] of filters.entries()) {
|
|
55
|
+
for (const [key, event] of $events) {
|
|
56
|
+
if (eventMatchesFilter(event, filter)) {
|
|
57
|
+
results.push(event);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return results;
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
return store;
|
|
65
|
+
}
|
|
66
|
+
$get(key) {
|
|
67
|
+
return derived(this.store, ($events) => {
|
|
68
|
+
return $events.get(key);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
$count(filters) {
|
|
72
|
+
const request = this.req("", filters);
|
|
73
|
+
const result = derived(request, ($events) => {
|
|
74
|
+
return $events.length;
|
|
75
|
+
});
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
_delete(keys) {
|
|
79
|
+
const deleted = /* @__PURE__ */ new Set();
|
|
80
|
+
this.store.update((current) => {
|
|
81
|
+
for (const key of keys) {
|
|
82
|
+
if (!current.has(key)) continue;
|
|
83
|
+
deleted.add(current.get(key)?.id || key);
|
|
84
|
+
current.delete(key);
|
|
85
|
+
}
|
|
86
|
+
return current;
|
|
87
|
+
});
|
|
88
|
+
return Array.from(deleted);
|
|
89
|
+
}
|
|
90
|
+
event(ev) {
|
|
91
|
+
return this.maybeInsert(ev) ? true : false;
|
|
92
|
+
}
|
|
93
|
+
eventBatch(evs) {
|
|
94
|
+
return this.maybeInsert(evs);
|
|
95
|
+
}
|
|
96
|
+
async wipe() {
|
|
97
|
+
this.store.set(/* @__PURE__ */ new Map());
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export {
|
|
102
|
+
SvelteMemoryRelay
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=chunk-WJKCFUCB.mjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/svelte.ts"],
|
|
4
|
+
"sourcesContent": ["// svelte.ts\n\nimport { derived, get, type Readable, type Writable } from \"svelte/store\";\nimport { Filter } from \"nostr-tools\";\nimport { eventKey, eventMatchesFilter } from \"./utils.js\";\nimport { AbstractMemoryRelay } from \"./abstract.js\";\nimport { BaseEvent } from \"./types.js\";\n\nexport class SvelteMemoryRelay<\n InputEvent extends BaseEvent,\n OutputEvent extends BaseEvent,\n OutputSingle extends Readable<OutputEvent> = Readable<OutputEvent>,\n OutputCollection extends Readable<OutputEvent[]> = Readable<OutputEvent[]>,\n OutputCount extends Readable<number> = Readable<number>\n> extends AbstractMemoryRelay<InputEvent, OutputEvent, OutputSingle, OutputCollection, OutputCount> {\n\n constructor(private _store: Writable<Map<string, OutputEvent>>) {\n super();\n }\n\n get events(): Map<string, OutputEvent> {\n return get(this._store);\n }\n\n get store(): Writable<Map<string, OutputEvent>> {\n return this._store;\n }\n\n protected set events(e: Map<string, OutputEvent>) {\n this.store.set(e);\n }\n\n async init(): Promise<void> {\n return Promise.resolve();\n }\n\n maybeInsert(payload: InputEvent | InputEvent[]): number {\n let added = 0;\n this._store.update((current) => {\n let newMap: Map<string, OutputEvent> | undefined;\n if (Array.isArray(payload)) {\n newMap = new Map(current);\n for (const ev of payload) {\n if (!this.shouldInsert(ev)) continue;\n const key = eventKey(ev);\n newMap.set(key, this.instantiateEvent(ev, key));\n added++;\n }\n } else {\n if (this.shouldInsert(payload)) {\n const key = eventKey(payload);\n current.set(key, this.instantiateEvent(payload, key));\n added++;\n }\n }\n return newMap? newMap: current;\n });\n return added;\n }\n\n $req(id: string, filters: Filter[], format: boolean = true): OutputCollection {\n const store = derived<Writable<Map<string, OutputEvent>>, OutputEvent[]>(\n this.store, \n ($events) => {\n const results: OutputEvent[] = [];\n for (const [index, filter] of filters.entries()) {\n for (const [key, event] of $events) {\n if (eventMatchesFilter(event, filter)) {\n results.push(event);\n // this.setTimestampRange(event, index);\n }\n }\n }\n return results;\n }\n );\n return store as OutputCollection;\n }\n\n $get(key: string): OutputSingle | undefined {\n return derived(this.store, ($events) => {\n return $events.get(key);\n }) as OutputSingle | undefined;\n }\n\n $count(filters: Filter[]): OutputCount {\n const request = this.req(\"\", filters) as Readable<OutputEvent[]>;\n const result = derived<Readable<OutputEvent[]>, number>(request, ($events) => {\n return $events.length;\n });\n return result as OutputCount;\n }\n\n _delete(keys: string[]): string[] {\n const deleted = new Set<string>();\n this.store.update((current) => {\n for (const key of keys) {\n if(!current.has(key)) continue;\n deleted.add(current.get(key)?.id || key);\n current.delete(key);\n }\n return current;\n });\n return Array.from(deleted);\n }\n\n event(ev: InputEvent): boolean {\n return this.maybeInsert(ev) ? true : false;\n }\n\n eventBatch(evs: InputEvent[]): number {\n return this.maybeInsert(evs);\n }\n\n async wipe(): Promise<void> {\n this.store.set(new Map<string, OutputEvent>());\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;AAEA,SAAS,SAAS,WAAyC;AAMpD,IAAM,oBAAN,cAMG,oBAA0F;AAAA,EAElG,YAAoB,QAA4C;AAC9D,UAAM;AADY;AAAA,EAEpB;AAAA,EAEA,IAAI,SAAmC;AACrC,WAAO,IAAI,KAAK,MAAM;AAAA,EACxB;AAAA,EAEA,IAAI,QAA4C;AAC9C,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAc,OAAO,GAA6B;AAChD,SAAK,MAAM,IAAI,CAAC;AAAA,EAClB;AAAA,EAEA,MAAM,OAAsB;AAC1B,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAAA,EAEA,YAAY,SAA4C;AACtD,QAAI,QAAQ;AACZ,SAAK,OAAO,OAAO,CAAC,YAAY;AAC9B,UAAI;AACJ,UAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,iBAAS,IAAI,IAAI,OAAO;AACxB,mBAAW,MAAM,SAAS;AACxB,cAAI,CAAC,KAAK,aAAa,EAAE,EAAG;AAC5B,gBAAM,MAAM,SAAS,EAAE;AACvB,iBAAO,IAAI,KAAK,KAAK,iBAAiB,IAAI,GAAG,CAAC;AAC9C;AAAA,QACF;AAAA,MACF,OAAO;AACL,YAAI,KAAK,aAAa,OAAO,GAAG;AAC9B,gBAAM,MAAM,SAAS,OAAO;AAC5B,kBAAQ,IAAI,KAAK,KAAK,iBAAiB,SAAS,GAAG,CAAC;AACpD;AAAA,QACF;AAAA,MACF;AACA,aAAO,SAAQ,SAAQ;AAAA,IACzB,CAAC;AACD,WAAO;AAAA,EACT;AAAA,EAEA,KAAK,IAAY,SAAmB,SAAkB,MAAwB;AAC5E,UAAM,QAAQ;AAAA,MACZ,KAAK;AAAA,MACL,CAAC,YAAY;AACX,cAAM,UAAyB,CAAC;AAChC,mBAAW,CAAC,OAAO,MAAM,KAAK,QAAQ,QAAQ,GAAG;AAC/C,qBAAW,CAAC,KAAK,KAAK,KAAK,SAAS;AAClC,gBAAI,mBAAmB,OAAO,MAAM,GAAG;AACrC,sBAAQ,KAAK,KAAK;AAAA,YAEpB;AAAA,UACF;AAAA,QACF;AACA,eAAO;AAAA,MACT;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EAEA,KAAK,KAAuC;AAC1C,WAAO,QAAQ,KAAK,OAAO,CAAC,YAAY;AACtC,aAAO,QAAQ,IAAI,GAAG;AAAA,IACxB,CAAC;AAAA,EACH;AAAA,EAEA,OAAO,SAAgC;AACrC,UAAM,UAAU,KAAK,IAAI,IAAI,OAAO;AACpC,UAAM,SAAS,QAAyC,SAAS,CAAC,YAAY;AAC5E,aAAO,QAAQ;AAAA,IACjB,CAAC;AACD,WAAO;AAAA,EACT;AAAA,EAEA,QAAQ,MAA0B;AAChC,UAAM,UAAU,oBAAI,IAAY;AAChC,SAAK,MAAM,OAAO,CAAC,YAAY;AAC7B,iBAAW,OAAO,MAAM;AACtB,YAAG,CAAC,QAAQ,IAAI,GAAG,EAAG;AACtB,gBAAQ,IAAI,QAAQ,IAAI,GAAG,GAAG,MAAM,GAAG;AACvC,gBAAQ,OAAO,GAAG;AAAA,MACpB;AACA,aAAO;AAAA,IACT,CAAC;AACD,WAAO,MAAM,KAAK,OAAO;AAAA,EAC3B;AAAA,EAEA,MAAM,IAAyB;AAC7B,WAAO,KAAK,YAAY,EAAE,IAAI,OAAO;AAAA,EACvC;AAAA,EAEA,WAAW,KAA2B;AACpC,WAAO,KAAK,YAAY,GAAG;AAAA,EAC7B;AAAA,EAEA,MAAM,OAAsB;AAC1B,SAAK,MAAM,IAAI,oBAAI,IAAyB,CAAC;AAAA,EAC/C;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SvelteMemoryRelay
|
|
3
|
+
} from "./chunk-WJKCFUCB.mjs";
|
|
4
|
+
import {
|
|
5
|
+
AbstractMemoryRelay,
|
|
6
|
+
eventAddr,
|
|
7
|
+
eventIdData,
|
|
8
|
+
eventKey,
|
|
9
|
+
eventMatchesFilter,
|
|
10
|
+
eventType
|
|
11
|
+
} from "./chunk-QC3J2K4S.mjs";
|
|
12
|
+
export {
|
|
13
|
+
AbstractMemoryRelay,
|
|
14
|
+
SvelteMemoryRelay,
|
|
15
|
+
eventAddr,
|
|
16
|
+
eventIdData,
|
|
17
|
+
eventKey,
|
|
18
|
+
eventMatchesFilter,
|
|
19
|
+
eventType
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=index.mjs.map
|
package/dist/index.d.ts
ADDED
package/dist/svelte.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Readable, type Writable } from "svelte/store";
|
|
2
|
+
import { Filter } from "nostr-tools";
|
|
3
|
+
import { AbstractMemoryRelay } from "./abstract.js";
|
|
4
|
+
import { BaseEvent } from "./types.js";
|
|
5
|
+
export declare class SvelteMemoryRelay<InputEvent extends BaseEvent, OutputEvent extends BaseEvent, OutputSingle extends Readable<OutputEvent> = Readable<OutputEvent>, OutputCollection extends Readable<OutputEvent[]> = Readable<OutputEvent[]>, OutputCount extends Readable<number> = Readable<number>> extends AbstractMemoryRelay<InputEvent, OutputEvent, OutputSingle, OutputCollection, OutputCount> {
|
|
6
|
+
private _store;
|
|
7
|
+
constructor(_store: Writable<Map<string, OutputEvent>>);
|
|
8
|
+
get events(): Map<string, OutputEvent>;
|
|
9
|
+
get store(): Writable<Map<string, OutputEvent>>;
|
|
10
|
+
protected set events(e: Map<string, OutputEvent>);
|
|
11
|
+
init(): Promise<void>;
|
|
12
|
+
maybeInsert(payload: InputEvent | InputEvent[]): number;
|
|
13
|
+
$req(id: string, filters: Filter[], format?: boolean): OutputCollection;
|
|
14
|
+
$get(key: string): OutputSingle | undefined;
|
|
15
|
+
$count(filters: Filter[]): OutputCount;
|
|
16
|
+
_delete(keys: string[]): string[];
|
|
17
|
+
event(ev: InputEvent): boolean;
|
|
18
|
+
eventBatch(evs: InputEvent[]): number;
|
|
19
|
+
wipe(): Promise<void>;
|
|
20
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type EventType = 'replaceable' | 'parameterized' | 'event';
|
|
2
|
+
export interface BaseEvent {
|
|
3
|
+
id?: string | null;
|
|
4
|
+
created_at?: number | null;
|
|
5
|
+
kind: number | null;
|
|
6
|
+
pubkey: string | null;
|
|
7
|
+
tags: string[][] | null;
|
|
8
|
+
content: string | null;
|
|
9
|
+
sig?: string | null;
|
|
10
|
+
}
|
|
11
|
+
export type EventKeyDataType = {
|
|
12
|
+
type: EventType;
|
|
13
|
+
key: string;
|
|
14
|
+
};
|
|
15
|
+
export type AbstractMemoryRelayCallbackQualify<I> = (event: any, key: string, instance: I) => boolean;
|
|
16
|
+
export type AbstractMemoryRelayCallbackInstatiate<I, Output> = (event: any, key: string, instance: I) => Output;
|
|
17
|
+
export type AbstractMemoryRelayCallback<I, Output> = AbstractMemoryRelayCallbackQualify<I> | AbstractMemoryRelayCallbackInstatiate<I, Output>;
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { EventKeyDataType, EventType } from "./types.js";
|
|
2
|
+
import { Filter, NostrEvent } from "nostr-tools";
|
|
3
|
+
import { IEvent } from "@nostrwatch/route66/models/Event";
|
|
4
|
+
export declare const eventType: (event: any) => EventType;
|
|
5
|
+
export declare const eventAddr: (event: any, type?: EventType) => string | undefined;
|
|
6
|
+
export declare const eventKey: (event: any, type?: EventType) => any;
|
|
7
|
+
export declare const eventIdData: (event: any) => EventKeyDataType;
|
|
8
|
+
export declare function eventMatchesFilter(ev: IEvent | NostrEvent | any, filter: Filter): boolean;
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nostrwatch/memory-relay",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "A simple memory relay",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/esm/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"module": "src/index.ts",
|
|
9
|
+
"repository": "https://github.com/sandwichfarm/nostr-watch",
|
|
10
|
+
"author": "Sandwich",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/esm/index.d.ts",
|
|
15
|
+
"import": "./dist/esm/index.mjs",
|
|
16
|
+
"require": "./dist/esm/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./abstract": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/esm/abstract.mjs",
|
|
21
|
+
"require": "./dist/esm/abstract.js"
|
|
22
|
+
},
|
|
23
|
+
"./svelte": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/esm/abstract.mjs",
|
|
26
|
+
"require": "./dist/esm/abstract.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"src",
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"lodash": "^4.17.23"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/lodash": "^4",
|
|
38
|
+
"esbuild": "^0.25.0",
|
|
39
|
+
"typescript": "^5.2.2"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"nostr-tools": "^2.10.4",
|
|
43
|
+
"svelte": "^5.2.7",
|
|
44
|
+
"tseep": "^1.3.1",
|
|
45
|
+
"@nostrwatch/route66": "^0.0.1",
|
|
46
|
+
"@nostrwatch/utils": "^0.1.9"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"clean": "rm -rf dist",
|
|
50
|
+
"tsc": "tsc",
|
|
51
|
+
"build": "pnpm clean && pnpm tsc && pnpm node build.js",
|
|
52
|
+
"build:esm": "esbuild src/index.ts src/abstract.ts src/svelte.ts --bundle --minify --sourcemap --outdir=dist/esm --format=esm --out-extension:.js=.mjs"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/abstract.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// abstract.ts
|
|
2
|
+
|
|
3
|
+
import { Filter } from "nostr-tools";
|
|
4
|
+
import { eventIdData, eventKey, eventMatchesFilter } from "./utils.js";
|
|
5
|
+
import { AbstractMemoryRelayCallback, AbstractMemoryRelayCallbackInstatiate, AbstractMemoryRelayCallbackQualify } from "./types.js";
|
|
6
|
+
import { BaseEvent } from "./types.js";
|
|
7
|
+
|
|
8
|
+
export abstract class AbstractMemoryRelay<
|
|
9
|
+
Input extends BaseEvent = any,
|
|
10
|
+
Output extends BaseEvent = any,
|
|
11
|
+
OutputSingle = Output,
|
|
12
|
+
OutputCollection = any | any[],
|
|
13
|
+
OutputCount = any
|
|
14
|
+
> {
|
|
15
|
+
private listeners: Map<string, AbstractMemoryRelayCallback<this, Output>> = new Map();
|
|
16
|
+
|
|
17
|
+
protected _events: Map<string, Output> = new Map();
|
|
18
|
+
protected _nip11s: Map<string, any> = new Map();
|
|
19
|
+
|
|
20
|
+
highestTimestamp: number[] = new Array(0);
|
|
21
|
+
lowestTimestamp: number[] = new Array(0);
|
|
22
|
+
debounceMS: number = 1000;
|
|
23
|
+
|
|
24
|
+
protected set events(e: Map<string, Output>) {
|
|
25
|
+
this._events = e;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get events() {
|
|
29
|
+
return this._events;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get nip11s() {
|
|
33
|
+
return this._nip11s;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
abstract init(): Promise<void>;
|
|
37
|
+
|
|
38
|
+
on(event: "qualify" | "instantiate", listener: AbstractMemoryRelayCallback<this, Output>): void {
|
|
39
|
+
this.listeners.set(event, listener);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
off(event: "qualify" | "instantiate"): void {
|
|
43
|
+
this.listeners.delete(event);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected instantiateEvent(event: Input, key: string): Output {
|
|
47
|
+
return (
|
|
48
|
+
(this.listeners.get("instantiate") as AbstractMemoryRelayCallbackInstatiate<this, Output>)?.(event, key, this)
|
|
49
|
+
?? (event as unknown as Output)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
protected qualifyEvent(event: Input, key: string): boolean {
|
|
54
|
+
return (
|
|
55
|
+
(this.listeners.get("qualify") as AbstractMemoryRelayCallbackQualify<this>)?.(event, key, this)
|
|
56
|
+
?? true
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
exists(note: Input | string): boolean {
|
|
61
|
+
return typeof note === "string" ? this.idExists(note) : this.noteExists(note);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
noteExists(note: Input): boolean {
|
|
65
|
+
return this.events.has(eventKey(note));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
idExists(id: string): boolean {
|
|
69
|
+
return this.events.has(id);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
count(filters: Filter[]): OutputCount {
|
|
73
|
+
let ret = 0;
|
|
74
|
+
for (const [, e] of this.events) {
|
|
75
|
+
for (const filter of filters) {
|
|
76
|
+
if (eventMatchesFilter(e, filter)) {
|
|
77
|
+
ret++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return ret as unknown as OutputCount;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
summary(): Record<string, number> {
|
|
85
|
+
const ret: Record<string, number> = {};
|
|
86
|
+
for (const [, event] of this.events) {
|
|
87
|
+
if (!event.kind) continue;
|
|
88
|
+
ret[event.kind.toString()] ??= 0;
|
|
89
|
+
ret[event.kind.toString()]++;
|
|
90
|
+
}
|
|
91
|
+
return ret;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async dump(): Promise<Uint8Array> {
|
|
95
|
+
const enc = new TextEncoder();
|
|
96
|
+
return enc.encode(JSON.stringify(Array.from(this.events.values())));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
close(): void {
|
|
100
|
+
// no-op
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async wipe() {
|
|
104
|
+
this.events = new Map();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
maybeInsert(event: Input): number {
|
|
108
|
+
if (!this.shouldInsert(event)) return 0;
|
|
109
|
+
const key = eventKey(event);
|
|
110
|
+
this.events.set(key, this.instantiateEvent(event, key));
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
event(ev: Input): boolean {
|
|
115
|
+
return this.maybeInsert(ev) ? true : false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
eventBatch(evs: Input[]): number {
|
|
119
|
+
const inserted = [];
|
|
120
|
+
for (const ev of evs) {
|
|
121
|
+
if(ev.tags?.find(tag => tag[1] === "tor")){
|
|
122
|
+
console.log('TOR RELAY', ev.id)
|
|
123
|
+
}
|
|
124
|
+
const inserts: number = this.maybeInsert(ev);
|
|
125
|
+
if (!inserts) continue;
|
|
126
|
+
inserted.push(ev);
|
|
127
|
+
}
|
|
128
|
+
return inserted.length;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
setTimestampRange(event: Input | Output | BaseEvent, index: number = 0) {
|
|
132
|
+
const ts = event.created_at as number;
|
|
133
|
+
if (!this.highestTimestamp?.[index] || ts > this.highestTimestamp[index]) this.highestTimestamp[index] = ts;
|
|
134
|
+
if (!this.lowestTimestamp?.[index] || ts < this.lowestTimestamp[index]) this.lowestTimestamp[index] = ts;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
formatCollection(collection: Output[]): OutputCollection {
|
|
138
|
+
return collection as unknown as OutputCollection;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
req(id: string, filters: Filter[], format: boolean = true): OutputCollection | Output[] {
|
|
142
|
+
const ret: Output[] = [];
|
|
143
|
+
for (const [, e] of this.events) {
|
|
144
|
+
for (const [index, filter] of filters.entries()) {
|
|
145
|
+
if (eventMatchesFilter(e, filter)) {
|
|
146
|
+
ret.push(e);
|
|
147
|
+
this.setTimestampRange(e, index);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return format ? this.formatCollection(ret) : ret;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
get(key: string): Output | undefined {
|
|
155
|
+
return this.events.get(key) as Output | undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
delete(filters: Filter[]): string[] {
|
|
159
|
+
const forDelete: Output[] = this.req("ids-for-delete", filters, false) as Output[];
|
|
160
|
+
const keys = forDelete
|
|
161
|
+
.map((e) => eventKey(e))
|
|
162
|
+
.filter((a) => typeof a === "string");
|
|
163
|
+
if (!keys?.length) return [];
|
|
164
|
+
return this._delete(keys);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
_delete(keys: string[]): string[] {
|
|
168
|
+
keys.forEach((k) => this.events.delete(k));
|
|
169
|
+
return keys;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
destroy() {
|
|
173
|
+
this.wipe();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
protected shouldInsert(event: Input) {
|
|
177
|
+
const { type, key } = eventIdData(event);
|
|
178
|
+
const existing = this.events.get(key);
|
|
179
|
+
const qualified = this.qualifyEvent(event, key)
|
|
180
|
+
if (!qualified) return false;
|
|
181
|
+
if (!existing) return true;
|
|
182
|
+
if (type === "replaceable" || type === "parameterized") {
|
|
183
|
+
return (event.created_at as number) > (existing.created_at as number)
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
}
|
package/src/index.ts
ADDED
package/src/svelte.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// svelte.ts
|
|
2
|
+
|
|
3
|
+
import { derived, get, type Readable, type Writable } from "svelte/store";
|
|
4
|
+
import { Filter } from "nostr-tools";
|
|
5
|
+
import { eventKey, eventMatchesFilter } from "./utils.js";
|
|
6
|
+
import { AbstractMemoryRelay } from "./abstract.js";
|
|
7
|
+
import { BaseEvent } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export class SvelteMemoryRelay<
|
|
10
|
+
InputEvent extends BaseEvent,
|
|
11
|
+
OutputEvent extends BaseEvent,
|
|
12
|
+
OutputSingle extends Readable<OutputEvent> = Readable<OutputEvent>,
|
|
13
|
+
OutputCollection extends Readable<OutputEvent[]> = Readable<OutputEvent[]>,
|
|
14
|
+
OutputCount extends Readable<number> = Readable<number>
|
|
15
|
+
> extends AbstractMemoryRelay<InputEvent, OutputEvent, OutputSingle, OutputCollection, OutputCount> {
|
|
16
|
+
|
|
17
|
+
constructor(private _store: Writable<Map<string, OutputEvent>>) {
|
|
18
|
+
super();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get events(): Map<string, OutputEvent> {
|
|
22
|
+
return get(this._store);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get store(): Writable<Map<string, OutputEvent>> {
|
|
26
|
+
return this._store;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
protected set events(e: Map<string, OutputEvent>) {
|
|
30
|
+
this.store.set(e);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async init(): Promise<void> {
|
|
34
|
+
return Promise.resolve();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
maybeInsert(payload: InputEvent | InputEvent[]): number {
|
|
38
|
+
let added = 0;
|
|
39
|
+
this._store.update((current) => {
|
|
40
|
+
let newMap: Map<string, OutputEvent> | undefined;
|
|
41
|
+
if (Array.isArray(payload)) {
|
|
42
|
+
newMap = new Map(current);
|
|
43
|
+
for (const ev of payload) {
|
|
44
|
+
if (!this.shouldInsert(ev)) continue;
|
|
45
|
+
const key = eventKey(ev);
|
|
46
|
+
newMap.set(key, this.instantiateEvent(ev, key));
|
|
47
|
+
added++;
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
if (this.shouldInsert(payload)) {
|
|
51
|
+
const key = eventKey(payload);
|
|
52
|
+
current.set(key, this.instantiateEvent(payload, key));
|
|
53
|
+
added++;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return newMap? newMap: current;
|
|
57
|
+
});
|
|
58
|
+
return added;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
$req(id: string, filters: Filter[], format: boolean = true): OutputCollection {
|
|
62
|
+
const store = derived<Writable<Map<string, OutputEvent>>, OutputEvent[]>(
|
|
63
|
+
this.store,
|
|
64
|
+
($events) => {
|
|
65
|
+
const results: OutputEvent[] = [];
|
|
66
|
+
for (const [index, filter] of filters.entries()) {
|
|
67
|
+
for (const [key, event] of $events) {
|
|
68
|
+
if (eventMatchesFilter(event, filter)) {
|
|
69
|
+
results.push(event);
|
|
70
|
+
// this.setTimestampRange(event, index);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
return store as OutputCollection;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
$get(key: string): OutputSingle | undefined {
|
|
81
|
+
return derived(this.store, ($events) => {
|
|
82
|
+
return $events.get(key);
|
|
83
|
+
}) as OutputSingle | undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
$count(filters: Filter[]): OutputCount {
|
|
87
|
+
const request = this.req("", filters) as Readable<OutputEvent[]>;
|
|
88
|
+
const result = derived<Readable<OutputEvent[]>, number>(request, ($events) => {
|
|
89
|
+
return $events.length;
|
|
90
|
+
});
|
|
91
|
+
return result as OutputCount;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_delete(keys: string[]): string[] {
|
|
95
|
+
const deleted = new Set<string>();
|
|
96
|
+
this.store.update((current) => {
|
|
97
|
+
for (const key of keys) {
|
|
98
|
+
if(!current.has(key)) continue;
|
|
99
|
+
deleted.add(current.get(key)?.id || key);
|
|
100
|
+
current.delete(key);
|
|
101
|
+
}
|
|
102
|
+
return current;
|
|
103
|
+
});
|
|
104
|
+
return Array.from(deleted);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
event(ev: InputEvent): boolean {
|
|
108
|
+
return this.maybeInsert(ev) ? true : false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
eventBatch(evs: InputEvent[]): number {
|
|
112
|
+
return this.maybeInsert(evs);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async wipe(): Promise<void> {
|
|
116
|
+
this.store.set(new Map<string, OutputEvent>());
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Listener } from "tseep";
|
|
2
|
+
|
|
3
|
+
export type EventType = 'replaceable' | 'parameterized' | 'event';
|
|
4
|
+
|
|
5
|
+
export interface BaseEvent {
|
|
6
|
+
id?: string | null;
|
|
7
|
+
created_at?: number | null;
|
|
8
|
+
kind: number | null;
|
|
9
|
+
pubkey: string | null;
|
|
10
|
+
tags: string[][] | null;
|
|
11
|
+
content: string | null;
|
|
12
|
+
sig?: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type EventKeyDataType = {
|
|
16
|
+
type: EventType;
|
|
17
|
+
key: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type AbstractMemoryRelayCallbackQualify<I> = (event: any, key: string, instance: I) => boolean;
|
|
21
|
+
export type AbstractMemoryRelayCallbackInstatiate<I, Output> = (event: any, key: string, instance: I) => Output;
|
|
22
|
+
|
|
23
|
+
export type AbstractMemoryRelayCallback<I, Output> = AbstractMemoryRelayCallbackQualify<I> | AbstractMemoryRelayCallbackInstatiate<I, Output>;
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { isParameterizedReplaceableKind, isReplaceableKind } from "nostr-tools/kinds";
|
|
2
|
+
import { EventKeyDataType, EventType } from "./types.js";
|
|
3
|
+
import { Filter, NostrEvent } from "nostr-tools";
|
|
4
|
+
import { IEvent } from "@nostrwatch/route66/models/Event";
|
|
5
|
+
|
|
6
|
+
export const eventType = ( event: any ): EventType => {
|
|
7
|
+
if(isReplaceableKind(event.kind)) {
|
|
8
|
+
return 'replaceable';
|
|
9
|
+
}
|
|
10
|
+
else if(isParameterizedReplaceableKind(event.kind)){
|
|
11
|
+
return 'parameterized';
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
return 'event'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const eventAddr = ( event: any, type?: EventType ) => {
|
|
19
|
+
let { pubkey, kind } = event;
|
|
20
|
+
type = type ?? eventType(event);
|
|
21
|
+
pubkey = pubkey.slice(0,16);
|
|
22
|
+
if(type === 'parameterized') {
|
|
23
|
+
const dtag = event.tags.find((t: string[]) => t[0] === 'd')?.[1]
|
|
24
|
+
const key = `${pubkey}:${kind}:${dtag}`;
|
|
25
|
+
return key;
|
|
26
|
+
}
|
|
27
|
+
else if(type === 'replaceable') {
|
|
28
|
+
const key = `${pubkey}:${kind}`
|
|
29
|
+
return key;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const eventKey = (event: any, type?: EventType) =>{
|
|
34
|
+
type = type ?? eventType(event);
|
|
35
|
+
if(type === 'replaceable') {
|
|
36
|
+
return eventAddr(event, type);
|
|
37
|
+
}
|
|
38
|
+
else if(type === 'parameterized') {
|
|
39
|
+
return eventAddr(event, type);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
return event.id
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const eventIdData = (event: any): EventKeyDataType => {
|
|
47
|
+
const type = eventType(event)
|
|
48
|
+
return {
|
|
49
|
+
type,
|
|
50
|
+
key: eventKey(event, type)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function eventMatchesFilter(ev: IEvent | NostrEvent | any, filter: Filter): boolean {
|
|
55
|
+
if (filter.since && ev.created_at < filter.since) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (filter.until && ev.created_at > filter.until) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (!(filter.ids?.includes(ev.id) ?? true)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (!(filter.authors?.includes(ev.pubkey) ?? true)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (!(filter.kinds?.includes(ev.kind) ?? true)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const orTags = Object.entries(filter).filter(([k]) => k.startsWith("#"));
|
|
71
|
+
for (const [k, v] of orTags) {
|
|
72
|
+
const vargs = v as Array<string>;
|
|
73
|
+
for (const x of vargs) {
|
|
74
|
+
if (!ev.tags.find((a: string[]) => a[0] === k.slice(1) && a[1] === x)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const andTags = Object.entries(filter).filter(([k]) => k.startsWith("&"));
|
|
80
|
+
for (const [k, v] of andTags) {
|
|
81
|
+
const vargs = v as Array<string>;
|
|
82
|
+
const allMatch = vargs.every(x => ev.tags.some((tag: string[]) => tag[0] === k.slice(1) && tag[1] === x));
|
|
83
|
+
if (!allMatch) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|