@ours.network/notifications 0.3.1-nightly.6 → 1.0.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.
@@ -11,6 +11,7 @@ export declare class NotificationViews {
11
11
  private get;
12
12
  private project;
13
13
  private emit;
14
+ private coverage;
14
15
  register(principal: string, input: Record<string, unknown>): {
15
16
  subscriptionEpoch: number;
16
17
  baselineId: string;
@@ -33,7 +33,7 @@ class Output {
33
33
  this.response = response;
34
34
  this.failed = failed;
35
35
  }
36
- send(envelope) {
36
+ send(envelope, admitted) {
37
37
  if (this.closed)
38
38
  return;
39
39
  const bytes = Buffer.byteLength(JSON.stringify(envelope));
@@ -41,6 +41,7 @@ class Output {
41
41
  this.failed();
42
42
  return;
43
43
  }
44
+ admitted?.();
44
45
  this.pending.push({ envelope, bytes });
45
46
  this.bytes += bytes;
46
47
  if (!this.active)
@@ -67,7 +68,9 @@ class Output {
67
68
  const { envelope, bytes } = this.pending.shift();
68
69
  this.bytes -= bytes;
69
70
  const encoded = Buffer.from(JSON.stringify(envelope));
70
- if (encoded.length + 32 <= FRAME)
71
+ if ('views' in envelope)
72
+ await this.write('resource-coverage', envelope);
73
+ else if (encoded.length + 32 <= FRAME)
71
74
  await this.write('resource', envelope);
72
75
  else {
73
76
  const binding = { protocolVersion: 1, id: `${envelope.viewId}:${envelope.subscriptionEpoch}:${envelope.sequence}`,
@@ -119,9 +122,8 @@ export class NotificationViews {
119
122
  };
120
123
  const output = new Output(response, close);
121
124
  const timer = setInterval(() => {
122
- for (const view of connection.views.values())
123
- this.emit(connection, view, 'coverage', { source: this.source(view.revision) });
124
- }, 2000);
125
+ this.coverage(connection);
126
+ }, 15000);
125
127
  timer.unref();
126
128
  const connection = { id, principal, user, response, output, controller, views: new Map(), epochs: new Map(), timer, close };
127
129
  this.connections.set(id, connection);
@@ -141,8 +143,7 @@ export class NotificationViews {
141
143
  continue;
142
144
  view.payload = payload;
143
145
  view.revision = captured.revision;
144
- this.emit(connection, view, 'snapshot', captured.payload);
145
- this.emit(connection, view, 'coverage', { source: this.source(captured.revision) });
146
+ this.emit(connection, view, 'snapshot', captured.payload, this.source(captured.revision));
146
147
  }
147
148
  }
148
149
  catch {
@@ -178,9 +179,21 @@ export class NotificationViews {
178
179
  return { payload: structuredClone({ items, unread: summary.unread, revision: summary.revision, nextCursor,
179
180
  summary, delivery: this.store.deliveryStatus(user) }), revision: summary.revision };
180
181
  }
181
- emit(connection, view, kind, payload) {
182
+ emit(connection, view, kind, payload, source) {
182
183
  connection.output.send({ protocolVersion: 1, producerGeneration: this.producerGeneration, viewId: view.descriptor.viewId,
183
- subscriptionEpoch: view.epoch, sequence: ++view.sequence, revision: view.revision, baselineId: view.baselineId, kind, payload });
184
+ subscriptionEpoch: view.epoch, sequence: view.sequence + 1, revision: view.revision, baselineId: view.baselineId, kind, payload, ...(source ? { source } : {}) }, () => { view.sequence++; if (source || kind === 'ready')
185
+ view.qualifiedAt = Date.now(); });
186
+ }
187
+ // A shared ordered batch retains each view's own captured revision.
188
+ coverage(connection) {
189
+ const now = Date.now(), eligible = [...connection.views.values()].filter(view => now - view.qualifiedAt >= 15000);
190
+ if (!eligible.length)
191
+ return;
192
+ const views = eligible.map(view => ({ protocolVersion: 1, producerGeneration: this.producerGeneration, viewId: view.descriptor.viewId, subscriptionEpoch: view.epoch, baselineId: view.baselineId, sequence: view.sequence + 1, revision: view.revision, source: this.source(view.revision) }));
193
+ connection.output.send({ protocolVersion: 1, producerGeneration: this.producerGeneration, views }, () => { for (const view of eligible) {
194
+ view.sequence++;
195
+ view.qualifiedAt = now;
196
+ } });
184
197
  }
185
198
  register(principal, input) {
186
199
  const connection = this.get(String(input.connectionId), principal), value = descriptor(input.descriptor), epoch = input.subscriptionEpoch;
@@ -192,10 +205,10 @@ export class NotificationViews {
192
205
  throw new HttpError(503, 'resource view capacity');
193
206
  const baseline = this.project(connection.user, value);
194
207
  const view = { descriptor: value, epoch: epoch, baselineId: randomUUID(), sequence: 0, revision: baseline.revision,
195
- baseline, payload: contentKey(baseline.payload) };
208
+ baseline, qualifiedAt: Date.now(), payload: contentKey(baseline.payload) };
196
209
  connection.views.set(value.viewId, view);
197
210
  connection.epochs.set(value.viewId, view.epoch);
198
- this.emit(connection, view, 'snapshot', baseline.payload);
211
+ this.emit(connection, view, 'snapshot', baseline.payload, this.source(baseline.revision));
199
212
  this.emit(connection, view, 'ready', { baselineId: view.baselineId, source: this.source(baseline.revision) });
200
213
  return { subscriptionEpoch: view.epoch, baselineId: view.baselineId };
201
214
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/notifications",
3
- "version": "0.3.1-nightly.6",
3
+ "version": "1.0.0",
4
4
  "description": "Authenticated web push delivery and shared notification inbox",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",