@2702rebels/wpidata 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.
Files changed (80) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +5 -0
  3. package/dist/abstractions.cjs +0 -0
  4. package/dist/abstractions.d.cts +246 -0
  5. package/dist/abstractions.d.cts.map +1 -0
  6. package/dist/abstractions.d.mts +246 -0
  7. package/dist/abstractions.d.mts.map +1 -0
  8. package/dist/abstractions.mjs +1 -0
  9. package/dist/formats/json.cjs +32 -0
  10. package/dist/formats/json.d.cts +14 -0
  11. package/dist/formats/json.d.cts.map +1 -0
  12. package/dist/formats/json.d.mts +14 -0
  13. package/dist/formats/json.d.mts.map +1 -0
  14. package/dist/formats/json.mjs +33 -0
  15. package/dist/formats/json.mjs.map +1 -0
  16. package/dist/formats/msgpack.cjs +30 -0
  17. package/dist/formats/msgpack.d.cts +14 -0
  18. package/dist/formats/msgpack.d.cts.map +1 -0
  19. package/dist/formats/msgpack.d.mts +14 -0
  20. package/dist/formats/msgpack.d.mts.map +1 -0
  21. package/dist/formats/msgpack.mjs +31 -0
  22. package/dist/formats/msgpack.mjs.map +1 -0
  23. package/dist/formats/protobuf.cjs +130 -0
  24. package/dist/formats/protobuf.d.cts +68 -0
  25. package/dist/formats/protobuf.d.cts.map +1 -0
  26. package/dist/formats/protobuf.d.mts +68 -0
  27. package/dist/formats/protobuf.d.mts.map +1 -0
  28. package/dist/formats/protobuf.mjs +128 -0
  29. package/dist/formats/protobuf.mjs.map +1 -0
  30. package/dist/formats/struct.cjs +593 -0
  31. package/dist/formats/struct.d.cts +134 -0
  32. package/dist/formats/struct.d.cts.map +1 -0
  33. package/dist/formats/struct.d.mts +134 -0
  34. package/dist/formats/struct.d.mts.map +1 -0
  35. package/dist/formats/struct.mjs +591 -0
  36. package/dist/formats/struct.mjs.map +1 -0
  37. package/dist/sink.cjs +360 -0
  38. package/dist/sink.d.cts +93 -0
  39. package/dist/sink.d.cts.map +1 -0
  40. package/dist/sink.d.mts +93 -0
  41. package/dist/sink.d.mts.map +1 -0
  42. package/dist/sink.mjs +361 -0
  43. package/dist/sink.mjs.map +1 -0
  44. package/dist/types/protobuf.cjs +0 -0
  45. package/dist/types/protobuf.d.cts +302 -0
  46. package/dist/types/protobuf.d.cts.map +1 -0
  47. package/dist/types/protobuf.d.mts +302 -0
  48. package/dist/types/protobuf.d.mts.map +1 -0
  49. package/dist/types/protobuf.mjs +1 -0
  50. package/dist/types/sendable.cjs +0 -0
  51. package/dist/types/sendable.d.cts +225 -0
  52. package/dist/types/sendable.d.cts.map +1 -0
  53. package/dist/types/sendable.d.mts +225 -0
  54. package/dist/types/sendable.d.mts.map +1 -0
  55. package/dist/types/sendable.mjs +1 -0
  56. package/dist/types/struct.cjs +0 -0
  57. package/dist/types/struct.d.cts +304 -0
  58. package/dist/types/struct.d.cts.map +1 -0
  59. package/dist/types/struct.d.mts +304 -0
  60. package/dist/types/struct.d.mts.map +1 -0
  61. package/dist/types/struct.mjs +1 -0
  62. package/dist/utils.cjs +140 -0
  63. package/dist/utils.d.cts +40 -0
  64. package/dist/utils.d.cts.map +1 -0
  65. package/dist/utils.d.mts +40 -0
  66. package/dist/utils.d.mts.map +1 -0
  67. package/dist/utils.mjs +135 -0
  68. package/dist/utils.mjs.map +1 -0
  69. package/package.json +51 -0
  70. package/src/abstractions.ts +308 -0
  71. package/src/formats/json.ts +53 -0
  72. package/src/formats/msgpack.ts +42 -0
  73. package/src/formats/protobuf.ts +213 -0
  74. package/src/formats/struct.test.ts +814 -0
  75. package/src/formats/struct.ts +992 -0
  76. package/src/sink.ts +611 -0
  77. package/src/types/protobuf.ts +334 -0
  78. package/src/types/sendable.ts +244 -0
  79. package/src/types/struct.ts +333 -0
  80. package/src/utils.ts +241 -0
package/src/sink.ts ADDED
@@ -0,0 +1,611 @@
1
+ import { JsonDataTransformer } from "./formats/json";
2
+ import { MsgpackDataTransformer } from "./formats/msgpack";
3
+ import { ProtobufDataTransformer } from "./formats/protobuf";
4
+ import { StructDataTransformer } from "./formats/struct";
5
+ import {
6
+ addTimestampedRecord,
7
+ getTimestampedRecord,
8
+ pruneTimestampedRecords,
9
+ setValueByPath,
10
+ toUint8Array,
11
+ } from "./utils";
12
+
13
+ import type {
14
+ CompositeDataChannel,
15
+ DataChannel,
16
+ DataChannelPublisherOptions,
17
+ DataTransformer,
18
+ DataTypeImpl,
19
+ } from "./abstractions";
20
+
21
+ /** Converts raw data type name to {@link DataType}. */
22
+ function getDataType(v: string) {
23
+ switch (v.toLocaleLowerCase()) {
24
+ case "boolean":
25
+ return "boolean";
26
+ case "string":
27
+ return "string";
28
+ case "double":
29
+ case "int":
30
+ case "float":
31
+ return "number";
32
+ case "boolean[]":
33
+ return "booleanArray";
34
+ case "string[]":
35
+ return "stringArray";
36
+ case "double[]":
37
+ case "int[]":
38
+ case "float[]":
39
+ return "numberArray";
40
+ case "json":
41
+ case "msgpack":
42
+ return "json";
43
+ default:
44
+ // case "raw":
45
+ // case "rpc":
46
+ return "binary";
47
+ }
48
+ }
49
+
50
+ /** Gets the raw data type of the value to publish. */
51
+ function getRawType(v: unknown): string | undefined {
52
+ if (Array.isArray(v)) {
53
+ if (v.length > 0) {
54
+ const itemType = getRawType(v[0]);
55
+ return itemType ? `${itemType}[]` : undefined;
56
+ }
57
+ } else {
58
+ switch (typeof v) {
59
+ case "boolean":
60
+ return "boolean";
61
+ case "string":
62
+ return "string";
63
+ case "number":
64
+ return "double"; // we can't distinguish between int, float and double
65
+ case "object":
66
+ return "json";
67
+ }
68
+ }
69
+ }
70
+
71
+ /** Enqueues data into the composite channel at the specified path. */
72
+ function enqueueDataWithPath(channel: CompositeDataChannel, timestamp: number, data: unknown, path: Array<string>) {
73
+ channel.records ??= [];
74
+
75
+ // three scenarios are possible here:
76
+ // - merge into existing record value with the same timestamp
77
+ // - merge into cloned most recent record value
78
+ // - create new record value
79
+ //
80
+ // additionally it is also possible that we are inserting data at
81
+ // a `historical` timestamp, i.e. there are already newer records
82
+ // following the record being inserted; in this case we have to
83
+ // also update each of the following records by setting the incoming
84
+ // value, unless it is already present there
85
+ const [record, recordIndex] = getTimestampedRecord(channel.records, timestamp);
86
+ let index = recordIndex;
87
+
88
+ if (record != null) {
89
+ if (record.timestamp === timestamp) {
90
+ setValueByPath(record.value, path, data);
91
+ } else {
92
+ index = addTimestampedRecord(
93
+ channel.records,
94
+ timestamp,
95
+ setValueByPath(structuredClone(record.value), path, data)
96
+ );
97
+ }
98
+ } else {
99
+ index = addTimestampedRecord(channel.records, timestamp, setValueByPath({}, path, data));
100
+ }
101
+
102
+ for (let i = index + 1; i < channel.records.length; ++i) {
103
+ setValueByPath(channel.records[i]!.value, path, data, true);
104
+ }
105
+ }
106
+
107
+ /** Enqueues data into the channel. */
108
+ function enqueueData(channel: DataChannel, timestamp: number, data: unknown, dlq: boolean) {
109
+ if (dlq) {
110
+ channel.dlq ??= [];
111
+ addTimestampedRecord<Uint8Array>(channel.dlq, timestamp, toUint8Array(data));
112
+ } else {
113
+ if (channel.composite != null) {
114
+ const path = channel.composite.channels.get(channel.id);
115
+ if (path == null) {
116
+ throw new Error(`Invariant violation: ${channel.id} must be a sub-channel of ${channel.composite.id}`);
117
+ }
118
+
119
+ enqueueDataWithPath(channel.composite, timestamp, data, path);
120
+ } else {
121
+ channel.records ??= [];
122
+ addTimestampedRecord(channel.records, timestamp, data as DataTypeImpl);
123
+ }
124
+ }
125
+ }
126
+
127
+ /** Converts channel into a sub-channel of a composite channel. */
128
+ function convertToSubchannel(channel: DataChannel, composite: CompositeDataChannel) {
129
+ // compute path to store channel data within the composite channel record
130
+ // for example,
131
+ // channel.id = `nt:/SmartDashboard/PowerDistribution[0]/Chan0`
132
+ // composite.id = `nt:/SmartDashboard/PowerDistribution[0]`
133
+ // path = [`Chan0`]
134
+ // -or-
135
+ // channel.id = `nt:/SmartDashboard/Module 0/RootSpeed/x`
136
+ // composite.id = `nt:/SmartDashboard/Module 0`
137
+ // path = [`RootSpeed`, `x`]
138
+ const path = channel.id
139
+ .substring(composite.id.length + 1)
140
+ .split(/\//)
141
+ .map((_) => _.trim())
142
+ .filter((_) => _.length > 0);
143
+
144
+ channel.composite = composite;
145
+ composite.channels.set(channel.id, path);
146
+ }
147
+
148
+ /** Compares two arrays for value equality. */
149
+ function arraysEquals<T>(a: ReadonlyArray<T>, b: ReadonlyArray<T>) {
150
+ if (a === b) return true;
151
+ if (a == null || b == null) return false;
152
+ if (a.length !== b.length) return false;
153
+
154
+ for (let i = 0; i < a.length; ++i) {
155
+ if (a[i] !== b[i]) return false;
156
+ }
157
+
158
+ return true;
159
+ }
160
+
161
+ /** Default console logger for error messages. */
162
+ function logError(message: string, error: unknown) {
163
+ console.error(`💣 [DataSink] ${message}`, error);
164
+ }
165
+
166
+ export type NativeChannelPublisher = (topic: string, type: string, value: unknown) => void;
167
+
168
+ export type DataRetentionPolicy = {
169
+ /** Maximum number of records to retain */
170
+ maxSize?: number;
171
+
172
+ /** Time window in seconds to retain */
173
+ maxTimeSeconds?: number;
174
+ };
175
+
176
+ /** Default {@link DataTransformer} used by {@link DataSink} that can be reused across instances. */
177
+ const DefaultDataSinkTransformers = [new JsonDataTransformer(), new MsgpackDataTransformer()];
178
+
179
+ /** A data sink in the data pipeline with support for protocol transformers, data retention, etc. */
180
+ export class DataSink {
181
+ private readonly channels: Map<string, DataChannel>;
182
+ private readonly composites: Map<string, CompositeDataChannel>;
183
+ private readonly schemas: Map<string, readonly [transformer: DataTransformer, typeName: string]>;
184
+ private readonly transformers: Array<DataTransformer>;
185
+ private readonly retention?: DataRetentionPolicy;
186
+ private readonly disableCompositeChannels: boolean;
187
+ private readonly onDataChannelAdded?: (channel: DataChannel) => void;
188
+ private readonly onDataChannelRemoved?: (channel: DataChannel) => void;
189
+ private readonly logger: (message: string, error: unknown) => void;
190
+ private timestamp: number;
191
+
192
+ // these transformers should be instantiated per sink since they maintain internal schema repos
193
+ private readonly structTransformer = new StructDataTransformer();
194
+ private readonly protobufTransformer = new ProtobufDataTransformer();
195
+
196
+ constructor(options: {
197
+ /** Additional data transformers. Default transformers for structured and binary types are always used. */
198
+ transformers?: Array<DataTransformer>;
199
+ /** Data retention policy. Default is unlimited retention. */
200
+ retention?: DataRetentionPolicy;
201
+ /** Callback invoked when new data channel is registered. */
202
+ onDataChannelAdded?: (channel: DataChannel) => void;
203
+ /** Callback invoked when existing data channel is removed. */
204
+ onDataChannelRemoved?: (channel: DataChannel) => void;
205
+ /** Disables support for composite (legacy) channels. */
206
+ disableCompositeChannels?: boolean;
207
+ /** Logger callback. Default is logging to console. */
208
+ logger?: (message: string, error: unknown) => void;
209
+ }) {
210
+ this.channels = new Map();
211
+ this.composites = new Map();
212
+ this.schemas = new Map();
213
+ this.retention = options?.retention;
214
+ this.onDataChannelAdded = options?.onDataChannelAdded;
215
+ this.onDataChannelRemoved = options?.onDataChannelRemoved;
216
+ this.disableCompositeChannels = options?.disableCompositeChannels ?? false;
217
+ this.logger = options?.logger ?? logError;
218
+ this.timestamp = 0;
219
+
220
+ this.transformers = [...DefaultDataSinkTransformers, this.structTransformer, this.protobufTransformer];
221
+
222
+ if (options?.transformers) {
223
+ this.transformers.push(...options.transformers);
224
+ }
225
+ }
226
+
227
+ /** Constructs channel identifier. */
228
+ private static createId(source: string, name: string) {
229
+ return `${source}:${name}`;
230
+ }
231
+
232
+ /** Returns parent composite data channel if one exists. */
233
+ private getCompositeParent(id: string) {
234
+ for (const composite of this.composites.values()) {
235
+ if (id.startsWith(`${composite.id}/`)) {
236
+ return composite;
237
+ }
238
+ }
239
+
240
+ return undefined;
241
+ }
242
+
243
+ /** Registers new data channel. */
244
+ private registerChannel(channel: DataChannel, publish?: NativeChannelPublisher) {
245
+ const id = DataSink.createId(channel.source, channel.id);
246
+ this.channels.set(id, channel);
247
+
248
+ // register publisher
249
+ channel.publish = publish ? this.createChannelPublisher(channel, publish) : undefined;
250
+
251
+ let silent = false;
252
+
253
+ // test if this channel should be rolled into an existing composite
254
+ if (!this.disableCompositeChannels) {
255
+ const composite = this.getCompositeParent(channel.id);
256
+ if (composite != null) {
257
+ convertToSubchannel(channel, composite);
258
+ silent = true;
259
+ }
260
+ }
261
+
262
+ // notify
263
+ if (!silent && this.onDataChannelAdded != null) {
264
+ this.onDataChannelAdded(channel);
265
+ }
266
+ }
267
+
268
+ /** Gets sub-channel by its path. */
269
+ private getSubChannel(channel: CompositeDataChannel, path: ReadonlyArray<string>) {
270
+ for (const [id, p] of channel.channels) {
271
+ if (arraysEquals(path, p)) {
272
+ return this.channels.get(id);
273
+ }
274
+ }
275
+
276
+ return undefined;
277
+ }
278
+
279
+ /** Creates channel publisher. */
280
+ private createChannelPublisher(channel: DataChannel, publish: NativeChannelPublisher) {
281
+ return (value: unknown, path?: ReadonlyArray<string>, options?: DataChannelPublisherOptions) => {
282
+ if (path != null && channel.dataType === "composite") {
283
+ const subchannel = this.getSubChannel(channel, path);
284
+
285
+ // sub-channel may not exist, e.g. it has not been published to yet,
286
+ // in this case publish to the topic constructed by appending the path
287
+ // to the composite channel id and derive the data type from the value
288
+ if (subchannel) {
289
+ // apply transformer
290
+ if (subchannel.transformer != null) {
291
+ value = subchannel.transformer.serialize(value, channel.structuredType);
292
+ }
293
+
294
+ publish(subchannel.id, subchannel.publishedDataType, value);
295
+ } else {
296
+ const topic = `${channel.id}/${path.join("/")}`;
297
+ let dataType = getRawType(value);
298
+
299
+ if (options?.structuredType) {
300
+ switch (options.structuredType.format) {
301
+ case "composite":
302
+ // not supported: must be published via subchannel
303
+ return;
304
+
305
+ case "struct":
306
+ value = this.structTransformer.serialize(value, options.structuredType);
307
+ dataType = `struct:${options.structuredType.name}`;
308
+ break;
309
+
310
+ case "protobuf":
311
+ value = this.protobufTransformer.serialize(value, options.structuredType);
312
+ dataType = `proto:${options.structuredType.name}`;
313
+ break;
314
+ }
315
+ }
316
+
317
+ // do not publish if we cannot figure out the data type
318
+ if (dataType) {
319
+ publish(topic, dataType, value);
320
+ }
321
+ }
322
+ } else {
323
+ // apply transformer
324
+ if (channel.transformer != null) {
325
+ value = channel.transformer.serialize(value, channel.structuredType);
326
+ }
327
+
328
+ publish(channel.id, channel.publishedDataType, value);
329
+ }
330
+ };
331
+ }
332
+
333
+ /** Returns most recent timestamp in microseconds observed by this instance. */
334
+ public get recentTimestamp() {
335
+ return this.timestamp;
336
+ }
337
+
338
+ /**
339
+ * Gets a channel descriptor.
340
+ *
341
+ * @param source source, e.g. `nt` or `wpilog`
342
+ * @param name channel name
343
+ */
344
+ public get(source: string, name: string) {
345
+ const channel = this.channels.get(DataSink.createId(source, name));
346
+ return channel && channel.composite == null ? channel : undefined;
347
+ }
348
+
349
+ /**
350
+ * Adds a channel descriptor.
351
+ *
352
+ * @param source source, e.g. `nt` or `wpilog`
353
+ * @param name channel name
354
+ * @param type channel type
355
+ * @param properties channel properties
356
+ * @param publish channel publisher
357
+ */
358
+ public add(
359
+ source: string,
360
+ name: string,
361
+ type: string,
362
+ properties?: Record<string, unknown> | string,
363
+ publish?: NativeChannelPublisher
364
+ ) {
365
+ // construct metadata
366
+ let metadata: Record<string, unknown> | string | undefined;
367
+ if (properties != null) {
368
+ if (typeof properties === "string") {
369
+ try {
370
+ metadata = JSON.parse(properties);
371
+ } catch {
372
+ metadata = properties;
373
+ }
374
+ } else if (typeof properties === "object") {
375
+ // remove standard properties when constructing metadata
376
+ const { persistent, retained, cached, ...other } = properties;
377
+ if (Object.keys(other).length > 0) {
378
+ metadata = other;
379
+ }
380
+ }
381
+ }
382
+
383
+ const dataType = getDataType(type);
384
+
385
+ // special handling of composite channels that are identified by the presence of `.type` topic;
386
+ // allows handling of legacy sendable data structures that are dispersed across individual topics
387
+ // as a single unified json-typed channel
388
+ if (!this.disableCompositeChannels && name.endsWith("/.type")) {
389
+ // ignore deeply nested `.type` entries, instead treat them as
390
+ // regular sub-channels that are rolled into the parent composite
391
+ const composite = this.getCompositeParent(name);
392
+ if (composite) {
393
+ const channel: DataChannel = {
394
+ source,
395
+ id: name,
396
+ dataType,
397
+ publishedDataType: type,
398
+ metadata,
399
+ };
400
+
401
+ // register publisher
402
+ channel.publish = publish ? this.createChannelPublisher(channel, publish) : undefined;
403
+
404
+ const id = DataSink.createId(channel.source, channel.id);
405
+ this.channels.set(id, channel);
406
+
407
+ convertToSubchannel(channel, composite);
408
+ } else {
409
+ const channel: CompositeDataChannel = {
410
+ source,
411
+ id: name.slice(0, -6),
412
+ dataType: "composite",
413
+ publishedDataType: "", // not applicable
414
+ metadata,
415
+ channels: new Map(),
416
+ };
417
+
418
+ // register publisher
419
+ channel.publish = publish ? this.createChannelPublisher(channel, publish) : undefined;
420
+
421
+ // convert any existing channels under this subtree to sub-channels
422
+ const prefix = `${channel.id}/`;
423
+ this.channels.forEach((value) => {
424
+ if (value.id.startsWith(prefix)) {
425
+ convertToSubchannel(value, channel);
426
+
427
+ if (this.onDataChannelRemoved != null) {
428
+ this.onDataChannelRemoved(value);
429
+ }
430
+ }
431
+ });
432
+
433
+ const id = DataSink.createId(channel.source, channel.id);
434
+ this.composites.set(id, channel);
435
+ this.channels.set(id, channel);
436
+
437
+ // notify
438
+ if (this.onDataChannelAdded != null) {
439
+ this.onDataChannelAdded(channel);
440
+ }
441
+ }
442
+ } else {
443
+ for (const transformer of this.transformers) {
444
+ try {
445
+ const result = transformer.inspect(source, name, type, metadata);
446
+ if (result != null) {
447
+ if (typeof result === "string") {
448
+ // schema channel
449
+ this.schemas.set(DataSink.createId(source, name), [transformer, result]);
450
+ } else {
451
+ // data channel
452
+ this.registerChannel(result, publish);
453
+ }
454
+ return;
455
+ }
456
+ } catch (exception) {
457
+ this.logger(`Transformer '${typeof transformer}' inspection failed`, exception);
458
+ }
459
+ }
460
+
461
+ this.registerChannel(
462
+ {
463
+ source,
464
+ id: name,
465
+ dataType,
466
+ publishedDataType: type,
467
+ metadata,
468
+ },
469
+ publish
470
+ );
471
+ }
472
+ }
473
+
474
+ /**
475
+ * Enqueues a timestamped value for a named data channel.
476
+ *
477
+ * @param source source, e.g. `nt` or `wpilog`
478
+ * @param name channel name
479
+ * @param timestamp timestamp in microseconds
480
+ * @param value raw value
481
+ */
482
+ public enqueue(source: string, name: string, timestamp: number, value: unknown): boolean {
483
+ // update the most recent timestamp
484
+ this.timestamp = Math.max(this.timestamp, timestamp);
485
+
486
+ const id = DataSink.createId(source, name);
487
+ const schema = this.schemas.get(id);
488
+ if (schema) {
489
+ // received data on the schema channel with a registered transformer
490
+ const [transformer, typeName] = schema;
491
+ try {
492
+ transformer.schema(typeName, value);
493
+ } catch (exception) {
494
+ // ignore garbage
495
+ this.logger(`Transformer '${typeof transformer}' failed to ingest schema data`, exception);
496
+ return false;
497
+ }
498
+
499
+ // process records in DLQ
500
+ this.channels.forEach((channel) => {
501
+ if (
502
+ channel.transformer === transformer &&
503
+ channel.dlq != null &&
504
+ channel.dlq.length > 0 &&
505
+ channel.structuredType != null &&
506
+ channel.transformer.canTransform(channel.structuredType.name)
507
+ ) {
508
+ channel.records ??= [];
509
+ for (const record of channel.dlq) {
510
+ try {
511
+ const v = transformer.deserialize(record.value, channel.structuredType);
512
+ if (v != null) {
513
+ enqueueData(channel, record.timestamp, v, false);
514
+ }
515
+ } catch (exception) {
516
+ // ignore garbage
517
+ this.logger(`Transformer '${typeof transformer}' failed to transform data in channel '${id}'`, exception);
518
+ }
519
+ }
520
+ channel.dlq = undefined;
521
+ }
522
+ });
523
+
524
+ return true;
525
+ }
526
+
527
+ if (!this.disableCompositeChannels && name.endsWith("/.type")) {
528
+ const channel = this.composites.get(id.slice(0, -6));
529
+ if (channel != null) {
530
+ if (typeof value === "string") {
531
+ channel.structuredType = {
532
+ name: value,
533
+ format: "composite",
534
+ };
535
+
536
+ enqueueDataWithPath(channel, timestamp, value, [".type"]);
537
+ }
538
+
539
+ return true;
540
+ }
541
+
542
+ // fallthrough for deeply nested `.type` channels
543
+ }
544
+
545
+ const channel = this.channels.get(id);
546
+ if (channel == null) {
547
+ return false;
548
+ }
549
+
550
+ let v = value;
551
+
552
+ // apply transformer
553
+ try {
554
+ if (channel.transformer != null) {
555
+ v = channel.transformer.deserialize(v, channel.structuredType);
556
+ if (v == null) {
557
+ enqueueData(channel, timestamp, value, true); // add to DLQ
558
+ return true;
559
+ }
560
+ }
561
+ } catch (exception) {
562
+ // ignore garbage
563
+ this.logger(`Transformer '${typeof channel.transformer}' failed to transform data in channel '${id}'`, exception);
564
+ return false;
565
+ }
566
+
567
+ enqueueData(channel, timestamp, v, false);
568
+ return true;
569
+ }
570
+
571
+ /**
572
+ * Prunes old records based on the retention policy if configured.
573
+ *
574
+ * The `currentTimestamp` represents time in the robot clock, not
575
+ * wall clock, typically you want to supply the most recent timestamp
576
+ * reported by live connection protocol, such as NetworkTables.
577
+ * Defaults to {@link recentTimestamp} field.
578
+ *
579
+ * @param currentTimestamp timestamp in microseconds representing current time
580
+ */
581
+ public enforceRetention(currentTimestamp?: number) {
582
+ if (this.retention == null) {
583
+ return;
584
+ }
585
+
586
+ const timestamp = currentTimestamp ?? this.timestamp;
587
+ const maxSize = this.retention.maxSize;
588
+ const cutoff =
589
+ this.retention.maxTimeSeconds != null ? Math.max(0, timestamp - this.retention.maxTimeSeconds * 1e6) : undefined;
590
+
591
+ if ((maxSize != null && maxSize > 0) || cutoff != null) {
592
+ this.channels.forEach((channel) => {
593
+ if (channel.records != null) {
594
+ pruneTimestampedRecords(channel.records, maxSize, cutoff);
595
+ }
596
+
597
+ if (channel.dlq != null) {
598
+ pruneTimestampedRecords(channel.dlq, maxSize, cutoff);
599
+ }
600
+ });
601
+ }
602
+ }
603
+
604
+ /** Purges this sink and all its records. */
605
+ public purge() {
606
+ this.channels.clear();
607
+ this.composites.clear();
608
+ this.schemas.clear();
609
+ this.timestamp = 0;
610
+ }
611
+ }