@synnaxlabs/x 0.44.2 → 0.44.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.
Files changed (63) hide show
  1. package/.turbo/turbo-build.log +15 -15
  2. package/dist/bounds-8OC_obRs.js +186 -0
  3. package/dist/bounds-CRK04jp7.cjs +1 -0
  4. package/dist/bounds.cjs +1 -1
  5. package/dist/bounds.js +1 -1
  6. package/dist/deep.cjs +1 -1
  7. package/dist/deep.js +1 -1
  8. package/dist/{external-Birv9jaY.js → external-BM_NS5yM.js} +6 -6
  9. package/dist/external-E3ErJeeM.cjs +1 -0
  10. package/dist/index.cjs +3 -3
  11. package/dist/index.js +235 -229
  12. package/dist/{path-DVFrKaNI.js → path-Blh4wJuA.js} +24 -21
  13. package/dist/path-CPSfCjde.cjs +1 -0
  14. package/dist/{scale-C6qKDbRb.cjs → scale-C3fEtXxW.cjs} +1 -1
  15. package/dist/{scale-EWNUk-bn.js → scale-Db1Gunj0.js} +1 -1
  16. package/dist/scale.cjs +1 -1
  17. package/dist/scale.js +1 -1
  18. package/dist/series-BcF7A8Je.cjs +6 -0
  19. package/dist/{series-EA1uaEDj.js → series-Cl3Vh_u-.js} +588 -471
  20. package/dist/spatial.cjs +1 -1
  21. package/dist/spatial.js +2 -2
  22. package/dist/src/breaker/breaker.d.ts +2 -1
  23. package/dist/src/breaker/breaker.d.ts.map +1 -1
  24. package/dist/src/deep/path.d.ts.map +1 -1
  25. package/dist/src/id/id.d.ts +3 -1
  26. package/dist/src/id/id.d.ts.map +1 -1
  27. package/dist/src/math/math.d.ts +2 -1
  28. package/dist/src/math/math.d.ts.map +1 -1
  29. package/dist/src/status/status.d.ts +1 -0
  30. package/dist/src/status/status.d.ts.map +1 -1
  31. package/dist/src/strings/strings.d.ts +1 -1
  32. package/dist/src/strings/strings.d.ts.map +1 -1
  33. package/dist/src/telem/series.d.ts +7 -0
  34. package/dist/src/telem/series.d.ts.map +1 -1
  35. package/dist/src/telem/telem.d.ts +78 -1
  36. package/dist/src/telem/telem.d.ts.map +1 -1
  37. package/dist/src/zod/util.d.ts.map +1 -1
  38. package/dist/telem.cjs +1 -1
  39. package/dist/telem.js +1 -1
  40. package/dist/zod.cjs +1 -1
  41. package/dist/zod.js +1 -1
  42. package/package.json +3 -3
  43. package/src/breaker/breaker.ts +4 -0
  44. package/src/deep/path.spec.ts +14 -0
  45. package/src/deep/path.ts +9 -2
  46. package/src/id/id.ts +8 -4
  47. package/src/math/math.spec.ts +20 -0
  48. package/src/math/math.ts +32 -29
  49. package/src/spatial/bounds/bounds.ts +1 -1
  50. package/src/status/status.ts +11 -0
  51. package/src/strings/strings.spec.ts +3 -0
  52. package/src/strings/strings.ts +2 -1
  53. package/src/telem/series.spec.ts +543 -2
  54. package/src/telem/series.ts +28 -9
  55. package/src/telem/telem.spec.ts +556 -0
  56. package/src/telem/telem.ts +118 -5
  57. package/src/zod/util.ts +5 -3
  58. package/tsconfig.tsbuildinfo +1 -1
  59. package/dist/bounds-D6e9xoHt.cjs +0 -1
  60. package/dist/bounds-Dj9nG39I.js +0 -174
  61. package/dist/external-DsmsSN1Y.cjs +0 -1
  62. package/dist/path-BeMr8xWN.cjs +0 -1
  63. package/dist/series-CcA_WjbJ.cjs +0 -6
@@ -778,6 +778,154 @@ describe("Series", () => {
778
778
  }).toThrow();
779
779
  });
780
780
  });
781
+
782
+ describe("property preservation during conversion", () => {
783
+ it("should preserve all properties when converting a series with .as()", () => {
784
+ // Create a series with all possible properties set
785
+ const timeRange = new TimeRange(TimeStamp.seconds(100), TimeStamp.seconds(200));
786
+ const original = new Series({
787
+ data: new Float32Array([1, 2, 3, 4, 5]),
788
+ dataType: DataType.FLOAT32,
789
+ timeRange,
790
+ sampleOffset: 10,
791
+ alignment: 100n,
792
+ alignmentMultiple: 5n,
793
+ key: "test-series-key",
794
+ });
795
+
796
+ // Convert to number type using .as()
797
+ const converted = original.as("number");
798
+
799
+ // Verify all properties are preserved
800
+ expect(converted.dataType).toEqual(original.dataType);
801
+ expect(converted.timeRange).toBe(original.timeRange);
802
+ expect(converted.sampleOffset).toBe(10);
803
+ expect(converted.alignment).toBe(100n);
804
+ expect(converted.alignmentMultiple).toBe(5n);
805
+ expect(converted.key).toBe("test-series-key");
806
+ expect(converted.length).toBe(5);
807
+ expect(converted.alignmentBounds).toEqual({ lower: 100n, upper: 125n });
808
+
809
+ // Verify data is still accessible and correct with offset applied
810
+ expect(converted.at(0)).toBe(11); // 1 + sampleOffset(10)
811
+ expect(converted.at(1)).toBe(12); // 2 + sampleOffset(10)
812
+ expect(converted.at(4)).toBe(15); // 5 + sampleOffset(10)
813
+
814
+ // Verify alignment access still works
815
+ expect(converted.atAlignment(100n)).toBe(11);
816
+ expect(converted.atAlignment(105n)).toBe(12);
817
+ expect(converted.atAlignment(120n)).toBe(15);
818
+
819
+ // Verify buffer is the same (no copy)
820
+ expect(converted.buffer).toBe(original.buffer);
821
+ });
822
+
823
+ it("should preserve properties when converting between different JS types", () => {
824
+ const timeRange = new TimeRange(TimeStamp.seconds(50), TimeStamp.seconds(150));
825
+
826
+ // Test with bigint series
827
+ const bigintSeries = new Series({
828
+ data: [100n, 200n, 300n],
829
+ dataType: DataType.INT64,
830
+ timeRange,
831
+ sampleOffset: 1000n,
832
+ alignment: 50n,
833
+ alignmentMultiple: 10n,
834
+ key: "bigint-series",
835
+ });
836
+
837
+ const bigintConverted = bigintSeries.as("bigint");
838
+ expect(bigintConverted.timeRange).toBe(timeRange);
839
+ expect(bigintConverted.sampleOffset).toBe(1000n);
840
+ expect(bigintConverted.alignment).toBe(50n);
841
+ expect(bigintConverted.alignmentMultiple).toBe(10n);
842
+ expect(bigintConverted.key).toBe("bigint-series");
843
+ expect(bigintConverted.at(0)).toBe(1100n); // 100n + 1000n
844
+ expect(bigintConverted.alignmentBounds).toEqual({ lower: 50n, upper: 80n });
845
+
846
+ // Test with string series
847
+ const stringSeries = new Series({
848
+ data: ["apple", "banana", "cherry"],
849
+ dataType: DataType.STRING,
850
+ timeRange,
851
+ alignment: 200n,
852
+ alignmentMultiple: 3n,
853
+ key: "string-series",
854
+ });
855
+
856
+ const stringConverted = stringSeries.as("string");
857
+ expect(stringConverted.timeRange).toBe(timeRange);
858
+ expect(stringConverted.alignment).toBe(200n);
859
+ expect(stringConverted.alignmentMultiple).toBe(3n);
860
+ expect(stringConverted.key).toBe("string-series");
861
+ expect(stringConverted.at(0)).toBe("apple");
862
+ expect(stringConverted.alignmentBounds).toEqual({ lower: 200n, upper: 209n });
863
+ });
864
+
865
+ it("should preserve properties when converting UUID series to string", () => {
866
+ const timeRange = new TimeRange(TimeStamp.seconds(10), TimeStamp.seconds(20));
867
+ const uuidSeries = new Series({
868
+ data: SAMPLE_UUID_BYTES,
869
+ dataType: DataType.UUID,
870
+ timeRange,
871
+ alignment: 1000n,
872
+ alignmentMultiple: 100n,
873
+ key: "uuid-series-key",
874
+ });
875
+
876
+ const stringConverted = uuidSeries.as("string");
877
+
878
+ // All non-data properties should be preserved
879
+ expect(stringConverted.dataType).toEqual(DataType.UUID);
880
+ expect(stringConverted.timeRange).toBe(timeRange);
881
+ expect(stringConverted.alignment).toBe(1000n);
882
+ expect(stringConverted.alignmentMultiple).toBe(100n);
883
+ expect(stringConverted.key).toBe("uuid-series-key");
884
+ expect(stringConverted.length).toBe(2);
885
+ expect(stringConverted.alignmentBounds).toEqual({ lower: 1000n, upper: 1200n });
886
+
887
+ // Verify data access still works
888
+ expect(stringConverted.at(0)).toBe("123e4567-e89b-40d3-8056-426614174000");
889
+ expect(stringConverted.atAlignment(1000n)).toBe(
890
+ "123e4567-e89b-40d3-8056-426614174000",
891
+ );
892
+ expect(stringConverted.atAlignment(1100n)).toBe(
893
+ "7f3e4567-e89b-40d3-8056-426614174000",
894
+ );
895
+ });
896
+
897
+ it("should preserve properties when creating a new series from an existing one", () => {
898
+ const timeRange = new TimeRange(TimeStamp.seconds(1), TimeStamp.seconds(10));
899
+ const original = new Series({
900
+ data: [1.5, 2.5, 3.5],
901
+ dataType: DataType.FLOAT64,
902
+ timeRange,
903
+ sampleOffset: 0.5,
904
+ alignment: 15n,
905
+ alignmentMultiple: 2n,
906
+ key: "original-key",
907
+ });
908
+
909
+ // Create new series from existing one
910
+ const copy = new Series(original);
911
+
912
+ // All properties should be preserved
913
+ expect(copy.dataType).toEqual(original.dataType);
914
+ expect(copy.timeRange).toBe(original.timeRange);
915
+ expect(copy.sampleOffset).toBe(0.5);
916
+ expect(copy.alignment).toBe(15n);
917
+ expect(copy.alignmentMultiple).toBe(2n);
918
+ expect(copy.key).toBe("original-key"); // Key is preserved when creating from existing series
919
+ expect(copy.length).toBe(3);
920
+ expect(copy.alignmentBounds).toEqual({ lower: 15n, upper: 21n });
921
+ expect(copy.buffer).toBe(original.buffer); // Should share buffer
922
+
923
+ // Data access with offset
924
+ expect(copy.at(0)).toBe(2); // 1.5 + 0.5
925
+ expect(copy.at(1)).toBe(3); // 2.5 + 0.5
926
+ expect(copy.at(2)).toBe(4); // 3.5 + 0.5
927
+ });
928
+ });
781
929
  });
782
930
 
783
931
  describe("alignmentBounds", () => {
@@ -791,6 +939,204 @@ describe("Series", () => {
791
939
  });
792
940
  });
793
941
 
942
+ describe("alignmentMultiple", () => {
943
+ it("should default to 1n when not specified", () => {
944
+ const series = new Series({
945
+ data: new Float32Array([1, 2, 3]),
946
+ alignment: 10n,
947
+ });
948
+ expect(series.alignmentMultiple).toBe(1n);
949
+ });
950
+
951
+ it("should be set correctly when specified in constructor", () => {
952
+ const series = new Series({
953
+ data: new Float32Array([1, 2, 3]),
954
+ alignment: 10n,
955
+ alignmentMultiple: 5n,
956
+ });
957
+ expect(series.alignmentMultiple).toBe(5n);
958
+ });
959
+
960
+ it("should correctly calculate alignment bounds with alignmentMultiple", () => {
961
+ const series = new Series({
962
+ data: new Float32Array([1, 2, 3]),
963
+ alignment: 10n,
964
+ alignmentMultiple: 5n,
965
+ });
966
+ // lower: 10n (alignment)
967
+ // upper: 10n + 3n * 5n = 25n
968
+ expect(series.alignmentBounds).toEqual({ lower: 10n, upper: 25n });
969
+ });
970
+
971
+ it("should correctly calculate alignment bounds with alignmentMultiple = 1", () => {
972
+ const series = new Series({
973
+ data: new Float32Array([1, 2, 3]),
974
+ alignment: 10n,
975
+ alignmentMultiple: 1n,
976
+ });
977
+ // lower: 10n (alignment)
978
+ // upper: 10n + 3n * 1n = 13n
979
+ expect(series.alignmentBounds).toEqual({ lower: 10n, upper: 13n });
980
+ });
981
+
982
+ it("should work with atAlignment when alignmentMultiple > 1", () => {
983
+ const series = new Series({
984
+ data: new Float32Array([10, 20, 30, 40, 50]),
985
+ alignment: 100n,
986
+ alignmentMultiple: 10n,
987
+ });
988
+
989
+ expect(series.atAlignment(100n)).toBe(10);
990
+ expect(series.atAlignment(110n)).toBe(20);
991
+ expect(series.atAlignment(120n)).toBe(30);
992
+ expect(series.atAlignment(130n)).toBe(40);
993
+ expect(series.atAlignment(140n)).toBe(50);
994
+ });
995
+
996
+ it("should return undefined for alignments not on the multiple grid", () => {
997
+ const series = new Series({
998
+ data: new Float32Array([10, 20, 30]),
999
+ alignment: 100n,
1000
+ alignmentMultiple: 10n,
1001
+ });
1002
+
1003
+ // These alignments are not on the 10n grid starting from 100n
1004
+ expect(series.atAlignment(105n)).toBe(10);
1005
+ expect(series.atAlignment(115n)).toBe(20);
1006
+ expect(series.atAlignment(125n)).toBe(30);
1007
+ expect(series.atAlignment(135n)).toBeUndefined();
1008
+ });
1009
+
1010
+ it("should handle alignmentMultiple with decimation scenario", () => {
1011
+ const series = new Series({
1012
+ data: new Float32Array([1, 5, 9, 13, 17]),
1013
+ alignment: 0n,
1014
+ alignmentMultiple: 4n,
1015
+ });
1016
+
1017
+ expect(series.alignmentBounds).toEqual({ lower: 0n, upper: 20n });
1018
+ expect(series.atAlignment(0n)).toBe(1);
1019
+ expect(series.atAlignment(4n)).toBe(5);
1020
+ expect(series.atAlignment(8n)).toBe(9);
1021
+ expect(series.atAlignment(12n)).toBe(13);
1022
+ expect(series.atAlignment(16n)).toBe(17);
1023
+
1024
+ expect(series.atAlignment(1n)).toBe(1);
1025
+ expect(series.atAlignment(2n)).toBe(1);
1026
+ expect(series.atAlignment(3n)).toBe(1);
1027
+ });
1028
+
1029
+ it("should handle alignmentMultiple with averaging scenario", () => {
1030
+ const series = new Series({
1031
+ data: new Float32Array([10, 20, 30]),
1032
+ alignment: 1000n,
1033
+ alignmentMultiple: 10n,
1034
+ });
1035
+
1036
+ expect(series.alignmentBounds).toEqual({ lower: 1000n, upper: 1030n });
1037
+
1038
+ expect(series.atAlignment(1000n)).toBe(10);
1039
+ expect(series.atAlignment(1010n)).toBe(20);
1040
+ expect(series.atAlignment(1020n)).toBe(30);
1041
+ });
1042
+
1043
+ it("should preserve alignmentMultiple when creating from another series", () => {
1044
+ const original = new Series({
1045
+ data: new Float32Array([1, 2, 3]),
1046
+ alignment: 100n,
1047
+ alignmentMultiple: 5n,
1048
+ });
1049
+
1050
+ const copy = new Series(original);
1051
+ expect(copy.alignmentMultiple).toBe(5n);
1052
+ expect(copy.alignment).toBe(100n);
1053
+ expect(copy.alignmentBounds).toEqual(original.alignmentBounds);
1054
+ });
1055
+
1056
+ it("should handle negative alignment indices correctly with alignmentMultiple", () => {
1057
+ const series = new Series({
1058
+ data: new Float32Array([10, 20, 30]),
1059
+ alignment: 50n,
1060
+ alignmentMultiple: 3n,
1061
+ });
1062
+
1063
+ expect(series.atAlignment(45n)).toBeUndefined();
1064
+ expect(series.atAlignment(47n)).toBeUndefined();
1065
+ });
1066
+
1067
+ it("should handle alignment beyond series end with alignmentMultiple", () => {
1068
+ const series = new Series({
1069
+ data: new Float32Array([10, 20]),
1070
+ alignment: 0n,
1071
+ alignmentMultiple: 5n,
1072
+ });
1073
+
1074
+ expect(series.atAlignment(0n)).toBe(10);
1075
+ expect(series.atAlignment(5n)).toBe(20);
1076
+ expect(series.atAlignment(10n)).toBeUndefined();
1077
+ });
1078
+
1079
+ it("should throw error when required=true and alignment not found with alignmentMultiple", () => {
1080
+ const series = new Series({
1081
+ data: new Float32Array([10, 20]),
1082
+ alignment: 100n,
1083
+ alignmentMultiple: 10n,
1084
+ });
1085
+
1086
+ expect(() => series.atAlignment(150n, true)).toThrow();
1087
+ expect(() => series.atAlignment(90n, true)).toThrow();
1088
+ });
1089
+
1090
+ it("should work with very large alignmentMultiple values", () => {
1091
+ const series = new Series({
1092
+ data: new Float32Array([1, 2]),
1093
+ alignment: 1000000000n,
1094
+ alignmentMultiple: 1000000n,
1095
+ });
1096
+
1097
+ expect(series.atAlignment(1000000000n)).toBe(1);
1098
+ expect(series.atAlignment(1001000000n)).toBe(2);
1099
+ expect(series.alignmentBounds).toEqual({
1100
+ lower: 1000000000n,
1101
+ upper: 1002000000n,
1102
+ });
1103
+ });
1104
+
1105
+ it("should handle alignmentMultiple with different data types", () => {
1106
+ const stringSeries = new Series({
1107
+ data: ["a", "b", "c"],
1108
+ alignment: 10n,
1109
+ alignmentMultiple: 2n,
1110
+ });
1111
+
1112
+ expect(stringSeries.atAlignment(10n)).toBe("a");
1113
+ expect(stringSeries.atAlignment(12n)).toBe("b");
1114
+ expect(stringSeries.atAlignment(14n)).toBe("c");
1115
+
1116
+ const jsonSeries = new Series({
1117
+ data: [{ x: 1 }, { x: 2 }],
1118
+ alignment: 5n,
1119
+ alignmentMultiple: 3n,
1120
+ });
1121
+
1122
+ expect(jsonSeries.atAlignment(5n)).toEqual({ x: 1 });
1123
+ expect(jsonSeries.atAlignment(8n)).toEqual({ x: 2 });
1124
+ });
1125
+
1126
+ it("should correctly calculate index from alignment with alignmentMultiple", () => {
1127
+ const series = new Series({
1128
+ data: new Float32Array([100, 200, 300, 400]),
1129
+ alignment: 50n,
1130
+ alignmentMultiple: 25n,
1131
+ });
1132
+
1133
+ expect(series.atAlignment(50n)).toBe(100);
1134
+ expect(series.atAlignment(75n)).toBe(200);
1135
+ expect(series.atAlignment(100n)).toBe(300);
1136
+ expect(series.atAlignment(125n)).toBe(400);
1137
+ });
1138
+ });
1139
+
794
1140
  describe("toStrings", () => {
795
1141
  interface Spec {
796
1142
  name: string;
@@ -975,9 +1321,20 @@ describe("Series", () => {
975
1321
  expect(iter.next().value).toEqual(3);
976
1322
  expect(iter.next().value).toEqual(4);
977
1323
  });
1324
+
1325
+ it("should iterate over the whole series", () => {
1326
+ const s = new Series(new Float32Array([1, 2, 3, 4, 5]));
1327
+ const iter = s.subIterator(0, 6);
1328
+ expect(iter.next().value).toEqual(1);
1329
+ expect(iter.next().value).toEqual(2);
1330
+ expect(iter.next().value).toEqual(3);
1331
+ expect(iter.next().value).toEqual(4);
1332
+ expect(iter.next().value).toEqual(5);
1333
+ expect(iter.next().done).toBe(true);
1334
+ });
978
1335
  });
979
1336
 
980
- describe("subIterAlignment", () => {
1337
+ describe("subAlignmentIterator", () => {
981
1338
  it("should return an iterator over a sub-series", () => {
982
1339
  const s = new Series({
983
1340
  data: new Float32Array([1, 2, 3, 4, 5]),
@@ -988,9 +1345,10 @@ describe("Series", () => {
988
1345
  expect(iter.next().value).toEqual(3);
989
1346
  expect(iter.next().done).toBe(true);
990
1347
  });
1348
+
991
1349
  it("should clamp the bounds to the alignment", () => {
992
1350
  const s = new Series({
993
- data: new Float32Array([1, 2, 3, 4, 5]),
1351
+ data: new Float32Array([1, 2, 3, 4, 5]), // 2n, 3n, 4n, 5n,
994
1352
  alignment: 2n,
995
1353
  });
996
1354
  const iter = s.subAlignmentIterator(1n, 5n);
@@ -999,6 +1357,102 @@ describe("Series", () => {
999
1357
  expect(iter.next().value).toEqual(3);
1000
1358
  expect(iter.next().done).toBe(true);
1001
1359
  });
1360
+
1361
+ it("should handle series with alignment multiple", () => {
1362
+ const s = new Series({
1363
+ // 2n, 4n, 6n, 8n, 10n,
1364
+ data: new Float32Array([1, 2, 3, 4, 5]),
1365
+ alignment: 2n,
1366
+ alignmentMultiple: 2n,
1367
+ });
1368
+ const iter = s.subAlignmentIterator(1n, 5n);
1369
+ // (1n - 2n) / 2n = 0
1370
+ // (5n - 2n) / 2n = 1
1371
+ expect(iter.next().value).toEqual(1);
1372
+ expect(iter.next().value).toEqual(2);
1373
+ expect(iter.next().value).toBeUndefined();
1374
+ expect(iter.next().done).toBe(true);
1375
+ });
1376
+
1377
+ it("should correctly iterate over a series with alignmentMultiple > 1", () => {
1378
+ const series = new Series({
1379
+ data: new Float32Array([10, 20, 30, 40, 50]),
1380
+ alignment: 100n,
1381
+ alignmentMultiple: 10n,
1382
+ });
1383
+ const iter = series.subAlignmentIterator(110n, 140n);
1384
+ expect(iter.next().value).toBe(20);
1385
+ expect(iter.next().value).toBe(30);
1386
+ expect(iter.next().value).toBe(40);
1387
+ expect(iter.next().done).toBe(true);
1388
+ });
1389
+
1390
+ it("should handle partial ranges with alignmentMultiple", () => {
1391
+ const series = new Series({
1392
+ data: new Float32Array([1, 2, 3, 4]),
1393
+ alignment: 0n,
1394
+ alignmentMultiple: 5n,
1395
+ });
1396
+ // Series covers alignments 0, 5, 10, 15
1397
+ const iter = series.subAlignmentIterator(3n, 12n);
1398
+ expect(iter.next().value).toBe(2); // alignment 5
1399
+ expect(iter.next().value).toBe(3); // alignment 10
1400
+ expect(iter.next().done).toBe(true);
1401
+ });
1402
+
1403
+ it("should handle when start alignment doesn't align with the multiple grid", () => {
1404
+ const series = new Series({
1405
+ data: new Float32Array([100, 200, 300]),
1406
+ alignment: 50n,
1407
+ alignmentMultiple: 25n,
1408
+ });
1409
+ // Series covers alignments 50, 75, 100
1410
+ const iter = series.subAlignmentIterator(60n, 95n);
1411
+ expect(iter.next().value).toBe(200); // alignment 75
1412
+ expect(iter.next().done).toBe(true);
1413
+ });
1414
+
1415
+ it("should return empty iterator when range is outside series bounds", () => {
1416
+ const series = new Series({
1417
+ data: new Float32Array([1, 2, 3]),
1418
+ alignment: 100n,
1419
+ alignmentMultiple: 10n,
1420
+ });
1421
+ // Series covers alignments 100, 110, 120
1422
+ const iter = series.subAlignmentIterator(200n, 300n);
1423
+ expect(iter.next().done).toBe(true);
1424
+ });
1425
+
1426
+ it("should handle decimation scenario with alignmentMultiple", () => {
1427
+ // Simulating decimated data where every 4th sample is kept
1428
+ const series = new Series({
1429
+ data: new Float32Array([1, 5, 9, 13, 17]),
1430
+ alignment: 0n,
1431
+ alignmentMultiple: 4n,
1432
+ });
1433
+ // Series covers alignments 0, 4, 8, 12, 16
1434
+ const iter = series.subAlignmentIterator(2n, 14n);
1435
+ expect(iter.next().value).toBe(5); // alignment 4
1436
+ expect(iter.next().value).toBe(9); // alignment 8
1437
+ expect(iter.next().value).toBe(13); // alignment 12
1438
+ expect(iter.next().done).toBe(true);
1439
+ });
1440
+
1441
+ it("should handle averaging scenario with alignmentMultiple", () => {
1442
+ // Simulating averaged data where each value represents 10 samples
1443
+ const series = new Series({
1444
+ data: new Float32Array([10, 20, 30]),
1445
+ alignment: 1000n,
1446
+ alignmentMultiple: 10n,
1447
+ });
1448
+ // Series covers alignments 1000-1009, 1010-1019, 1020-1029
1449
+ const iter = series.subAlignmentIterator(1005n, 1025n);
1450
+ // Math.ceil((1005 - 1000) / 10) = 1
1451
+ // Math.ceil((1025 - 1000) / 10) = 3
1452
+ expect(iter.next().value).toBe(20); // alignment 1010
1453
+ expect(iter.next().value).toBe(30); // alignment 1020
1454
+ expect(iter.next().done).toBe(true);
1455
+ });
1002
1456
  });
1003
1457
 
1004
1458
  describe("bounds", () => {
@@ -1317,6 +1771,93 @@ describe("MultiSeries", () => {
1317
1771
  expect(iter.next().value).toEqual(10);
1318
1772
  expect(iter.next().done).toBe(true);
1319
1773
  });
1774
+
1775
+ describe("with alignmentMultiple", () => {
1776
+ it("should work with MultiSeries having different alignmentMultiples", () => {
1777
+ const s1 = new Series({
1778
+ data: new Float32Array([1, 2, 3]),
1779
+ alignment: 0n,
1780
+ alignmentMultiple: 2n,
1781
+ });
1782
+ // s1 covers alignments 0, 2, 4
1783
+
1784
+ const s2 = new Series({
1785
+ data: new Float32Array([10, 20, 30]),
1786
+ alignment: 10n,
1787
+ alignmentMultiple: 5n,
1788
+ });
1789
+ // s2 covers alignments 10, 15, 20
1790
+
1791
+ const multi = new MultiSeries([s1, s2]);
1792
+ const iter = multi.subAlignmentIterator(3n, 18n);
1793
+ expect(iter.next().value).toBe(3); // alignment 4 from s1
1794
+ expect(iter.next().value).toBe(10); // alignment 10 from s2
1795
+ expect(iter.next().value).toBe(20); // alignment 15 from s2
1796
+ expect(iter.next().done).toBe(true);
1797
+ });
1798
+
1799
+ it("should handle MultiSeries with gaps between series when using alignmentMultiple", () => {
1800
+ const s1 = new Series({
1801
+ data: new Float32Array([1, 2]),
1802
+ alignment: 0n,
1803
+ alignmentMultiple: 3n,
1804
+ });
1805
+ const s2 = new Series({
1806
+ data: new Float32Array([100, 200]),
1807
+ alignment: 100n,
1808
+ alignmentMultiple: 50n,
1809
+ });
1810
+ const multi = new MultiSeries([s1, s2]);
1811
+ const iter = multi.subAlignmentIterator(2n, 120n);
1812
+ expect(iter.next().value).toBe(2);
1813
+ expect(iter.next().value).toBe(100);
1814
+ expect(iter.next().done).toBe(true);
1815
+ });
1816
+
1817
+ it("should correctly calculate indices with very large alignmentMultiple values", () => {
1818
+ const series = new Series({
1819
+ data: new Float32Array([1, 2]),
1820
+ alignment: 1000000000n,
1821
+ alignmentMultiple: 1000000n,
1822
+ });
1823
+ const iter = series.subAlignmentIterator(1000500000n, 1001500000n);
1824
+ expect(iter.next().value).toBe(2);
1825
+ expect(iter.next().done).toBe(true);
1826
+ });
1827
+
1828
+ it("should handle edge case where start equals end with alignmentMultiple", () => {
1829
+ const series = new Series({
1830
+ data: new Float32Array([1, 2, 3]),
1831
+ alignment: 10n,
1832
+ alignmentMultiple: 5n,
1833
+ });
1834
+ const iter = series.subAlignmentIterator(15n, 15n);
1835
+ expect(iter.next().done).toBe(true);
1836
+ });
1837
+
1838
+ it("should work correctly when iterating exactly one sample with alignmentMultiple", () => {
1839
+ const series = new Series({
1840
+ data: new Float32Array([10, 20, 30]),
1841
+ alignment: 100n,
1842
+ alignmentMultiple: 10n,
1843
+ });
1844
+ const iter = series.subAlignmentIterator(110n, 120n);
1845
+ expect(iter.next().value).toBe(20);
1846
+ expect(iter.next().done).toBe(true);
1847
+ });
1848
+
1849
+ it("should handle negative start indices correctly with alignmentMultiple", () => {
1850
+ const series = new Series({
1851
+ data: new Float32Array([1, 2, 3]),
1852
+ alignment: 50n,
1853
+ alignmentMultiple: 10n,
1854
+ });
1855
+ const iter = series.subAlignmentIterator(40n, 65n);
1856
+ expect(iter.next().value).toBe(1);
1857
+ expect(iter.next().value).toBe(2);
1858
+ expect(iter.next().done).toBe(true);
1859
+ });
1860
+ });
1320
1861
  });
1321
1862
 
1322
1863
  describe("parseJSON", () => {
@@ -66,6 +66,7 @@ interface BaseSeriesArgs {
66
66
  sampleOffset?: math.Numeric;
67
67
  glBufferUsage?: GLBufferUsage;
68
68
  alignment?: bigint;
69
+ alignmentMultiple?: bigint;
69
70
  key?: string;
70
71
  }
71
72
 
@@ -165,6 +166,12 @@ export class Series<T extends TelemValue = TelemValue>
165
166
  * group. Useful for defining the position of the series within a channel's data.
166
167
  */
167
168
  readonly alignment: bigint = 0n;
169
+ /**
170
+ * Alignment multiple defines the number of alignment steps taken per sample. This is
171
+ * useful for when the samples in a series represent a partial view of the raw data
172
+ * i.e. decimation or averaging.
173
+ */
174
+ readonly alignmentMultiple: bigint = 1n;
168
175
  /** A cached minimum value. */
169
176
  private cachedMin?: math.Numeric;
170
177
  /** A cached maximum value. */
@@ -282,6 +289,7 @@ export class Series<T extends TelemValue = TelemValue>
282
289
  sampleOffset = 0,
283
290
  glBufferUsage = "static",
284
291
  alignment = 0n,
292
+ alignmentMultiple = 1n,
285
293
  key = id.create(),
286
294
  data,
287
295
  } = props;
@@ -294,6 +302,7 @@ export class Series<T extends TelemValue = TelemValue>
294
302
  this._data = data_._data;
295
303
  this.timeRange = data_.timeRange;
296
304
  this.alignment = data_.alignment;
305
+ this.alignmentMultiple = data_.alignmentMultiple;
297
306
  this.cachedMin = data_.cachedMin;
298
307
  this.cachedMax = data_.cachedMax;
299
308
  this.writePos = data_.writePos;
@@ -369,6 +378,7 @@ export class Series<T extends TelemValue = TelemValue>
369
378
 
370
379
  this.key = key;
371
380
  this.alignment = alignment;
381
+ this.alignmentMultiple = alignmentMultiple;
372
382
  this.sampleOffset = sampleOffset ?? 0;
373
383
  this.timeRange = timeRange ?? TimeRange.ZERO;
374
384
  this.gl = {
@@ -685,7 +695,7 @@ export class Series<T extends TelemValue = TelemValue>
685
695
  atAlignment(alignment: bigint, required?: false): T | undefined;
686
696
 
687
697
  atAlignment(alignment: bigint, required?: boolean): T | undefined {
688
- const index = Number(alignment - this.alignment);
698
+ const index = Number((alignment - this.alignment) / this.alignmentMultiple);
689
699
  if (index < 0 || index >= this.length) {
690
700
  if (required === true) throw new Error(`[series] - no value at index ${index}`);
691
701
  return undefined;
@@ -873,7 +883,10 @@ export class Series<T extends TelemValue = TelemValue>
873
883
  * is exclusive.
874
884
  */
875
885
  get alignmentBounds(): bounds.Bounds<bigint> {
876
- return bounds.construct(this.alignment, this.alignment + BigInt(this.length));
886
+ return bounds.construct(
887
+ this.alignment,
888
+ this.alignment + BigInt(this.length) * this.alignmentMultiple,
889
+ );
877
890
  }
878
891
 
879
892
  private maybeGarbageCollectGLBuffer(gl: GLBufferController): void {
@@ -946,11 +959,13 @@ export class Series<T extends TelemValue = TelemValue>
946
959
  * @returns An iterator over the specified alignment range.
947
960
  */
948
961
  subAlignmentIterator(start: bigint, end: bigint): Iterator<T> {
949
- return new SubIterator(
950
- this,
951
- Number(start - this.alignment),
952
- Number(end - this.alignment),
962
+ const startIdx = Math.ceil(
963
+ Number(start - this.alignment) / Number(this.alignmentMultiple),
953
964
  );
965
+ const endIdx = Math.ceil(
966
+ Number(end - this.alignment) / Number(this.alignmentMultiple),
967
+ );
968
+ return new SubIterator(this, startIdx, endIdx);
954
969
  }
955
970
 
956
971
  private subBytes(start: number, end?: number): Series {
@@ -1041,7 +1056,7 @@ class SubIterator<T> implements Iterator<T> {
1041
1056
 
1042
1057
  constructor(series: Series, start: number, end: number) {
1043
1058
  this.series = series;
1044
- const b = bounds.construct(0, series.length);
1059
+ const b = bounds.construct(0, series.length + 1);
1045
1060
  this.end = bounds.clamp(b, end);
1046
1061
  this.index = bounds.clamp(b, start);
1047
1062
  }
@@ -1360,7 +1375,9 @@ export class MultiSeries<T extends TelemValue = TelemValue> implements Iterable<
1360
1375
  if (start < ser.alignment) break;
1361
1376
  else if (start >= ser.alignmentBounds.upper) startIdx += ser.length;
1362
1377
  else if (bounds.contains(ser.alignmentBounds, start)) {
1363
- startIdx += Number(start - ser.alignment);
1378
+ startIdx += Math.ceil(
1379
+ Number(start - ser.alignment) / Number(ser.alignmentMultiple),
1380
+ );
1364
1381
  break;
1365
1382
  }
1366
1383
  }
@@ -1370,7 +1387,9 @@ export class MultiSeries<T extends TelemValue = TelemValue> implements Iterable<
1370
1387
  if (end < ser.alignment) break;
1371
1388
  else if (end >= ser.alignmentBounds.upper) endIdx += ser.length;
1372
1389
  else if (bounds.contains(ser.alignmentBounds, end)) {
1373
- endIdx += Number(end - ser.alignment);
1390
+ endIdx += Math.ceil(
1391
+ Number(end - ser.alignment) / Number(ser.alignmentMultiple),
1392
+ );
1374
1393
  break;
1375
1394
  }
1376
1395
  }