save-event.go raw
1 //go:build js && wasm
2
3 package wasmdb
4
5 import (
6 "bytes"
7 "context"
8 "errors"
9 "fmt"
10 "strings"
11
12 "github.com/aperturerobotics/go-indexeddb/idb"
13 "github.com/hack-pad/safejs"
14 "next.orly.dev/pkg/lol/chk"
15
16 "next.orly.dev/pkg/nostr/encoders/event"
17 "next.orly.dev/pkg/nostr/encoders/hex"
18 "next.orly.dev/pkg/nostr/encoders/kind"
19 "next.orly.dev/pkg/nostr/encoders/tag"
20 "next.orly.dev/pkg/database"
21 "next.orly.dev/pkg/database/indexes"
22 "next.orly.dev/pkg/database/indexes/types"
23 )
24
25 var (
26 // ErrOlderThanExisting is returned when a candidate event is older than an existing replaceable/addressable event.
27 ErrOlderThanExisting = errors.New("older than existing event")
28 // ErrMissingDTag is returned when a parameterized replaceable event lacks the required 'd' tag.
29 ErrMissingDTag = errors.New("event is missing a d tag identifier")
30 )
31
32 // SaveEvent saves an event to the database, generating all necessary indexes.
33 func (w *W) SaveEvent(c context.Context, ev *event.E) (replaced bool, err error) {
34 if ev == nil {
35 err = errors.New("nil event")
36 return
37 }
38
39 // Reject ephemeral events (kinds 20000-29999) - they should never be stored
40 if ev.Kind >= 20000 && ev.Kind <= 29999 {
41 err = errors.New("blocked: ephemeral events should not be stored")
42 return
43 }
44
45 // Validate kind 3 (follow list) events have at least one p tag
46 if ev.Kind == 3 {
47 hasPTag := false
48 if ev.Tags != nil {
49 for _, t := range *ev.Tags {
50 if t != nil && t.Len() >= 2 {
51 key := t.Key()
52 if len(key) == 1 && key[0] == 'p' {
53 hasPTag = true
54 break
55 }
56 }
57 }
58 }
59 if !hasPTag {
60 w.Logger.Warnf("SaveEvent: rejecting kind 3 event without p tags from pubkey %x", ev.Pubkey)
61 err = errors.New("blocked: kind 3 follow list events must have at least one p tag")
62 return
63 }
64 }
65
66 // Check if the event already exists
67 var ser *types.Uint40
68 if ser, err = w.GetSerialById(ev.ID); err == nil && ser != nil {
69 err = errors.New("blocked: event already exists: " + hex.Enc(ev.ID[:]))
70 return
71 }
72
73 // If the error is "id not found", we can proceed
74 if err != nil && strings.Contains(err.Error(), "id not found") {
75 err = nil
76 } else if err != nil {
77 return
78 }
79
80 // Check for replacement - only validate, don't delete old events
81 if kind.IsReplaceable(ev.Kind) || kind.IsParameterizedReplaceable(ev.Kind) {
82 var werr error
83 if replaced, _, werr = w.WouldReplaceEvent(ev); werr != nil {
84 if errors.Is(werr, ErrOlderThanExisting) {
85 if kind.IsReplaceable(ev.Kind) {
86 err = errors.New("blocked: event is older than existing replaceable event")
87 } else {
88 err = errors.New("blocked: event is older than existing addressable event")
89 }
90 return
91 }
92 if errors.Is(werr, ErrMissingDTag) {
93 err = ErrMissingDTag
94 return
95 }
96 return
97 }
98 }
99
100 // Get the next sequence number for the event
101 serial, err := w.nextEventSerial()
102 if err != nil {
103 return
104 }
105
106 // Generate all indexes for the event
107 idxs, err := database.GetIndexesForEvent(ev, serial)
108 if err != nil {
109 return
110 }
111
112 // Serialize event to binary
113 eventDataBuf := new(bytes.Buffer)
114 ev.MarshalBinary(eventDataBuf)
115 eventData := eventDataBuf.Bytes()
116
117 // Determine storage strategy
118 smallEventThreshold := 1024 // Could be made configurable
119 isSmallEvent := len(eventData) <= smallEventThreshold
120 isReplaceableEvent := kind.IsReplaceable(ev.Kind)
121 isAddressableEvent := kind.IsParameterizedReplaceable(ev.Kind)
122
123 // Create serial type
124 ser = new(types.Uint40)
125 if err = ser.Set(serial); chk.E(err) {
126 return
127 }
128
129 // Start a transaction to save the event and all its indexes
130 // We need to include all object stores we'll write to
131 storesToWrite := []string{
132 string(indexes.IdPrefix),
133 string(indexes.FullIdPubkeyPrefix),
134 string(indexes.CreatedAtPrefix),
135 string(indexes.PubkeyPrefix),
136 string(indexes.KindPrefix),
137 string(indexes.KindPubkeyPrefix),
138 string(indexes.TagPrefix),
139 string(indexes.TagKindPrefix),
140 string(indexes.TagPubkeyPrefix),
141 string(indexes.TagKindPubkeyPrefix),
142 string(indexes.WordPrefix),
143 }
144
145 // Add event storage store
146 if isSmallEvent {
147 storesToWrite = append(storesToWrite, string(indexes.SmallEventPrefix))
148 } else {
149 storesToWrite = append(storesToWrite, string(indexes.EventPrefix))
150 }
151
152 // Add specialized stores if needed
153 if isAddressableEvent && isSmallEvent {
154 storesToWrite = append(storesToWrite, string(indexes.AddressableEventPrefix))
155 } else if isReplaceableEvent && isSmallEvent {
156 storesToWrite = append(storesToWrite, string(indexes.ReplaceableEventPrefix))
157 }
158
159 // Start transaction
160 tx, err := w.db.Transaction(idb.TransactionReadWrite, storesToWrite[0], storesToWrite[1:]...)
161 if err != nil {
162 return false, fmt.Errorf("failed to start transaction: %w", err)
163 }
164
165 // Save each index to its respective object store
166 for _, key := range idxs {
167 if len(key) < 3 {
168 continue
169 }
170 // Extract store name from 3-byte prefix
171 storeName := string(key[:3])
172
173 store, storeErr := tx.ObjectStore(storeName)
174 if storeErr != nil {
175 w.Logger.Warnf("SaveEvent: failed to get object store %s: %v", storeName, storeErr)
176 continue
177 }
178
179 // Use the full key as the IndexedDB key, empty value
180 keyJS := bytesToSafeValue(key)
181 _, putErr := store.PutKey(keyJS, safejs.Null())
182 if putErr != nil {
183 w.Logger.Warnf("SaveEvent: failed to put index %s: %v", storeName, putErr)
184 }
185 }
186
187 // Store the event data
188 if isSmallEvent {
189 // Small event: store inline with sev prefix
190 // Format: sev|serial|size_uint16|event_data
191 keyBuf := new(bytes.Buffer)
192 if err = indexes.SmallEventEnc(ser).MarshalWrite(keyBuf); chk.E(err) {
193 return
194 }
195 // Append size as uint16 big-endian
196 sizeBytes := []byte{byte(len(eventData) >> 8), byte(len(eventData))}
197 keyBuf.Write(sizeBytes)
198 keyBuf.Write(eventData)
199
200 store, storeErr := tx.ObjectStore(string(indexes.SmallEventPrefix))
201 if storeErr == nil {
202 keyJS := bytesToSafeValue(keyBuf.Bytes())
203 store.PutKey(keyJS, safejs.Null())
204 }
205 } else {
206 // Large event: store separately with evt prefix
207 keyBuf := new(bytes.Buffer)
208 if err = indexes.EventEnc(ser).MarshalWrite(keyBuf); chk.E(err) {
209 return
210 }
211
212 store, storeErr := tx.ObjectStore(string(indexes.EventPrefix))
213 if storeErr == nil {
214 keyJS := bytesToSafeValue(keyBuf.Bytes())
215 valueJS := bytesToSafeValue(eventData)
216 store.PutKey(keyJS, valueJS)
217 }
218 }
219
220 // Store specialized keys for replaceable/addressable events
221 if isAddressableEvent && isSmallEvent {
222 dTag := ev.Tags.GetFirst([]byte("d"))
223 if dTag != nil {
224 pubHash := new(types.PubHash)
225 pubHash.FromPubkey(ev.Pubkey)
226 kindVal := new(types.Uint16)
227 kindVal.Set(ev.Kind)
228 dTagHash := new(types.Ident)
229 dTagHash.FromIdent(dTag.Value())
230
231 keyBuf := new(bytes.Buffer)
232 if err = indexes.AddressableEventEnc(pubHash, kindVal, dTagHash).MarshalWrite(keyBuf); chk.E(err) {
233 return
234 }
235 sizeBytes := []byte{byte(len(eventData) >> 8), byte(len(eventData))}
236 keyBuf.Write(sizeBytes)
237 keyBuf.Write(eventData)
238
239 store, storeErr := tx.ObjectStore(string(indexes.AddressableEventPrefix))
240 if storeErr == nil {
241 keyJS := bytesToSafeValue(keyBuf.Bytes())
242 store.PutKey(keyJS, safejs.Null())
243 }
244 }
245 } else if isReplaceableEvent && isSmallEvent {
246 pubHash := new(types.PubHash)
247 pubHash.FromPubkey(ev.Pubkey)
248 kindVal := new(types.Uint16)
249 kindVal.Set(ev.Kind)
250
251 keyBuf := new(bytes.Buffer)
252 if err = indexes.ReplaceableEventEnc(pubHash, kindVal).MarshalWrite(keyBuf); chk.E(err) {
253 return
254 }
255 sizeBytes := []byte{byte(len(eventData) >> 8), byte(len(eventData))}
256 keyBuf.Write(sizeBytes)
257 keyBuf.Write(eventData)
258
259 store, storeErr := tx.ObjectStore(string(indexes.ReplaceableEventPrefix))
260 if storeErr == nil {
261 keyJS := bytesToSafeValue(keyBuf.Bytes())
262 store.PutKey(keyJS, safejs.Null())
263 }
264 }
265
266 // Commit transaction
267 if err = tx.Await(c); err != nil {
268 return false, fmt.Errorf("failed to commit transaction: %w", err)
269 }
270
271 w.Logger.Debugf("SaveEvent: saved event %x (kind %d, %d bytes, %d indexes)",
272 ev.ID[:8], ev.Kind, len(eventData), len(idxs))
273
274 return
275 }
276
277 // WouldReplaceEvent checks if the provided event would replace existing events
278 func (w *W) WouldReplaceEvent(ev *event.E) (bool, types.Uint40s, error) {
279 // Only relevant for replaceable or parameterized replaceable kinds
280 if !(kind.IsReplaceable(ev.Kind) || kind.IsParameterizedReplaceable(ev.Kind)) {
281 return false, nil, nil
282 }
283
284 // Build filter for existing events
285 var f interface{}
286 if kind.IsReplaceable(ev.Kind) {
287 // For now, simplified check - would need full filter implementation
288 return false, nil, nil
289 } else {
290 // Parameterized replaceable requires 'd' tag
291 dTag := ev.Tags.GetFirst([]byte("d"))
292 if dTag == nil {
293 return false, nil, ErrMissingDTag
294 }
295 // Simplified - full implementation would query existing events
296 _ = f
297 }
298
299 // Simplified implementation - assume no conflicts for now
300 // Full implementation would query the database and compare timestamps
301 return false, nil, nil
302 }
303
304 // GetSerialById looks up the serial number for an event ID
305 func (w *W) GetSerialById(id []byte) (ser *types.Uint40, err error) {
306 if len(id) != 32 {
307 return nil, errors.New("invalid event ID length")
308 }
309
310 // Create ID hash
311 idHash := new(types.IdHash)
312 if err = idHash.FromId(id); chk.E(err) {
313 return nil, err
314 }
315
316 // Build the prefix to search for
317 keyBuf := new(bytes.Buffer)
318 indexes.IdEnc(idHash, nil).MarshalWrite(keyBuf)
319 prefix := keyBuf.Bytes()[:11] // 3 prefix + 8 id hash
320
321 // Search in the eid object store
322 tx, err := w.db.Transaction(idb.TransactionReadOnly, string(indexes.IdPrefix))
323 if err != nil {
324 return nil, err
325 }
326
327 store, err := tx.ObjectStore(string(indexes.IdPrefix))
328 if err != nil {
329 return nil, err
330 }
331
332 // Use cursor to find matching key
333 cursorReq, err := store.OpenCursor(idb.CursorNext)
334 if err != nil {
335 return nil, err
336 }
337
338 err = cursorReq.Iter(w.ctx, func(cursor *idb.CursorWithValue) error {
339 keyVal, keyErr := cursor.Key()
340 if keyErr != nil {
341 return keyErr
342 }
343
344 keyBytes := safeValueToBytes(keyVal)
345 if len(keyBytes) >= len(prefix) && bytes.HasPrefix(keyBytes, prefix) {
346 // Found matching key, extract serial from last 5 bytes
347 if len(keyBytes) >= 16 { // 3 + 8 + 5
348 ser = new(types.Uint40)
349 ser.UnmarshalRead(bytes.NewReader(keyBytes[11:16]))
350 return errors.New("found") // Stop iteration
351 }
352 }
353
354 return cursor.Continue()
355 })
356
357 if ser != nil {
358 return ser, nil
359 }
360 if err != nil && err.Error() != "found" {
361 return nil, err
362 }
363
364 return nil, errors.New("id not found in database")
365 }
366
367 // GetSerialsByIds looks up serial numbers for multiple event IDs
368 func (w *W) GetSerialsByIds(ids *tag.T) (serials map[string]*types.Uint40, err error) {
369 serials = make(map[string]*types.Uint40)
370
371 if ids == nil {
372 return
373 }
374
375 for i := 1; i < ids.Len(); i++ {
376 idBytes := ids.T[i]
377 if len(idBytes) == 64 {
378 // Hex encoded ID
379 var decoded []byte
380 decoded, err = hex.Dec(string(idBytes))
381 if err != nil {
382 continue
383 }
384 idBytes = decoded
385 }
386
387 if len(idBytes) == 32 {
388 var ser *types.Uint40
389 ser, err = w.GetSerialById(idBytes)
390 if err == nil && ser != nil {
391 serials[hex.Enc(idBytes)] = ser
392 }
393 }
394 }
395
396 err = nil
397 return
398 }
399
400 // GetSerialsByIdsWithFilter looks up serial numbers with a filter function
401 func (w *W) GetSerialsByIdsWithFilter(ids *tag.T, fn func(ev *event.E, ser *types.Uint40) bool) (serials map[string]*types.Uint40, err error) {
402 allSerials, err := w.GetSerialsByIds(ids)
403 if err != nil {
404 return nil, err
405 }
406
407 if fn == nil {
408 return allSerials, nil
409 }
410
411 serials = make(map[string]*types.Uint40)
412 for idHex, ser := range allSerials {
413 ev, fetchErr := w.FetchEventBySerial(ser)
414 if fetchErr != nil {
415 continue
416 }
417 if fn(ev, ser) {
418 serials[idHex] = ser
419 }
420 }
421
422 return serials, nil
423 }
424