Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
NotificationServiceV2.cs
1using System;
3using System.Globalization;
5using System.Text;
6using System.Text.Json;
7using System.Threading;
8using System.Threading.Tasks;
9using System.Linq;
12using Waher.Events;
15
17{
22 {
23 private const int DefaultSchemaVersion = 1;
24 private const int MaxPerChannel = 100;
25 private const int MaxTotal = 1000;
26 private readonly INotificationIntentRouter intentRouter;
27 private readonly INotificationFilterRegistry filterRegistry;
28 private readonly INotificationRenderer renderer;
29 private readonly List<Expectation> expectations = [];
30 private readonly Dictionary<string, int> channelCounts = new(StringComparer.OrdinalIgnoreCase);
31 private readonly Queue<NotificationIntent> pendingRoutes = new();
32
40 {
41 this.intentRouter = IntentRouter;
42 this.filterRegistry = FilterRegistry;
43 this.renderer = Renderer;
44 }
45
49 public event EventHandlerAsync<NotificationRecordEventArgs>? OnNotificationAdded;
50
54 public IReadOnlyDictionary<string, int> ChannelCounts
55 {
56 get
57 {
58 lock (this.channelCounts)
59 {
60 return new Dictionary<string, int>(this.channelCounts);
61 }
62 }
63 }
64
69 public IDisposable AddIgnoreFilter(Func<NotificationIntent, NotificationFilterDecision> Predicate)
70 {
71 return this.filterRegistry.AddFilter(Predicate);
72 }
73
75 public string ComputeId(NotificationIntent Intent, NotificationSource Source)
76 {
77 return this.ResolveId(Intent, Intent.Channel ?? string.Empty, Source);
78 }
79
85 public override async Task Load(bool IsResuming, CancellationToken CancellationToken)
86 {
87 await base.Load(IsResuming, CancellationToken);
88 await this.RebuildChannelCountsAsync(CancellationToken);
89 }
90
98 public async Task AddAsync(NotificationIntent Intent, NotificationSource Source, string? RawPayload, CancellationToken CancellationToken)
99 {
100 NotificationFilterDecision filterDecision = this.filterRegistry.ShouldIgnore(Intent, false, CancellationToken);
101 bool shouldPersist = Intent.Presentation is NotificationPresentation.RenderAndStore or NotificationPresentation.StoreOnly;
102 bool shouldRender = Intent.Presentation is NotificationPresentation.RenderAndStore or NotificationPresentation.RenderOnly;
103 if (filterDecision.IgnoreStore)
104 shouldPersist = false;
105 if (filterDecision.IgnoreRender)
106 shouldRender = false;
107 NotificationRecord Record = this.CreateRecord(Intent, Source, RawPayload);
108
109 if (!shouldPersist)
110 {
111 this.TrySatisfyExpectations(Record);
112 if (shouldRender)
113 {
114 await this.renderer.RenderAsync(Intent, CancellationToken);
115 }
116 ServiceRef.LogService.LogInformational(
117 "Notification ignored",
118 this.BuildLogProperties(Intent, Source, Record.Id)
119 .Append(new KeyValuePair<string, object?>("Reason", "IgnoreStoreOrPresentation"))
120 .ToArray());
121 return;
122 }
123
124 NotificationRecord? ExistingRecord = await this.LoadByCorrelationdAsync(Record.CorrelationId);
125 if (ExistingRecord is not null && this.AreMergeCompatible(ExistingRecord, Record))
126 {
127 ExistingRecord.TimestampCreated = DateTime.UtcNow;
128 ExistingRecord.Title = Record.Title;
129 ExistingRecord.Body = Record.Body;
130 ExistingRecord.Action = Record.Action;
131 ExistingRecord.EntityId = Record.EntityId;
132 ExistingRecord.CorrelationId = Record.CorrelationId;
133 ExistingRecord.ExtrasJson = Record.ExtrasJson;
134 ExistingRecord.RawPayload = Record.RawPayload ?? ExistingRecord.RawPayload;
135 ExistingRecord.SchemaVersion = Record.SchemaVersion;
136 ExistingRecord.Source = Record.Source;
137 ExistingRecord.State = NotificationState.New;
138 ExistingRecord.ReadAt = null;
139 ExistingRecord.ConsumedAt = null;
140 ExistingRecord.Presentation = Record.Presentation;
141 ExistingRecord.DeliveredAt = DateTime.UtcNow;
142 ExistingRecord.OccurrenceCount = ExistingRecord.OccurrenceCount > 0 ? ExistingRecord.OccurrenceCount + 1 : 1;
143
144 await Database.Update(ExistingRecord);
145 ServiceRef.LogService.LogInformational(
146 "Notification merged",
147 this.BuildLogProperties(Intent, Source, ExistingRecord.Id)
148 .Append(new KeyValuePair<string, object?>("OccurrenceCount", ExistingRecord.OccurrenceCount))
149 .ToArray());
150 await this.RaiseAdded(ExistingRecord);
151 if (shouldRender)
152 {
153 await this.renderer.RenderAsync(Intent, CancellationToken);
154 }
155 return;
156 }
157 else if (ExistingRecord is not null)
158 {
159 Record.Id = this.ResolveIdWithSalt(Intent, Source);
160 }
161
162 Record.State = NotificationState.Delivered;
163 Record.DeliveredAt = DateTime.UtcNow;
164
165 await Database.Insert(Record);
166 this.IncrementChannelCount(Record.Channel);
167 ServiceRef.LogService.LogInformational(
168 "Notification stored",
169 this.BuildLogProperties(Intent, Source, Record.Id)
170 .Append(new KeyValuePair<string, object?>("OccurrenceCount", Record.OccurrenceCount))
171 .ToArray());
172 await this.RaiseAdded(Record);
173 await this.PruneAsync(CancellationToken);
174 if (shouldRender)
175 {
176 await this.renderer.RenderAsync(Intent, CancellationToken);
177 }
178 }
179
185 public async Task MarkReadAsync(string Id, CancellationToken CancellationToken)
186 {
187 NotificationRecord? record = await this.LoadByIdAsync(Id);
188 if (record is null)
189 return;
190
191 if (record.State == NotificationState.Consumed || record.State == NotificationState.Read)
192 return;
193
194 record.State = NotificationState.Read;
195 record.ReadAt = DateTime.UtcNow;
196 await Database.Update(record);
197 await this.RaiseAdded(record);
198 }
199
205 public async Task ConsumeAsync(string Id, CancellationToken CancellationToken)
206 {
207 NotificationRecord? Record = await this.MarkConsumedCoreAsync(Id);
208 if (Record is null)
209 return;
210
211 NotificationIntent Intent = this.ToIntent(Record);
212 NotificationRouteResult result = await this.intentRouter.RouteAsync(Intent, true, CancellationToken);
213 if (result == NotificationRouteResult.Deferred)
214 {
215 lock (this.pendingRoutes)
216 {
217 this.pendingRoutes.Enqueue(Intent);
218 }
219 }
220 }
221
227 public async Task MarkConsumedAsync(string Id, CancellationToken CancellationToken)
228 {
229 _ = CancellationToken;
230 await this.MarkConsumedCoreAsync(Id);
231 }
232
233 private async Task<NotificationRecord?> MarkConsumedCoreAsync(string Id)
234 {
235 NotificationRecord? Record = await this.LoadByIdAsync(Id);
236 if (Record is null)
237 return null;
238
239 Record.State = NotificationState.Consumed;
240 Record.ConsumedAt = DateTime.UtcNow;
241 Record.OccurrenceCount = 1;
242 await Database.Update(Record);
243 await this.RaiseAdded(Record);
244 return Record;
245 }
246
253 public async Task<IReadOnlyList<NotificationRecord>> GetAsync(NotificationQuery Query, CancellationToken CancellationToken)
254 {
255 Filter filter = new(Query.Channels, Query.States);
256 int Skip = Query.Skip ?? 0;
257 int Limit = Query.Limit ?? int.MaxValue;
258 int Matched = 0;
259 List<NotificationRecord> Results = new();
260
261 IEnumerable<NotificationRecord> FromDb = await Database.Find<NotificationRecord>(nameof(NotificationRecord.TimestampCreated));
262 IEnumerable<NotificationRecord> Ordered = FromDb.OrderByDescending(Record => Record.TimestampCreated);
263
264 foreach (NotificationRecord Record in Ordered)
265 {
266 if (!filter.Matches(Record))
267 continue;
268
269 if (Matched < Skip)
270 {
271 Matched++;
272 continue;
273 }
274
275 Results.Add(Record);
276 Matched++;
277
278 if (Results.Count >= Limit)
279 break;
280 }
281
282 return Results;
283 }
284
289 public async Task PruneAsync(CancellationToken CancellationToken)
290 {
291 List<NotificationRecord> All = new();
292 Dictionary<string, List<NotificationRecord>> PerChannel = new(StringComparer.OrdinalIgnoreCase);
293
294 IEnumerable<NotificationRecord> Records = await Database.Find<NotificationRecord>(nameof(NotificationRecord.TimestampCreated));
295 foreach (NotificationRecord Record in Records)
296 {
297 All.Add(Record);
298 if (!PerChannel.TryGetValue(Record.Channel, out List<NotificationRecord>? List))
299 {
300 List = new List<NotificationRecord>();
301 PerChannel[Record.Channel] = List;
302 }
303 List.Add(Record);
304 }
305
306 List<NotificationRecord> ToDelete = new();
307
308 foreach (KeyValuePair<string, List<NotificationRecord>> Pair in PerChannel)
309 {
310 List<NotificationRecord> Ordered = Pair.Value.OrderBy(R => R.TimestampCreated).ToList();
311 int Excess = Ordered.Count - MaxPerChannel;
312 if (Excess > 0)
313 ToDelete.AddRange(Ordered.Take(Excess));
314 }
315
316 if (All.Count - ToDelete.Count > MaxTotal)
317 {
318 List<NotificationRecord> Ordered = All.OrderBy(R => R.TimestampCreated).ToList();
319 int Need = (All.Count - MaxTotal) - ToDelete.Count;
320 if (Need > 0)
321 ToDelete.AddRange(Ordered.Take(Need));
322 }
323
324 foreach (NotificationRecord Record in ToDelete)
325 {
326 await Database.Delete(Record);
327 this.DecrementChannelCount(Record.Channel);
328 }
329 }
330
336 public async Task DeleteAsync(IEnumerable<string> Ids, CancellationToken CancellationToken)
337 {
338 if (Ids is null)
339 return;
340
341 foreach (string Id in Ids)
342 {
343 NotificationRecord? Record = await this.LoadByIdAsync(Id);
344 if (Record is null)
345 continue;
346
347 await Database.Delete(Record);
348 this.DecrementChannelCount(Record.Channel);
349 await this.RaiseAdded(Record);
350 }
351 }
352
354 public async Task ProcessPendingAsync(CancellationToken CancellationToken)
355 {
356 while (true)
357 {
358 NotificationIntent? intent = null;
359 lock (this.pendingRoutes)
360 {
361 if (this.pendingRoutes.Count > 0)
362 intent = this.pendingRoutes.Dequeue();
363 }
364
365 if (intent is null)
366 break;
367
368 await this.intentRouter.RouteAsync(intent, true, CancellationToken);
369 }
370 }
371
372 private NotificationRecord CreateRecord(NotificationIntent Intent, NotificationSource Source, string? RawPayload)
373 {
374 string Channel = Intent.Channel ?? string.Empty;
375 string ActionString = Intent.Action.ToString();
376 string ExtrasJson = JsonSerializer.Serialize(Intent.Extras);
377
378 string Id = this.ResolveId(Intent, Channel, Source);
379
380 return new NotificationRecord
381 {
382 Id = Id,
383 Channel = Channel,
384 Title = Intent.Title,
385 Body = Intent.Body,
386 Action = ActionString,
387 EntityId = Intent.EntityId,
388 CorrelationId = Intent.CorrelationId,
389 ExtrasJson = ExtrasJson,
390 RawPayload = RawPayload,
391 SchemaVersion = Intent.Version > 0 ? Intent.Version : DefaultSchemaVersion,
392 TimestampCreated = DateTime.UtcNow,
393 State = NotificationState.New,
394 Source = Source,
395 Presentation = Intent.Presentation
396 };
397 }
398
399 private string ResolveId(NotificationIntent Intent, string Channel, NotificationSource Source)
400 {
401 StringBuilder builder = new();
402 string correlation = Intent.CorrelationId ?? string.Empty;
403
404 builder.Append(Channel);
405 builder.Append('|');
406 builder.Append(Intent.Action.ToString());
407 builder.Append('|');
408 builder.Append(Intent.EntityId ?? string.Empty);
409 builder.Append('|');
410 builder.Append(correlation);
411 builder.Append('|');
412 builder.Append(Intent.Title ?? string.Empty);
413 builder.Append('|');
414 builder.Append(Intent.Body ?? string.Empty);
415 builder.Append('|');
416 builder.Append(this.SerializeExtras(Intent.Extras));
417 builder.Append('|');
418 builder.Append(Source.ToString());
419
420 byte[] Input = Encoding.UTF8.GetBytes(builder.ToString());
421 byte[] Hash = SHA256.HashData(Input);
422 return Convert.ToHexString(Hash);
423 }
424
425 private async Task<NotificationRecord?> LoadByIdAsync(string? Id)
426 {
427 if (Id is null)
428 return null;
429
430 try
431 {
432 return await Database.FindFirstDeleteRest<NotificationRecord>(new FilterFieldEqualTo(nameof(NotificationRecord.Id), Id));
433 }
434 catch (KeyNotFoundException)
435 {
436 return null;
437 }
438 }
439
440 private async Task<NotificationRecord?> LoadByCorrelationdAsync(string CorrelationId)
441 {
442 if (CorrelationId is null)
443 return null;
444
445 try
446 {
447 return await Database.FindFirstDeleteRest<NotificationRecord>(new FilterFieldEqualTo(nameof(NotificationRecord.CorrelationId), CorrelationId));
448 }
449 catch (KeyNotFoundException)
450 {
451 return null;
452 }
453 }
454
455 private async Task RaiseAdded(NotificationRecord Record)
456 {
457 EventHandlerAsync<NotificationRecordEventArgs>? Handler = this.OnNotificationAdded;
458 if (Handler is not null)
459 await Handler.Raise(this, new NotificationRecordEventArgs(Record));
460
461 this.TrySatisfyExpectations(Record);
462 }
463
471 public async Task<NotificationRecord?> WaitForAsync(Func<NotificationRecord, bool> Predicate, TimeSpan Timeout, CancellationToken CancellationToken)
472 {
473 IEnumerable<NotificationRecord> FromDb = await Database.Find<NotificationRecord>(nameof(NotificationRecord.TimestampCreated));
474 foreach (NotificationRecord Record in FromDb)
475 {
476 if (Predicate(Record))
477 return Record;
478 }
479
480 TaskCompletionSource<NotificationRecord?> Tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
481 Expectation expectation = new(Predicate, DateTime.UtcNow.Add(Timeout), Tcs, false);
482
483 lock (this.expectations)
484 {
485 this.expectations.Add(expectation);
486 }
487
488 using CancellationTokenRegistration reg = CancellationToken.Register(() => Tcs.TrySetCanceled(CancellationToken));
489 Task delayTask = Task.Delay(Timeout, CancellationToken);
490 Task finished = await Task.WhenAny(Tcs.Task, delayTask);
491
492 lock (this.expectations)
493 {
494 this.expectations.Remove(expectation);
495 }
496
497 if (finished == Tcs.Task)
498 return await Tcs.Task;
499
500 return null;
501 }
502
509 public Task ExpectAsync(Func<NotificationRecord, bool> Predicate, TimeSpan Timeout, CancellationToken CancellationToken)
510 {
511 Expectation expectation = new(Predicate, DateTime.UtcNow.Add(Timeout), null, true);
512
513 lock (this.expectations)
514 {
515 this.expectations.Add(expectation);
516 }
517
518 _ = Task.Delay(Timeout, CancellationToken).ContinueWith(_ =>
519 {
520 lock (this.expectations)
521 {
522 this.expectations.Remove(expectation);
523 }
524 }, CancellationToken.None, TaskContinuationOptions.RunContinuationsAsynchronously, TaskScheduler.Default);
525
526 return Task.CompletedTask;
527 }
528
529 private void TrySatisfyExpectations(NotificationRecord Record)
530 {
531 List<Expectation> Matches = new();
532
533 lock (this.expectations)
534 {
535 DateTime now = DateTime.UtcNow;
536 for (int i = this.expectations.Count - 1; i >= 0; i--)
537 {
538 Expectation exp = this.expectations[i];
539 if (exp.ExpiresAt < now)
540 {
541 this.expectations.RemoveAt(i);
542 continue;
543 }
544
545 if (exp.Predicate(Record))
546 {
547 this.expectations.RemoveAt(i);
548 Matches.Add(exp);
549 }
550 }
551 }
552
553 foreach (Expectation exp in Matches)
554 {
555 if (exp.Source is not null)
556 exp.Source.TrySetResult(Record);
557
558 if (exp.RouteOnMatch)
559 _ = this.ConsumeAsync(Record.Id, CancellationToken.None);
560 }
561 }
562
563 private NotificationIntent ToIntent(NotificationRecord Record)
564 {
565 Dictionary<string, string>? extras = JsonSerializer.Deserialize<Dictionary<string, string>>(Record.ExtrasJson);
566
567 return new NotificationIntent
568 {
569 Action = Enum.TryParse(Record.Action, out NotificationAction action) ? action : NotificationAction.Unknown,
570 EntityId = Record.EntityId,
571 Channel = Record.Channel,
572 Title = Record.Title,
573 Body = Record.Body,
574 Extras = extras ?? new Dictionary<string, string>(),
575 Version = Record.SchemaVersion,
576 Presentation = Record.Presentation
577 };
578 }
579
580 private sealed record Expectation(Func<NotificationRecord, bool> Predicate, DateTime ExpiresAt, TaskCompletionSource<NotificationRecord?>? Source, bool RouteOnMatch);
581
582 private void IncrementChannelCount(string Channel)
583 {
584 lock (this.channelCounts)
585 {
586 this.channelCounts.TryGetValue(Channel, out int count);
587 this.channelCounts[Channel] = count + 1;
588 }
589 }
590
591 private void DecrementChannelCount(string Channel)
592 {
593 lock (this.channelCounts)
594 {
595 if (this.channelCounts.TryGetValue(Channel, out int count) && count > 0)
596 this.channelCounts[Channel] = count - 1;
597 }
598 }
599
600 private async Task RebuildChannelCountsAsync(CancellationToken CancellationToken)
601 {
602 lock (this.channelCounts)
603 {
604 this.channelCounts.Clear();
605 }
606
607 IEnumerable<NotificationRecord> records = await Database.Find<NotificationRecord>(nameof(NotificationRecord.TimestampCreated));
608 foreach (NotificationRecord record in records)
609 {
610 CancellationToken.ThrowIfCancellationRequested();
611 this.IncrementChannelCount(record.Channel);
612 }
613 }
614
615 private string SerializeExtras(Dictionary<string, string> extras)
616 {
617 if (extras is null || extras.Count == 0)
618 return string.Empty;
619
620 StringBuilder builder = new StringBuilder();
621 foreach (KeyValuePair<string, string> pair in extras.OrderBy(k => k.Key, StringComparer.Ordinal))
622 {
623 builder.Append(pair.Key);
624 builder.Append('=');
625 builder.Append(pair.Value);
626 builder.Append(';');
627 }
628
629 return builder.ToString();
630 }
631
632 private IEnumerable<KeyValuePair<string, object?>> BuildLogProperties(NotificationIntent Intent, NotificationSource Source, string notificationId)
633 {
634 yield return new KeyValuePair<string, object?>("NotificationId", notificationId);
635 yield return new KeyValuePair<string, object?>("Channel", Intent.Channel ?? string.Empty);
636 yield return new KeyValuePair<string, object?>("Action", Intent.Action.ToString());
637 yield return new KeyValuePair<string, object?>("EntityId", Intent.EntityId ?? string.Empty);
638 yield return new KeyValuePair<string, object?>("CorrelationId", Intent.CorrelationId ?? string.Empty);
639 yield return new KeyValuePair<string, object?>("Presentation", Intent.Presentation.ToString());
640 yield return new KeyValuePair<string, object?>("Source", Source.ToString());
641 }
642
643 private bool AreMergeCompatible(NotificationRecord existing, NotificationRecord incoming)
644 {
645 return string.Equals(existing.Action, incoming.Action, StringComparison.Ordinal) &&
646 string.Equals(existing.EntityId, incoming.EntityId, StringComparison.Ordinal) &&
647 string.Equals(existing.CorrelationId ?? string.Empty, incoming.CorrelationId ?? string.Empty, StringComparison.Ordinal);
648 }
649
650 private string ResolveIdWithSalt(NotificationIntent intent, NotificationSource source)
651 {
652 string salt = Guid.NewGuid().ToString("N");
653 StringBuilder builder = new StringBuilder();
654 builder.Append(intent.Channel ?? string.Empty);
655 builder.Append('|');
656 builder.Append(intent.Action.ToString());
657 builder.Append('|');
658 builder.Append(intent.EntityId ?? string.Empty);
659 builder.Append('|');
660 builder.Append(intent.CorrelationId ?? string.Empty);
661 builder.Append('|');
662 builder.Append(intent.Title ?? string.Empty);
663 builder.Append('|');
664 builder.Append(intent.Body ?? string.Empty);
665 builder.Append('|');
666 builder.Append(this.SerializeExtras(intent.Extras));
667 builder.Append('|');
668 builder.Append(source.ToString());
669 builder.Append('|');
670 builder.Append(salt);
671
672 byte[] input = Encoding.UTF8.GetBytes(builder.ToString());
673 byte[] hash = SHA256.HashData(input);
674 return Convert.ToHexString(hash);
675 }
676
677
678 private sealed class Filter
679 {
680 private readonly HashSet<string>? channels;
681 private readonly HashSet<NotificationState>? states;
682
683 public Filter(IReadOnlyList<string>? Channels, IReadOnlyList<NotificationState>? States)
684 {
685 if (Channels is not null && Channels.Count > 0)
686 this.channels = new HashSet<string>(Channels, StringComparer.OrdinalIgnoreCase);
687
688 if (States is not null && States.Count > 0)
689 this.states = new HashSet<NotificationState>(States);
690 }
691
692 public bool Matches(NotificationRecord Record)
693 {
694 if (this.channels is not null && !this.channels.Contains(Record.Channel))
695 return false;
696
697 if (this.states is not null && !this.states.Contains(Record.State))
698 return false;
699
700 return true;
701 }
702 }
703 }
704}
bool IsResuming
If App is resuming service.
Represents a filter decision for notification handling.
bool IgnoreStore
Gets a value indicating whether storage should be suppressed.
bool IgnoreRender
Gets a value indicating whether rendering should be suppressed.
Platform-neutral intent describing how to route a notification.
string? EntityId
Gets or sets an entity identifier associated with the action.
string? CorrelationId
Gets or sets an optional correlation identifier.
string? Body
Gets or sets the message body to display.
string Title
Gets or sets the title to display.
NotificationAction Action
Gets or sets the intended action.
string? Channel
Gets or sets the push channel identifier.
Dictionary< string, string > Extras
Gets or sets extra data used for routing.
Query options for retrieving notifications.
IReadOnlyList< NotificationState >? States
Gets states to include. Empty/null means all.
IReadOnlyList< string >? Channels
Gets channels to include. Empty/null means all.
string? RawPayload
Gets or sets the raw payload received from the transport.
NotificationSource Source
Gets or sets the notification source.
string? CorrelationId
Gets or sets the correlation identifier used for deduplication.
int SchemaVersion
Gets or sets the schema version used when parsing the payload.
string Channel
Gets or sets the channel identifier.
NotificationPresentation Presentation
Gets or sets the presentation preference.
string? EntityId
Gets or sets the entity identifier associated with the notification.
DateTime TimestampCreated
Gets or sets the timestamp when the notification was created.
string Id
Gets or sets the stable notification identifier.
int OccurrenceCount
Gets or sets how many times this notification intent has been observed.
NotificationState State
Gets or sets the notification state.
string? Body
Gets or sets the notification body.
string Title
Gets or sets the notification title.
string Action
Gets or sets the action to execute when consumed.
string ExtrasJson
Gets or sets serialized extras payload (JSON).
Redesigned notification service with Waher persistence.
IDisposable AddIgnoreFilter(Func< NotificationIntent, NotificationFilterDecision > Predicate)
Adds a runtime ignore filter. Dispose the handle to remove it.
async Task< IReadOnlyList< NotificationRecord > > GetAsync(NotificationQuery Query, CancellationToken CancellationToken)
Retrieves notifications that match the query.
async Task ProcessPendingAsync(CancellationToken CancellationToken)
Attempts to route any pending deferred intents.
async Task< NotificationRecord?> WaitForAsync(Func< NotificationRecord, bool > Predicate, TimeSpan Timeout, CancellationToken CancellationToken)
Awaits a notification matching the predicate within a timeout.
string ComputeId(NotificationIntent Intent, NotificationSource Source)
Computes the stable identifier for an intent using the same logic as storage. Stable identifier.
EventHandlerAsync< NotificationRecordEventArgs >? OnNotificationAdded
Event raised when a notification is added or updated.
override async Task Load(bool IsResuming, CancellationToken CancellationToken)
Loads the service.
async Task PruneAsync(CancellationToken CancellationToken)
Applies retention pruning to stored notifications.
async Task MarkConsumedAsync(string Id, CancellationToken CancellationToken)
Marks a notification as consumed without routing it.
IReadOnlyDictionary< string, int > ChannelCounts
Current counts per channel.
async Task ConsumeAsync(string Id, CancellationToken CancellationToken)
Marks a notification as consumed and updates state.
async Task DeleteAsync(IEnumerable< string > Ids, CancellationToken CancellationToken)
Deletes notifications by identifier.
async Task MarkReadAsync(string Id, CancellationToken CancellationToken)
Marks a notification as read without consuming it.
async Task AddAsync(NotificationIntent Intent, NotificationSource Source, string? RawPayload, CancellationToken CancellationToken)
Adds or updates a notification based on the provided intent.
NotificationServiceV2(INotificationIntentRouter IntentRouter, INotificationFilterRegistry FilterRegistry, INotificationRenderer Renderer)
Initializes a new instance of the NotificationServiceV2 class.
Task ExpectAsync(Func< NotificationRecord, bool > Predicate, TimeSpan Timeout, CancellationToken CancellationToken)
Registers an expectation that will auto-route matching notifications when they arrive.
Base class that references services in the app.
Definition: ServiceRef.cs:43
static ILogService LogService
Log service.
Definition: ServiceRef.cs:214
Static interface for database persistence. In order to work, a database provider has to be assigned t...
Definition: Database.cs:21
static async Task Update(object Object)
Updates an object in the database.
Definition: Database.cs:1211
static async Task Delete(object Object)
Deletes an object in the database.
Definition: Database.cs:1291
static Task< IEnumerable< object > > Find(string Collection, params string[] SortOrder)
Finds objects in a given collection.
Definition: Database.cs:238
static async Task Insert(object Object)
Inserts an object into the default collection of the database.
Definition: Database.cs:97
This filter selects objects that have a named field equal to a given value.
Base class for all filter classes.
Definition: Filter.cs:15
Interface for the redesigned notification service.
Renders local notifications on the platform.
Definition: ImplTypes.g.cs:58
NotificationState
Notification lifecycle state.
NotificationRouteResult
Possible outcomes when routing a notification intent.
NotificationAction
Actions that can be routed from notifications.
NotificationPresentation
Presentation preference for a notification intent.
NotificationSource
Describes the source producing a notification.
Action
The Action field indicates the action performed by the Reporting-MTA as a result of its attempt to de...