@observertc/observer-js 1.3.0-rc.2 → 1.3.0-rc.3

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/dist/index.mjs CHANGED
@@ -44,6 +44,46 @@ function setObserverLogger(logger13) {
44
44
  // src/ObservedCall.ts
45
45
  import { EventEmitter as EventEmitter4 } from "events";
46
46
 
47
+ // src/sinks/JsonlFileSink.ts
48
+ import * as fs from "fs";
49
+ import { EventEmitter } from "events";
50
+ var JsonlFileSink = class extends EventEmitter {
51
+ id;
52
+ /** The file this sink writes to. Read it (e.g. in a `close` handler) to upload/move the file. */
53
+ path;
54
+ _stream;
55
+ _serializeSample;
56
+ _ended = false;
57
+ constructor(options) {
58
+ super();
59
+ this.id = options.id ?? options.path;
60
+ this.path = options.path;
61
+ this._stream = fs.createWriteStream(this.path, { flags: "a" });
62
+ this._stream.on("close", () => this.emit("close"));
63
+ this._stream.on("finish", () => this.emit("finish"));
64
+ this._stream.on("drain", () => this.emit("drain"));
65
+ this._stream.on("error", (err) => this.emit("error", err));
66
+ this._serializeSample = options.serializeSample ?? ((record) => JSON.stringify(record));
67
+ }
68
+ get ended() {
69
+ return this._ended;
70
+ }
71
+ write(record) {
72
+ if (this._ended) return false;
73
+ return this._stream.write(`${this._serializeSample(record)}
74
+ `);
75
+ }
76
+ end() {
77
+ if (this._ended) return;
78
+ this._ended = true;
79
+ this.emit("ended");
80
+ this._stream.end();
81
+ }
82
+ };
83
+ function createJsonlFileSink(options) {
84
+ return new JsonlFileSink(options);
85
+ }
86
+
47
87
  // src/sinks/SinkRegistry.ts
48
88
  import { v4 as uuidv4 } from "uuid";
49
89
  var logger = createLogger("SinkRegistry");
@@ -55,6 +95,8 @@ var SinkRegistry = class {
55
95
  // This registry's sinks and its descendants'.
56
96
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
97
  _sinks = /* @__PURE__ */ new Map();
98
+ // The ids of the sinks added to this registry itself (not to a descendant).
99
+ _ownIds = /* @__PURE__ */ new Set();
58
100
  /** Number of open sinks in this registry and its descendants. */
59
101
  get size() {
60
102
  return this._sinks.size;
@@ -83,17 +125,48 @@ var SinkRegistry = class {
83
125
  return void 0;
84
126
  }
85
127
  const sink = new SinkClass({ ...settings, id });
86
- const onError = (err) => logger.warn("Sink %s failed: %o", id, err);
87
- sink.on("error", onError);
88
- sink.once("close", () => sink.off("error", onError));
89
- sink.once("ended", () => {
90
- for (const registry of lineage) {
91
- if (registry._sinks.get(id) === sink) registry._sinks.delete(id);
92
- }
93
- });
94
- for (const registry of lineage) registry._sinks.set(id, sink);
128
+ this._register(sink, lineage);
129
+ return sink;
130
+ }
131
+ /**
132
+ * Register a sink constructed elsewhere, under its own `id`. Returns `false`, registering nothing,
133
+ * when the id is already in use in the observer (the sink is then the caller's to end). Prefer
134
+ * `create` when the sink opens a resource on construction: it checks the id first.
135
+ */
136
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
137
+ add(sink) {
138
+ const lineage = this._lineage();
139
+ if (lineage[lineage.length - 1]._sinks.has(sink.id)) {
140
+ logger.warn("Sink id %s is already in use; sink ids must be unique per observer. The sink was not added", sink.id);
141
+ return false;
142
+ }
143
+ this._register(sink, lineage);
144
+ return true;
145
+ }
146
+ /**
147
+ * Register the sink a `SinkDestination` describes, under `id`: for `jsonlPath`, a `JsonlFileSink`
148
+ * is created (the id is checked before the file is opened); a `sink` is added, and its `id` must
149
+ * be `id`. Returns `undefined` when the id is taken, the ids differ, or both are given (possible
150
+ * from JavaScript); a given `sink` that is not registered is ended.
151
+ */
152
+ createFromDestination(id, destination) {
153
+ const { jsonlPath, sink } = destination;
154
+ const refuse = (reason, ...args) => {
155
+ logger.warn(`Sink ${id}: ${reason}`, ...args);
156
+ sink?.end();
157
+ return void 0;
158
+ };
159
+ if (jsonlPath !== void 0 && sink) return refuse("give either jsonlPath or sink, not both");
160
+ if (jsonlPath !== void 0) return this.create(JsonlFileSink, { id, path: jsonlPath });
161
+ if (!sink) return void 0;
162
+ if (sink.id !== id) return refuse("the sink id %s must be %s; it is ended", sink.id, id);
163
+ if (!this.add(sink)) return refuse("the id is already in use; the sink is ended");
95
164
  return sink;
96
165
  }
166
+ /** Whether the sink `id` was added to this registry itself, rather than to a descendant. */
167
+ owns(id) {
168
+ return this._ownIds.has(id);
169
+ }
97
170
  /** Write `record` to the sink `id`. `false` when there is no such sink, or on backpressure. */
98
171
  write(id, record) {
99
172
  const sink = this._sinks.get(id);
@@ -114,6 +187,21 @@ var SinkRegistry = class {
114
187
  }
115
188
  }
116
189
  /** This registry and its ancestors, up to the root. */
190
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
191
+ _register(sink, lineage) {
192
+ const id = sink.id;
193
+ const onError = (err) => logger.warn("Sink %s failed: %o", id, err);
194
+ sink.on("error", onError);
195
+ sink.once("close", () => sink.off("error", onError));
196
+ sink.once("ended", () => {
197
+ if (this._sinks.get(id) === sink) this._ownIds.delete(id);
198
+ for (const registry of lineage) {
199
+ if (registry._sinks.get(id) === sink) registry._sinks.delete(id);
200
+ }
201
+ });
202
+ this._ownIds.add(id);
203
+ for (const registry of lineage) registry._sinks.set(id, sink);
204
+ }
117
205
  _lineage() {
118
206
  return [this, ...this.parent?._lineage() ?? []];
119
207
  }
@@ -144,19 +232,20 @@ var ExtensionStatsRegistry = class {
144
232
  }
145
233
  }
146
234
  create(settings) {
147
- if (this.closed) {
148
- logger2.warn("Attempted to create extension stats %s on closed %s", settings.id, this.id);
149
- return void 0;
150
- }
151
- if (this.values.has(settings.id)) {
152
- logger2.warn("Extension stats with id %s already exists on %s", settings.id, this.id);
235
+ const { id, jsonlPath, sink } = settings;
236
+ const refuse = (reason) => {
237
+ logger2.warn("Extension stats %s on %s: %s", id, this.id, reason);
238
+ sink?.end();
153
239
  return void 0;
154
- }
155
- this.values.set(settings.id, void 0);
156
- if (!settings.createSink) return void 0;
157
- const [SinkClass, sinkSettings] = settings.createSink;
158
- this._sinks.get(settings.id)?.end();
159
- return this._sinks.create(SinkClass, { ...sinkSettings, id: settings.id });
240
+ };
241
+ if (this.closed) return refuse("the owner is closed");
242
+ if (this.values.has(id)) return refuse("the id already exists");
243
+ if (jsonlPath !== void 0 && sink) return refuse("give either jsonlPath or sink, not both");
244
+ if (sink && sink.id !== id) return refuse(`the sink id ${sink.id} differs from the extension stats id`);
245
+ this.values.set(id, void 0);
246
+ if (jsonlPath === void 0 && !sink) return void 0;
247
+ if (this._sinks.owns(id)) this._sinks.get(id)?.end();
248
+ return this._sinks.createFromDestination(id, settings);
160
249
  }
161
250
  accept(id, data) {
162
251
  if (this.closed) {
@@ -175,46 +264,6 @@ var ExtensionStatsRegistry = class {
175
264
  }
176
265
  };
177
266
 
178
- // src/sinks/JsonlFileSink.ts
179
- import * as fs from "fs";
180
- import { EventEmitter } from "events";
181
- var JsonlFileSink = class extends EventEmitter {
182
- id;
183
- /** The file this sink writes to. Read it (e.g. in a `close` handler) to upload/move the file. */
184
- path;
185
- _stream;
186
- _serializeSample;
187
- _ended = false;
188
- constructor(options) {
189
- super();
190
- this.id = options.id ?? options.path;
191
- this.path = options.path;
192
- this._stream = fs.createWriteStream(this.path, { flags: "a" });
193
- this._stream.on("close", () => this.emit("close"));
194
- this._stream.on("finish", () => this.emit("finish"));
195
- this._stream.on("drain", () => this.emit("drain"));
196
- this._stream.on("error", (err) => this.emit("error", err));
197
- this._serializeSample = options.serializeSample ?? ((record) => JSON.stringify(record));
198
- }
199
- get ended() {
200
- return this._ended;
201
- }
202
- write(record) {
203
- if (this._ended) return false;
204
- return this._stream.write(`${this._serializeSample(record)}
205
- `);
206
- }
207
- end() {
208
- if (this._ended) return;
209
- this._ended = true;
210
- this.emit("ended");
211
- this._stream.end();
212
- }
213
- };
214
- function createJsonlFileSink(options) {
215
- return new JsonlFileSink(options);
216
- }
217
-
218
267
  // src/ObservedClient.ts
219
268
  import { EventEmitter as EventEmitter3 } from "events";
220
269
 
@@ -3075,7 +3124,7 @@ var ObservedClient = class extends EventEmitter3 {
3075
3124
  /** The distinct `publicAddress`es currently known by this client's open peer connections. */
3076
3125
  /**
3077
3126
  * The sink this client's samples are written to: whatever is registered under its client id in
3078
- * `call.sinks` (the `createClientJsonlSink` file, or a sink the application added), if any.
3127
+ * `call.sinks` (the one `createClientSink` returned, or a sink the application added), if any.
3079
3128
  */
3080
3129
  get sink() {
3081
3130
  return this.call.sinks.get(this.clientId);
@@ -5178,17 +5227,11 @@ var ObservedCall = class extends EventEmitter4 {
5178
5227
  clearTimeout(this.closeTimer);
5179
5228
  this.closeTimer = void 0;
5180
5229
  }
5181
- const jsonlPath = this.observer.config.createClientJsonlSink?.({ observedCall: this, clientId: settings.clientId });
5182
- let jsonlSink;
5183
- if (jsonlPath !== void 0 && this.observer.sinks.get(settings.clientId)) {
5184
- logger6.warn("No sample file for client %s in call %s: sink id %s is already in use", settings.clientId, this.callId, settings.clientId);
5185
- } else if (jsonlPath !== void 0) {
5186
- jsonlSink = this.sinks.create(JsonlFileSink, { id: settings.clientId, path: jsonlPath });
5187
- }
5230
+ const clientSink = this._createClientSink(settings.clientId);
5188
5231
  this.emit("newclient", result);
5189
5232
  this._notify("client-added", { ...this.eventScope, observedClient: result });
5190
- if (jsonlSink) {
5191
- this._notify("client-sink-created", { ...this.eventScope, observedClient: result, sink: jsonlSink });
5233
+ if (clientSink) {
5234
+ this._notify("client-sink-created", { ...this.eventScope, observedClient: result, sink: clientSink });
5192
5235
  }
5193
5236
  if (wasEmpty) {
5194
5237
  this.emit("not-empty");
@@ -5235,6 +5278,16 @@ var ObservedCall = class extends EventEmitter4 {
5235
5278
  this.endedAt = Math.max(this.endedAt ?? client.leftAt, client.leftAt);
5236
5279
  }
5237
5280
  /** Emit an Observer-bus event scoped to this call. */
5281
+ _createClientSink(clientId) {
5282
+ const createClientSink = this.observer.config.createClientSink;
5283
+ if (!createClientSink) return void 0;
5284
+ if (this.observer.sinks.get(clientId)) {
5285
+ logger6.warn("No sample sink for client %s in call %s: sink id %s is already in use", clientId, this.callId, clientId);
5286
+ return void 0;
5287
+ }
5288
+ const destination = createClientSink({ id: clientId, observedCall: this, clientId });
5289
+ return destination ? this.sinks.createFromDestination(clientId, destination) : void 0;
5290
+ }
5238
5291
  _notify(type, ...args) {
5239
5292
  this.observer.emit(type, ...args);
5240
5293
  }