@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/README.md +38 -19
- package/dist/index.d.mts +91 -74
- package/dist/index.d.ts +91 -74
- package/dist/index.js +124 -71
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +124 -71
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
87
|
-
sink
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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.
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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 `
|
|
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
|
|
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 (
|
|
5191
|
-
this._notify("client-sink-created", { ...this.eventScope, observedClient: result, sink:
|
|
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
|
}
|