@logtape/logtape 1.1.0-dev.338 → 1.1.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.
- package/deno.json +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/mod.cjs +1 -0
- package/dist/mod.d.cts +2 -2
- package/dist/mod.d.ts +2 -2
- package/dist/mod.js +2 -2
- package/dist/sink.cjs +136 -0
- package/dist/sink.d.cts +74 -1
- package/dist/sink.d.cts.map +1 -1
- package/dist/sink.d.ts +74 -1
- package/dist/sink.d.ts.map +1 -1
- package/dist/sink.js +136 -1
- package/dist/sink.js.map +1 -1
- package/package.json +1 -1
- package/src/mod.ts +2 -0
- package/src/sink.test.ts +833 -0
- package/src/sink.ts +269 -1
package/src/sink.test.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { LogLevel } from "./level.ts";
|
|
|
11
11
|
import type { LogRecord } from "./record.ts";
|
|
12
12
|
import {
|
|
13
13
|
type AsyncSink,
|
|
14
|
+
fingersCrossed,
|
|
14
15
|
fromAsyncSink,
|
|
15
16
|
getConsoleSink,
|
|
16
17
|
getStreamSink,
|
|
@@ -873,3 +874,835 @@ test("fromAsyncSink() - empty async sink", async () => {
|
|
|
873
874
|
// Test passes if no errors thrown
|
|
874
875
|
assert(true);
|
|
875
876
|
});
|
|
877
|
+
|
|
878
|
+
test("fingersCrossed() - basic functionality", () => {
|
|
879
|
+
const buffer: LogRecord[] = [];
|
|
880
|
+
const sink = fingersCrossed(buffer.push.bind(buffer));
|
|
881
|
+
|
|
882
|
+
// Add debug and info logs - should be buffered
|
|
883
|
+
sink(trace);
|
|
884
|
+
sink(debug);
|
|
885
|
+
sink(info);
|
|
886
|
+
assertEquals(buffer.length, 0); // Not flushed yet
|
|
887
|
+
|
|
888
|
+
// Add warning - still buffered (default trigger is error)
|
|
889
|
+
sink(warning);
|
|
890
|
+
assertEquals(buffer.length, 0);
|
|
891
|
+
|
|
892
|
+
// Add error - should trigger flush
|
|
893
|
+
sink(error);
|
|
894
|
+
assertEquals(buffer, [trace, debug, info, warning, error]);
|
|
895
|
+
|
|
896
|
+
// After trigger, logs pass through directly
|
|
897
|
+
sink(fatal);
|
|
898
|
+
assertEquals(buffer, [trace, debug, info, warning, error, fatal]);
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
test("fingersCrossed() - custom trigger level", () => {
|
|
902
|
+
const buffer: LogRecord[] = [];
|
|
903
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
904
|
+
triggerLevel: "warning",
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
// Add logs below warning
|
|
908
|
+
sink(trace);
|
|
909
|
+
sink(debug);
|
|
910
|
+
sink(info);
|
|
911
|
+
assertEquals(buffer.length, 0);
|
|
912
|
+
|
|
913
|
+
// Warning should trigger flush
|
|
914
|
+
sink(warning);
|
|
915
|
+
assertEquals(buffer, [trace, debug, info, warning]);
|
|
916
|
+
|
|
917
|
+
// Subsequent logs pass through
|
|
918
|
+
sink(error);
|
|
919
|
+
assertEquals(buffer, [trace, debug, info, warning, error]);
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
test("fingersCrossed() - buffer overflow protection", () => {
|
|
923
|
+
const buffer: LogRecord[] = [];
|
|
924
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
925
|
+
maxBufferSize: 3,
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
// Add more logs than buffer size
|
|
929
|
+
sink(trace);
|
|
930
|
+
sink(debug);
|
|
931
|
+
sink(info);
|
|
932
|
+
sink(warning); // Should drop trace
|
|
933
|
+
assertEquals(buffer.length, 0); // Still buffered
|
|
934
|
+
|
|
935
|
+
// Trigger flush
|
|
936
|
+
sink(error);
|
|
937
|
+
// Should only have last 3 records + error
|
|
938
|
+
assertEquals(buffer, [debug, info, warning, error]);
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
test("fingersCrossed() - multiple trigger events", () => {
|
|
942
|
+
const buffer: LogRecord[] = [];
|
|
943
|
+
const sink = fingersCrossed(buffer.push.bind(buffer));
|
|
944
|
+
|
|
945
|
+
// First batch
|
|
946
|
+
sink(debug);
|
|
947
|
+
sink(info);
|
|
948
|
+
sink(error); // Trigger
|
|
949
|
+
assertEquals(buffer, [debug, info, error]);
|
|
950
|
+
|
|
951
|
+
// After trigger, everything passes through
|
|
952
|
+
sink(debug);
|
|
953
|
+
assertEquals(buffer, [debug, info, error, debug]);
|
|
954
|
+
|
|
955
|
+
sink(error); // Another error
|
|
956
|
+
assertEquals(buffer, [debug, info, error, debug, error]);
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
test("fingersCrossed() - trigger includes fatal", () => {
|
|
960
|
+
const buffer: LogRecord[] = [];
|
|
961
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
962
|
+
triggerLevel: "error",
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
sink(debug);
|
|
966
|
+
sink(info);
|
|
967
|
+
assertEquals(buffer.length, 0);
|
|
968
|
+
|
|
969
|
+
// Fatal should also trigger (since it's >= error)
|
|
970
|
+
sink(fatal);
|
|
971
|
+
assertEquals(buffer, [debug, info, fatal]);
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
test("fingersCrossed() - category isolation descendant mode", () => {
|
|
975
|
+
const buffer: LogRecord[] = [];
|
|
976
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
977
|
+
isolateByCategory: "descendant",
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
// Create test records with different categories
|
|
981
|
+
const appDebug: LogRecord = {
|
|
982
|
+
...debug,
|
|
983
|
+
category: ["app"],
|
|
984
|
+
};
|
|
985
|
+
const appModuleDebug: LogRecord = {
|
|
986
|
+
...debug,
|
|
987
|
+
category: ["app", "module"],
|
|
988
|
+
};
|
|
989
|
+
const appModuleSubDebug: LogRecord = {
|
|
990
|
+
...debug,
|
|
991
|
+
category: ["app", "module", "sub"],
|
|
992
|
+
};
|
|
993
|
+
const otherDebug: LogRecord = {
|
|
994
|
+
...debug,
|
|
995
|
+
category: ["other"],
|
|
996
|
+
};
|
|
997
|
+
const appError: LogRecord = {
|
|
998
|
+
...error,
|
|
999
|
+
category: ["app"],
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
// Buffer logs in different categories
|
|
1003
|
+
sink(appDebug);
|
|
1004
|
+
sink(appModuleDebug);
|
|
1005
|
+
sink(appModuleSubDebug);
|
|
1006
|
+
sink(otherDebug);
|
|
1007
|
+
assertEquals(buffer.length, 0);
|
|
1008
|
+
|
|
1009
|
+
// Trigger in parent category
|
|
1010
|
+
sink(appError);
|
|
1011
|
+
|
|
1012
|
+
// Should flush parent and all descendants, but not other
|
|
1013
|
+
assertEquals(buffer.length, 4); // app, app.module, app.module.sub, and trigger
|
|
1014
|
+
assert(buffer.includes(appDebug));
|
|
1015
|
+
assert(buffer.includes(appModuleDebug));
|
|
1016
|
+
assert(buffer.includes(appModuleSubDebug));
|
|
1017
|
+
assert(buffer.includes(appError));
|
|
1018
|
+
assert(!buffer.includes(otherDebug));
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
test("fingersCrossed() - category isolation ancestor mode", () => {
|
|
1022
|
+
const buffer: LogRecord[] = [];
|
|
1023
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1024
|
+
isolateByCategory: "ancestor",
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
// Create test records
|
|
1028
|
+
const appDebug: LogRecord = {
|
|
1029
|
+
...debug,
|
|
1030
|
+
category: ["app"],
|
|
1031
|
+
};
|
|
1032
|
+
const appModuleDebug: LogRecord = {
|
|
1033
|
+
...debug,
|
|
1034
|
+
category: ["app", "module"],
|
|
1035
|
+
};
|
|
1036
|
+
const appModuleSubDebug: LogRecord = {
|
|
1037
|
+
...debug,
|
|
1038
|
+
category: ["app", "module", "sub"],
|
|
1039
|
+
};
|
|
1040
|
+
const appModuleSubError: LogRecord = {
|
|
1041
|
+
...error,
|
|
1042
|
+
category: ["app", "module", "sub"],
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
// Buffer logs
|
|
1046
|
+
sink(appDebug);
|
|
1047
|
+
sink(appModuleDebug);
|
|
1048
|
+
sink(appModuleSubDebug);
|
|
1049
|
+
assertEquals(buffer.length, 0);
|
|
1050
|
+
|
|
1051
|
+
// Trigger in child category
|
|
1052
|
+
sink(appModuleSubError);
|
|
1053
|
+
|
|
1054
|
+
// Should flush child and all ancestors
|
|
1055
|
+
assertEquals(buffer.length, 4);
|
|
1056
|
+
assert(buffer.includes(appDebug));
|
|
1057
|
+
assert(buffer.includes(appModuleDebug));
|
|
1058
|
+
assert(buffer.includes(appModuleSubDebug));
|
|
1059
|
+
assert(buffer.includes(appModuleSubError));
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
test("fingersCrossed() - category isolation both mode", () => {
|
|
1063
|
+
const buffer: LogRecord[] = [];
|
|
1064
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1065
|
+
isolateByCategory: "both",
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
// Create test records
|
|
1069
|
+
const rootDebug: LogRecord = {
|
|
1070
|
+
...debug,
|
|
1071
|
+
category: ["app"],
|
|
1072
|
+
};
|
|
1073
|
+
const parentDebug: LogRecord = {
|
|
1074
|
+
...debug,
|
|
1075
|
+
category: ["app", "parent"],
|
|
1076
|
+
};
|
|
1077
|
+
const siblingDebug: LogRecord = {
|
|
1078
|
+
...debug,
|
|
1079
|
+
category: ["app", "sibling"],
|
|
1080
|
+
};
|
|
1081
|
+
const childDebug: LogRecord = {
|
|
1082
|
+
...debug,
|
|
1083
|
+
category: ["app", "parent", "child"],
|
|
1084
|
+
};
|
|
1085
|
+
const unrelatedDebug: LogRecord = {
|
|
1086
|
+
...debug,
|
|
1087
|
+
category: ["other"],
|
|
1088
|
+
};
|
|
1089
|
+
const parentError: LogRecord = {
|
|
1090
|
+
...error,
|
|
1091
|
+
category: ["app", "parent"],
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
// Buffer logs
|
|
1095
|
+
sink(rootDebug);
|
|
1096
|
+
sink(parentDebug);
|
|
1097
|
+
sink(siblingDebug);
|
|
1098
|
+
sink(childDebug);
|
|
1099
|
+
sink(unrelatedDebug);
|
|
1100
|
+
assertEquals(buffer.length, 0);
|
|
1101
|
+
|
|
1102
|
+
// Trigger in middle category
|
|
1103
|
+
sink(parentError);
|
|
1104
|
+
|
|
1105
|
+
// Should flush ancestors and descendants, but not siblings or unrelated
|
|
1106
|
+
assertEquals(buffer.length, 4);
|
|
1107
|
+
assert(buffer.includes(rootDebug)); // Ancestor
|
|
1108
|
+
assert(buffer.includes(parentDebug)); // Same
|
|
1109
|
+
assert(buffer.includes(childDebug)); // Descendant
|
|
1110
|
+
assert(buffer.includes(parentError)); // Trigger
|
|
1111
|
+
assert(!buffer.includes(siblingDebug)); // Sibling
|
|
1112
|
+
assert(!buffer.includes(unrelatedDebug)); // Unrelated
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
test("fingersCrossed() - custom category matcher", () => {
|
|
1116
|
+
const buffer: LogRecord[] = [];
|
|
1117
|
+
|
|
1118
|
+
// Custom matcher: only flush if categories share first element
|
|
1119
|
+
const customMatcher = (
|
|
1120
|
+
trigger: readonly string[],
|
|
1121
|
+
buffered: readonly string[],
|
|
1122
|
+
): boolean => {
|
|
1123
|
+
return trigger[0] === buffered[0];
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1126
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1127
|
+
isolateByCategory: customMatcher,
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
// Create test records
|
|
1131
|
+
const app1Debug: LogRecord = {
|
|
1132
|
+
...debug,
|
|
1133
|
+
category: ["app", "module1"],
|
|
1134
|
+
};
|
|
1135
|
+
const app2Debug: LogRecord = {
|
|
1136
|
+
...debug,
|
|
1137
|
+
category: ["app", "module2"],
|
|
1138
|
+
};
|
|
1139
|
+
const otherDebug: LogRecord = {
|
|
1140
|
+
...debug,
|
|
1141
|
+
category: ["other", "module"],
|
|
1142
|
+
};
|
|
1143
|
+
const appError: LogRecord = {
|
|
1144
|
+
...error,
|
|
1145
|
+
category: ["app", "module3"],
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
// Buffer logs
|
|
1149
|
+
sink(app1Debug);
|
|
1150
|
+
sink(app2Debug);
|
|
1151
|
+
sink(otherDebug);
|
|
1152
|
+
assertEquals(buffer.length, 0);
|
|
1153
|
+
|
|
1154
|
+
// Trigger
|
|
1155
|
+
sink(appError);
|
|
1156
|
+
|
|
1157
|
+
// Should flush all with same first category element
|
|
1158
|
+
assertEquals(buffer.length, 3);
|
|
1159
|
+
assert(buffer.includes(app1Debug));
|
|
1160
|
+
assert(buffer.includes(app2Debug));
|
|
1161
|
+
assert(buffer.includes(appError));
|
|
1162
|
+
assert(!buffer.includes(otherDebug));
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
test("fingersCrossed() - isolated buffers maintain separate states", () => {
|
|
1166
|
+
const buffer: LogRecord[] = [];
|
|
1167
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1168
|
+
isolateByCategory: "descendant",
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
// Create test records
|
|
1172
|
+
const app1Debug: LogRecord = {
|
|
1173
|
+
...debug,
|
|
1174
|
+
category: ["app1"],
|
|
1175
|
+
};
|
|
1176
|
+
const app1Error: LogRecord = {
|
|
1177
|
+
...error,
|
|
1178
|
+
category: ["app1"],
|
|
1179
|
+
};
|
|
1180
|
+
const app2Debug: LogRecord = {
|
|
1181
|
+
...debug,
|
|
1182
|
+
category: ["app2"],
|
|
1183
|
+
};
|
|
1184
|
+
const app2Info: LogRecord = {
|
|
1185
|
+
...info,
|
|
1186
|
+
category: ["app2"],
|
|
1187
|
+
};
|
|
1188
|
+
|
|
1189
|
+
// Buffer in app1
|
|
1190
|
+
sink(app1Debug);
|
|
1191
|
+
|
|
1192
|
+
// Trigger app1
|
|
1193
|
+
sink(app1Error);
|
|
1194
|
+
assertEquals(buffer, [app1Debug, app1Error]);
|
|
1195
|
+
|
|
1196
|
+
// Buffer in app2 (should still be buffering)
|
|
1197
|
+
sink(app2Debug);
|
|
1198
|
+
assertEquals(buffer, [app1Debug, app1Error]); // app2 still buffered
|
|
1199
|
+
|
|
1200
|
+
// Add more to triggered app1 (should pass through)
|
|
1201
|
+
sink(app1Debug);
|
|
1202
|
+
assertEquals(buffer, [app1Debug, app1Error, app1Debug]);
|
|
1203
|
+
|
|
1204
|
+
// app2 still buffering
|
|
1205
|
+
sink(app2Info);
|
|
1206
|
+
assertEquals(buffer, [app1Debug, app1Error, app1Debug]); // app2 still buffered
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
test("fingersCrossed() - chronological order in category isolation", () => {
|
|
1210
|
+
const buffer: LogRecord[] = [];
|
|
1211
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1212
|
+
isolateByCategory: "both",
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
// Create test records with different timestamps
|
|
1216
|
+
const app1: LogRecord = {
|
|
1217
|
+
...debug,
|
|
1218
|
+
category: ["app"],
|
|
1219
|
+
timestamp: 1000,
|
|
1220
|
+
};
|
|
1221
|
+
const app2: LogRecord = {
|
|
1222
|
+
...debug,
|
|
1223
|
+
category: ["app", "sub"],
|
|
1224
|
+
timestamp: 2000,
|
|
1225
|
+
};
|
|
1226
|
+
const app3: LogRecord = {
|
|
1227
|
+
...info,
|
|
1228
|
+
category: ["app"],
|
|
1229
|
+
timestamp: 3000,
|
|
1230
|
+
};
|
|
1231
|
+
const app4: LogRecord = {
|
|
1232
|
+
...debug,
|
|
1233
|
+
category: ["app", "sub"],
|
|
1234
|
+
timestamp: 4000,
|
|
1235
|
+
};
|
|
1236
|
+
const appError: LogRecord = {
|
|
1237
|
+
...error,
|
|
1238
|
+
category: ["app"],
|
|
1239
|
+
timestamp: 5000,
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
// Add out of order
|
|
1243
|
+
sink(app3);
|
|
1244
|
+
sink(app1);
|
|
1245
|
+
sink(app4);
|
|
1246
|
+
sink(app2);
|
|
1247
|
+
|
|
1248
|
+
// Trigger
|
|
1249
|
+
sink(appError);
|
|
1250
|
+
|
|
1251
|
+
// Should be sorted by timestamp
|
|
1252
|
+
assertEquals(buffer, [app1, app2, app3, app4, appError]);
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
test("fingersCrossed() - empty buffer trigger", () => {
|
|
1256
|
+
const buffer: LogRecord[] = [];
|
|
1257
|
+
const sink = fingersCrossed(buffer.push.bind(buffer));
|
|
1258
|
+
|
|
1259
|
+
// Trigger immediately without any buffered logs
|
|
1260
|
+
sink(error);
|
|
1261
|
+
assertEquals(buffer, [error]);
|
|
1262
|
+
|
|
1263
|
+
// Continue to pass through
|
|
1264
|
+
sink(debug);
|
|
1265
|
+
assertEquals(buffer, [error, debug]);
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
test("fingersCrossed() - buffer size per category in isolation mode", () => {
|
|
1269
|
+
const buffer: LogRecord[] = [];
|
|
1270
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1271
|
+
maxBufferSize: 2,
|
|
1272
|
+
isolateByCategory: "descendant",
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
// Create records for different categories
|
|
1276
|
+
const app1Trace: LogRecord = { ...trace, category: ["app1"] };
|
|
1277
|
+
const app1Debug: LogRecord = { ...debug, category: ["app1"] };
|
|
1278
|
+
const app1Info: LogRecord = { ...info, category: ["app1"] };
|
|
1279
|
+
const app2Trace: LogRecord = { ...trace, category: ["app2"] };
|
|
1280
|
+
const app2Debug: LogRecord = { ...debug, category: ["app2"] };
|
|
1281
|
+
const app1Error: LogRecord = { ...error, category: ["app1"] };
|
|
1282
|
+
|
|
1283
|
+
// Fill app1 buffer beyond max
|
|
1284
|
+
sink(app1Trace);
|
|
1285
|
+
sink(app1Debug);
|
|
1286
|
+
sink(app1Info); // Should drop app1Trace
|
|
1287
|
+
|
|
1288
|
+
// Fill app2 buffer
|
|
1289
|
+
sink(app2Trace);
|
|
1290
|
+
sink(app2Debug);
|
|
1291
|
+
|
|
1292
|
+
// Trigger app1
|
|
1293
|
+
sink(app1Error);
|
|
1294
|
+
|
|
1295
|
+
// Should only have last 2 from app1 + error
|
|
1296
|
+
assertEquals(buffer.length, 3);
|
|
1297
|
+
assert(!buffer.some((r) => r === app1Trace)); // Dropped
|
|
1298
|
+
assert(buffer.includes(app1Debug));
|
|
1299
|
+
assert(buffer.includes(app1Info));
|
|
1300
|
+
assert(buffer.includes(app1Error));
|
|
1301
|
+
// app2 records should not be flushed
|
|
1302
|
+
assert(!buffer.includes(app2Trace));
|
|
1303
|
+
assert(!buffer.includes(app2Debug));
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
test("fingersCrossed() - edge case: trigger level not in severity order", () => {
|
|
1307
|
+
const buffer: LogRecord[] = [];
|
|
1308
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1309
|
+
triggerLevel: "trace", // Lowest level triggers immediately
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
// Everything should pass through immediately
|
|
1313
|
+
sink(trace);
|
|
1314
|
+
assertEquals(buffer, [trace]);
|
|
1315
|
+
|
|
1316
|
+
sink(debug);
|
|
1317
|
+
assertEquals(buffer, [trace, debug]);
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
test("fingersCrossed() - edge case: invalid trigger level", () => {
|
|
1321
|
+
const buffer: LogRecord[] = [];
|
|
1322
|
+
|
|
1323
|
+
// Should throw TypeError during sink creation
|
|
1324
|
+
assertThrows(
|
|
1325
|
+
() => {
|
|
1326
|
+
fingersCrossed(buffer.push.bind(buffer), {
|
|
1327
|
+
triggerLevel: "invalid" as LogLevel,
|
|
1328
|
+
});
|
|
1329
|
+
},
|
|
1330
|
+
TypeError,
|
|
1331
|
+
"Invalid triggerLevel",
|
|
1332
|
+
);
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
test("fingersCrossed() - edge case: very large buffer size", () => {
|
|
1336
|
+
const buffer: LogRecord[] = [];
|
|
1337
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1338
|
+
maxBufferSize: Number.MAX_SAFE_INTEGER,
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
// Add many records
|
|
1342
|
+
for (let i = 0; i < 1000; i++) {
|
|
1343
|
+
sink(debug);
|
|
1344
|
+
}
|
|
1345
|
+
assertEquals(buffer.length, 0); // Still buffered
|
|
1346
|
+
|
|
1347
|
+
sink(error);
|
|
1348
|
+
assertEquals(buffer.length, 1001); // All 1000 + error
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
test("fingersCrossed() - edge case: zero buffer size", () => {
|
|
1352
|
+
const buffer: LogRecord[] = [];
|
|
1353
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1354
|
+
maxBufferSize: 0,
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
// Nothing should be buffered
|
|
1358
|
+
sink(debug);
|
|
1359
|
+
sink(info);
|
|
1360
|
+
assertEquals(buffer.length, 0);
|
|
1361
|
+
|
|
1362
|
+
// Trigger should still work
|
|
1363
|
+
sink(error);
|
|
1364
|
+
assertEquals(buffer, [error]); // Only the trigger
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
test("fingersCrossed() - edge case: negative buffer size", () => {
|
|
1368
|
+
const buffer: LogRecord[] = [];
|
|
1369
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1370
|
+
maxBufferSize: -1,
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
// Should behave like zero
|
|
1374
|
+
sink(debug);
|
|
1375
|
+
sink(info);
|
|
1376
|
+
assertEquals(buffer.length, 0);
|
|
1377
|
+
|
|
1378
|
+
sink(error);
|
|
1379
|
+
assertEquals(buffer, [error]);
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
test("fingersCrossed() - edge case: same record logged multiple times", () => {
|
|
1383
|
+
const buffer: LogRecord[] = [];
|
|
1384
|
+
const sink = fingersCrossed(buffer.push.bind(buffer));
|
|
1385
|
+
|
|
1386
|
+
// Log same record multiple times
|
|
1387
|
+
sink(debug);
|
|
1388
|
+
sink(debug);
|
|
1389
|
+
sink(debug);
|
|
1390
|
+
assertEquals(buffer.length, 0);
|
|
1391
|
+
|
|
1392
|
+
sink(error);
|
|
1393
|
+
// All instances should be preserved
|
|
1394
|
+
assertEquals(buffer.length, 4);
|
|
1395
|
+
assertEquals(buffer, [debug, debug, debug, error]);
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
test("fingersCrossed() - edge case: empty category array", () => {
|
|
1399
|
+
const buffer: LogRecord[] = [];
|
|
1400
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1401
|
+
isolateByCategory: "both",
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
const emptyCategory: LogRecord = {
|
|
1405
|
+
...debug,
|
|
1406
|
+
category: [],
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
const normalCategory: LogRecord = {
|
|
1410
|
+
...info,
|
|
1411
|
+
category: ["app"],
|
|
1412
|
+
};
|
|
1413
|
+
|
|
1414
|
+
const emptyError: LogRecord = {
|
|
1415
|
+
...error,
|
|
1416
|
+
category: [],
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1419
|
+
sink(emptyCategory);
|
|
1420
|
+
sink(normalCategory);
|
|
1421
|
+
assertEquals(buffer.length, 0);
|
|
1422
|
+
|
|
1423
|
+
// Trigger with empty category
|
|
1424
|
+
sink(emptyError);
|
|
1425
|
+
|
|
1426
|
+
// Only empty category should flush (no ancestors/descendants)
|
|
1427
|
+
assertEquals(buffer.length, 2);
|
|
1428
|
+
assert(buffer.includes(emptyCategory));
|
|
1429
|
+
assert(buffer.includes(emptyError));
|
|
1430
|
+
assert(!buffer.includes(normalCategory));
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
test("fingersCrossed() - edge case: category with special characters", () => {
|
|
1434
|
+
const buffer: LogRecord[] = [];
|
|
1435
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1436
|
+
isolateByCategory: "descendant",
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
// Category with null character (our separator)
|
|
1440
|
+
const specialCategory: LogRecord = {
|
|
1441
|
+
...debug,
|
|
1442
|
+
category: ["app\0special", "sub"],
|
|
1443
|
+
};
|
|
1444
|
+
|
|
1445
|
+
const normalCategory: LogRecord = {
|
|
1446
|
+
...info,
|
|
1447
|
+
category: ["app"],
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1450
|
+
const specialError: LogRecord = {
|
|
1451
|
+
...error,
|
|
1452
|
+
category: ["app\0special"],
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1455
|
+
sink(specialCategory);
|
|
1456
|
+
sink(normalCategory);
|
|
1457
|
+
assertEquals(buffer.length, 0);
|
|
1458
|
+
|
|
1459
|
+
// Should still work correctly despite special characters
|
|
1460
|
+
sink(specialError);
|
|
1461
|
+
|
|
1462
|
+
assertEquals(buffer.length, 2);
|
|
1463
|
+
assert(buffer.includes(specialCategory));
|
|
1464
|
+
assert(buffer.includes(specialError));
|
|
1465
|
+
assert(!buffer.includes(normalCategory));
|
|
1466
|
+
});
|
|
1467
|
+
|
|
1468
|
+
test("fingersCrossed() - edge case: rapid alternating triggers", () => {
|
|
1469
|
+
const buffer: LogRecord[] = [];
|
|
1470
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1471
|
+
isolateByCategory: "both",
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
const app1Debug: LogRecord = { ...debug, category: ["app1"] };
|
|
1475
|
+
const app2Debug: LogRecord = { ...debug, category: ["app2"] };
|
|
1476
|
+
const app1Error: LogRecord = { ...error, category: ["app1"] };
|
|
1477
|
+
const app2Error: LogRecord = { ...error, category: ["app2"] };
|
|
1478
|
+
|
|
1479
|
+
// Rapidly alternate between categories and triggers
|
|
1480
|
+
sink(app1Debug);
|
|
1481
|
+
sink(app2Debug);
|
|
1482
|
+
sink(app1Error); // Trigger app1
|
|
1483
|
+
assertEquals(buffer.length, 2); // app1Debug + app1Error
|
|
1484
|
+
|
|
1485
|
+
sink(app2Error); // Trigger app2
|
|
1486
|
+
assertEquals(buffer.length, 4); // Previous + app2Debug + app2Error
|
|
1487
|
+
|
|
1488
|
+
// After both triggered, everything passes through
|
|
1489
|
+
sink(app1Debug);
|
|
1490
|
+
sink(app2Debug);
|
|
1491
|
+
assertEquals(buffer.length, 6);
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
test("fingersCrossed() - edge case: custom matcher throws error", () => {
|
|
1495
|
+
const buffer: LogRecord[] = [];
|
|
1496
|
+
|
|
1497
|
+
const errorMatcher = (): boolean => {
|
|
1498
|
+
throw new Error("Matcher error");
|
|
1499
|
+
};
|
|
1500
|
+
|
|
1501
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1502
|
+
isolateByCategory: errorMatcher,
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
const app1Debug: LogRecord = { ...debug, category: ["app1"] };
|
|
1506
|
+
const app2Debug: LogRecord = { ...debug, category: ["app2"] };
|
|
1507
|
+
const app1Error: LogRecord = { ...error, category: ["app1"] };
|
|
1508
|
+
|
|
1509
|
+
sink(app1Debug);
|
|
1510
|
+
sink(app2Debug);
|
|
1511
|
+
|
|
1512
|
+
// Should handle error gracefully and still trigger
|
|
1513
|
+
try {
|
|
1514
|
+
sink(app1Error);
|
|
1515
|
+
} catch {
|
|
1516
|
+
// Should not throw to caller
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// At minimum, trigger record should be sent
|
|
1520
|
+
assert(buffer.includes(app1Error));
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
test("fingersCrossed() - edge case: circular category references", () => {
|
|
1524
|
+
const buffer: LogRecord[] = [];
|
|
1525
|
+
|
|
1526
|
+
// Custom matcher that creates circular logic
|
|
1527
|
+
const circularMatcher = (
|
|
1528
|
+
_trigger: readonly string[],
|
|
1529
|
+
_buffered: readonly string[],
|
|
1530
|
+
): boolean => {
|
|
1531
|
+
// Always return true, creating a circular flush
|
|
1532
|
+
return true;
|
|
1533
|
+
};
|
|
1534
|
+
|
|
1535
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1536
|
+
isolateByCategory: circularMatcher,
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
const app1: LogRecord = { ...debug, category: ["app1"] };
|
|
1540
|
+
const app2: LogRecord = { ...debug, category: ["app2"] };
|
|
1541
|
+
const app3: LogRecord = { ...debug, category: ["app3"] };
|
|
1542
|
+
const trigger: LogRecord = { ...error, category: ["trigger"] };
|
|
1543
|
+
|
|
1544
|
+
sink(app1);
|
|
1545
|
+
sink(app2);
|
|
1546
|
+
sink(app3);
|
|
1547
|
+
assertEquals(buffer.length, 0);
|
|
1548
|
+
|
|
1549
|
+
// Should flush all despite circular logic
|
|
1550
|
+
sink(trigger);
|
|
1551
|
+
assertEquals(buffer.length, 4);
|
|
1552
|
+
|
|
1553
|
+
// All buffers should be cleared after flush
|
|
1554
|
+
const newDebug: LogRecord = { ...debug, category: ["new"] };
|
|
1555
|
+
sink(newDebug);
|
|
1556
|
+
assertEquals(buffer.length, 4); // New category should be buffered
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
test("fingersCrossed() - edge case: timestamps in wrong order", () => {
|
|
1560
|
+
const buffer: LogRecord[] = [];
|
|
1561
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1562
|
+
isolateByCategory: "both",
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
const future: LogRecord = {
|
|
1566
|
+
...debug,
|
|
1567
|
+
category: ["app"],
|
|
1568
|
+
timestamp: Date.now() + 10000, // Future
|
|
1569
|
+
};
|
|
1570
|
+
|
|
1571
|
+
const past: LogRecord = {
|
|
1572
|
+
...info,
|
|
1573
|
+
category: ["app", "sub"],
|
|
1574
|
+
timestamp: Date.now() - 10000, // Past
|
|
1575
|
+
};
|
|
1576
|
+
|
|
1577
|
+
const present: LogRecord = {
|
|
1578
|
+
...warning,
|
|
1579
|
+
category: ["app"],
|
|
1580
|
+
timestamp: Date.now(),
|
|
1581
|
+
};
|
|
1582
|
+
|
|
1583
|
+
const trigger: LogRecord = {
|
|
1584
|
+
...error,
|
|
1585
|
+
category: ["app"],
|
|
1586
|
+
timestamp: Date.now() + 5000,
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
// Add in random order
|
|
1590
|
+
sink(future);
|
|
1591
|
+
sink(past);
|
|
1592
|
+
sink(present);
|
|
1593
|
+
|
|
1594
|
+
// Trigger
|
|
1595
|
+
sink(trigger);
|
|
1596
|
+
|
|
1597
|
+
// Should be sorted by timestamp
|
|
1598
|
+
assertEquals(buffer[0], past);
|
|
1599
|
+
assertEquals(buffer[1], present);
|
|
1600
|
+
assertEquals(buffer[2], future);
|
|
1601
|
+
assertEquals(buffer[3], trigger);
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
test("fingersCrossed() - edge case: NaN and Infinity in timestamps", () => {
|
|
1605
|
+
const buffer: LogRecord[] = [];
|
|
1606
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1607
|
+
isolateByCategory: "both",
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
const nanTime: LogRecord = {
|
|
1611
|
+
...debug,
|
|
1612
|
+
category: ["app"],
|
|
1613
|
+
timestamp: NaN,
|
|
1614
|
+
};
|
|
1615
|
+
|
|
1616
|
+
const infinityTime: LogRecord = {
|
|
1617
|
+
...info,
|
|
1618
|
+
category: ["app"],
|
|
1619
|
+
timestamp: Infinity,
|
|
1620
|
+
};
|
|
1621
|
+
|
|
1622
|
+
const negInfinityTime: LogRecord = {
|
|
1623
|
+
...warning,
|
|
1624
|
+
category: ["app"],
|
|
1625
|
+
timestamp: -Infinity,
|
|
1626
|
+
};
|
|
1627
|
+
|
|
1628
|
+
const normalTime: LogRecord = {
|
|
1629
|
+
...error,
|
|
1630
|
+
category: ["app"],
|
|
1631
|
+
timestamp: 1000,
|
|
1632
|
+
};
|
|
1633
|
+
|
|
1634
|
+
sink(nanTime);
|
|
1635
|
+
sink(infinityTime);
|
|
1636
|
+
sink(negInfinityTime);
|
|
1637
|
+
|
|
1638
|
+
// Should handle special values without crashing
|
|
1639
|
+
sink(normalTime);
|
|
1640
|
+
|
|
1641
|
+
// Check all records are present (order might vary with NaN)
|
|
1642
|
+
assertEquals(buffer.length, 4);
|
|
1643
|
+
assert(buffer.includes(nanTime));
|
|
1644
|
+
assert(buffer.includes(infinityTime));
|
|
1645
|
+
assert(buffer.includes(negInfinityTime));
|
|
1646
|
+
assert(buffer.includes(normalTime));
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
test("fingersCrossed() - edge case: undefined properties in record", () => {
|
|
1650
|
+
const buffer: LogRecord[] = [];
|
|
1651
|
+
const sink = fingersCrossed(buffer.push.bind(buffer));
|
|
1652
|
+
|
|
1653
|
+
const weirdRecord: LogRecord = {
|
|
1654
|
+
...debug,
|
|
1655
|
+
properties: {
|
|
1656
|
+
normal: "value",
|
|
1657
|
+
undef: undefined,
|
|
1658
|
+
nullish: null,
|
|
1659
|
+
nan: NaN,
|
|
1660
|
+
inf: Infinity,
|
|
1661
|
+
},
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
sink(weirdRecord);
|
|
1665
|
+
sink(error);
|
|
1666
|
+
|
|
1667
|
+
// Should preserve all properties as-is
|
|
1668
|
+
assertEquals(buffer[0].properties, weirdRecord.properties);
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
test("fingersCrossed() - edge case: very deep category hierarchy", () => {
|
|
1672
|
+
const buffer: LogRecord[] = [];
|
|
1673
|
+
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1674
|
+
isolateByCategory: "both",
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
// Create very deep hierarchy
|
|
1678
|
+
const deepCategory = Array.from({ length: 100 }, (_, i) => `level${i}`);
|
|
1679
|
+
const parentCategory = deepCategory.slice(0, 50);
|
|
1680
|
+
|
|
1681
|
+
const deepRecord: LogRecord = {
|
|
1682
|
+
...debug,
|
|
1683
|
+
category: deepCategory,
|
|
1684
|
+
};
|
|
1685
|
+
|
|
1686
|
+
const parentRecord: LogRecord = {
|
|
1687
|
+
...info,
|
|
1688
|
+
category: parentCategory,
|
|
1689
|
+
};
|
|
1690
|
+
|
|
1691
|
+
const deepError: LogRecord = {
|
|
1692
|
+
...error,
|
|
1693
|
+
category: deepCategory,
|
|
1694
|
+
};
|
|
1695
|
+
|
|
1696
|
+
sink(deepRecord);
|
|
1697
|
+
sink(parentRecord);
|
|
1698
|
+
assertEquals(buffer.length, 0);
|
|
1699
|
+
|
|
1700
|
+
// Should handle deep hierarchies
|
|
1701
|
+
sink(deepError);
|
|
1702
|
+
|
|
1703
|
+
// Both should flush (ancestor relationship)
|
|
1704
|
+
assertEquals(buffer.length, 3);
|
|
1705
|
+
assert(buffer.includes(deepRecord));
|
|
1706
|
+
assert(buffer.includes(parentRecord));
|
|
1707
|
+
assert(buffer.includes(deepError));
|
|
1708
|
+
});
|