@rotorsoft/act-tck 0.2.0 → 0.3.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/dist/index.js CHANGED
@@ -189,7 +189,7 @@ var runLoggerTck = (options) => {
189
189
  };
190
190
 
191
191
  // src/store-tck.ts
192
- import { ConcurrencyError } from "@rotorsoft/act";
192
+ import { ConcurrencyError, SNAP_EVENT, TOMBSTONE_EVENT } from "@rotorsoft/act";
193
193
  import { afterAll, beforeAll, describe as describe3, expect as expect3, it as it3 } from "vitest";
194
194
  var runStoreTck = (options) => {
195
195
  describe3(`TCK / Store / ${options.name}`, () => {
@@ -311,6 +311,14 @@ var runStoreTck = (options) => {
311
311
  expect3(backward.map((e) => e.id)).toEqual(
312
312
  [...committed].reverse().map((c) => c.id)
313
313
  );
314
+ const latest = await collect(store, {
315
+ stream: s,
316
+ stream_exact: true,
317
+ backward: true,
318
+ limit: 1
319
+ });
320
+ expect3(latest).toHaveLength(1);
321
+ expect3(latest[0].id).toBe(committed.at(-1).id);
314
322
  });
315
323
  it3("after/before bound the id range", async () => {
316
324
  const s = `q-bounds-${uid()}`;
@@ -1019,6 +1027,229 @@ var runStoreTck = (options) => {
1019
1027
  expect3(unblocked).toEqual([sibling]);
1020
1028
  });
1021
1029
  });
1030
+ describe3("query_stats", () => {
1031
+ it3("array input \u2014 returns head per stream, absent when not in input", async () => {
1032
+ const tag = uid();
1033
+ const sA = `qst-${tag}-a`;
1034
+ const sB = `qst-${tag}-b`;
1035
+ const sUnasked = `qst-${tag}-unasked`;
1036
+ await store.commit(
1037
+ sA,
1038
+ [inc(1), inc(2)],
1039
+ makeMeta({ stream: sA })
1040
+ );
1041
+ await store.commit(
1042
+ sB,
1043
+ [dec(5)],
1044
+ makeMeta({ stream: sB })
1045
+ );
1046
+ await store.commit(
1047
+ sUnasked,
1048
+ [inc(99)],
1049
+ makeMeta({ stream: sUnasked })
1050
+ );
1051
+ const stats = await store.query_stats([sA, sB]);
1052
+ expect3(stats.size).toBe(2);
1053
+ expect3(stats.get(sA)?.head.name).toBe("Incremented");
1054
+ expect3((stats.get(sA)?.head.data).amount).toBe(2);
1055
+ expect3(stats.get(sB)?.head.name).toBe("Decremented");
1056
+ expect3((stats.get(sB)?.head.data).amount).toBe(5);
1057
+ expect3(stats.has(sUnasked)).toBe(false);
1058
+ const empty = await store.query_stats([]);
1059
+ expect3(empty.size).toBe(0);
1060
+ const unknown = await store.query_stats([`qst-${tag}-missing`]);
1061
+ expect3(unknown.size).toBe(0);
1062
+ });
1063
+ it3("tail returns the earliest event per stream", async () => {
1064
+ const tag = uid();
1065
+ const s = `qst-tail-${tag}`;
1066
+ await store.commit(s, [inc(1)], makeMeta({ stream: s }));
1067
+ await store.commit(s, [inc(2)], makeMeta({ stream: s }));
1068
+ await store.commit(s, [inc(3)], makeMeta({ stream: s }));
1069
+ const stats = await store.query_stats([s], {
1070
+ tail: true
1071
+ });
1072
+ const r = stats.get(s);
1073
+ expect3(r?.head.name).toBe("Incremented");
1074
+ expect3((r?.head.data).amount).toBe(3);
1075
+ expect3(r?.tail?.name).toBe("Incremented");
1076
+ expect3((r?.tail?.data).amount).toBe(1);
1077
+ });
1078
+ it3("count + names \u2014 full aggregates including framework markers", async () => {
1079
+ const tag = uid();
1080
+ const s = `qst-cn-${tag}`;
1081
+ await store.commit(s, [inc(1)], makeMeta({ stream: s }));
1082
+ await store.truncate([{ stream: s, snapshot: { count: 99 } }]);
1083
+ await store.commit(
1084
+ s,
1085
+ [inc(2), inc(3), dec(1)],
1086
+ makeMeta({ stream: s })
1087
+ );
1088
+ const stats = await store.query_stats([s], {
1089
+ count: true,
1090
+ names: true
1091
+ });
1092
+ const r = stats.get(s);
1093
+ expect3(r?.count).toBe(4);
1094
+ expect3(r?.names?.[SNAP_EVENT]).toBe(1);
1095
+ expect3(r?.names?.Incremented).toBe(2);
1096
+ expect3(r?.names?.Decremented).toBe(1);
1097
+ expect3(r?.names?.[SNAP_EVENT]).toBe(1);
1098
+ });
1099
+ it3("exclude shifts head past filtered events; stream absent when all filtered", async () => {
1100
+ const tag = uid();
1101
+ const s = `qst-excl-${tag}`;
1102
+ const sAllOut = `qst-allout-${tag}`;
1103
+ await store.commit(
1104
+ s,
1105
+ [inc(1), dec(2), inc(3)],
1106
+ makeMeta({ stream: s })
1107
+ );
1108
+ await store.commit(
1109
+ sAllOut,
1110
+ [inc(7)],
1111
+ makeMeta({ stream: sAllOut })
1112
+ );
1113
+ const all = await store.query_stats([s]);
1114
+ expect3(all.get(s)?.head.name).toBe("Incremented");
1115
+ expect3((all.get(s)?.head.data).amount).toBe(3);
1116
+ const excl = await store.query_stats([s], {
1117
+ exclude: ["Incremented"]
1118
+ });
1119
+ expect3(excl.get(s)?.head.name).toBe("Decremented");
1120
+ expect3((excl.get(s)?.head.data).amount).toBe(2);
1121
+ const wipe = await store.query_stats([sAllOut], {
1122
+ exclude: ["Incremented", "Decremented", "Reset"]
1123
+ });
1124
+ expect3(wipe.has(sAllOut)).toBe(false);
1125
+ const noTomb = await store.query_stats([s], {
1126
+ exclude: [TOMBSTONE_EVENT]
1127
+ });
1128
+ expect3(noTomb.get(s)?.head.name).toBe("Incremented");
1129
+ });
1130
+ it3("before \u2014 time travel narrows head/tail/count", async () => {
1131
+ const tag = uid();
1132
+ const s = `qst-tt-${tag}`;
1133
+ const c1 = await store.commit(
1134
+ s,
1135
+ [inc(1)],
1136
+ makeMeta({ stream: s })
1137
+ );
1138
+ const c2 = await store.commit(
1139
+ s,
1140
+ [inc(2)],
1141
+ makeMeta({ stream: s })
1142
+ );
1143
+ await store.commit(s, [inc(3)], makeMeta({ stream: s }));
1144
+ const before = c2[0].id;
1145
+ const stats = await store.query_stats([s], {
1146
+ tail: true,
1147
+ count: true,
1148
+ before
1149
+ });
1150
+ const r = stats.get(s);
1151
+ expect3(r?.count).toBe(1);
1152
+ expect3(r?.head.id).toBe(c1[0].id);
1153
+ expect3(r?.tail?.id).toBe(c1[0].id);
1154
+ const empty = await store.query_stats([s], {
1155
+ before: 0
1156
+ });
1157
+ expect3(empty.has(s)).toBe(false);
1158
+ });
1159
+ it3("filter form \u2014 stream regex, stream_exact, empty {} match", async () => {
1160
+ const tag = uid();
1161
+ const sA = `qsf-${tag}-orders-1`;
1162
+ const sB = `qsf-${tag}-orders-2`;
1163
+ const sOther = `qsf-${tag}-users-1`;
1164
+ await store.commit(
1165
+ sA,
1166
+ [inc(1)],
1167
+ makeMeta({ stream: sA })
1168
+ );
1169
+ await store.commit(
1170
+ sB,
1171
+ [inc(2)],
1172
+ makeMeta({ stream: sB })
1173
+ );
1174
+ await store.commit(
1175
+ sOther,
1176
+ [inc(3)],
1177
+ makeMeta({ stream: sOther })
1178
+ );
1179
+ const orders = await store.query_stats({
1180
+ stream: `^qsf-${tag}-orders-`
1181
+ });
1182
+ expect3([...orders.keys()].sort()).toEqual([sA, sB].sort());
1183
+ const exact = await store.query_stats({
1184
+ stream: sA,
1185
+ stream_exact: true
1186
+ });
1187
+ expect3([...exact.keys()]).toEqual([sA]);
1188
+ const all = await store.query_stats({
1189
+ stream: `^qsf-${tag}-`
1190
+ });
1191
+ expect3([...all.keys()].sort()).toEqual([sA, sB, sOther].sort());
1192
+ });
1193
+ it3("compose with query_streams for subscription-level filters", async () => {
1194
+ const tag = uid();
1195
+ const a = `qsc-${tag}-a`;
1196
+ const b = `qsc-${tag}-b`;
1197
+ await store.subscribe([{ stream: a }, { stream: b }]);
1198
+ await store.commit(a, [inc(1)], makeMeta({ stream: a }));
1199
+ await store.commit(b, [inc(2)], makeMeta({ stream: b }));
1200
+ const leased = await store.claim(100, 0, `w-${uid()}`, 1e5);
1201
+ const mine = leased.find((l) => l.stream === a);
1202
+ expect3(mine).toBeDefined();
1203
+ const others = leased.filter((l) => l.stream !== a);
1204
+ await store.ack(others);
1205
+ await store.block([{ ...mine, error: "boom" }]);
1206
+ const blockedNames = [];
1207
+ await store.query_streams((p) => blockedNames.push(p.stream), {
1208
+ stream: `^qsc-${tag}-`,
1209
+ blocked: true
1210
+ });
1211
+ expect3(blockedNames).toEqual([a]);
1212
+ const stats = await store.query_stats(blockedNames);
1213
+ expect3(stats.get(a)?.head.name).toBe("Incremented");
1214
+ expect3(stats.has(b)).toBe(false);
1215
+ });
1216
+ it3("empty filter {} \u2014 matches every event-bearing stream", async () => {
1217
+ const tag = uid();
1218
+ const a = `qse-${tag}-a`;
1219
+ const b = `qse-${tag}-b`;
1220
+ await store.commit(a, [inc(1)], makeMeta({ stream: a }));
1221
+ await store.commit(b, [dec(2)], makeMeta({ stream: b }));
1222
+ const all = await store.query_stats({});
1223
+ expect3(all.has(a)).toBe(true);
1224
+ expect3(all.has(b)).toBe(true);
1225
+ });
1226
+ it3("stat-flag combinations \u2014 count-only, names-only, tail-only", async () => {
1227
+ const tag = uid();
1228
+ const s = `qsfl-${tag}`;
1229
+ await store.commit(
1230
+ s,
1231
+ [inc(1), inc(2), dec(3)],
1232
+ makeMeta({ stream: s })
1233
+ );
1234
+ const c = await store.query_stats([s], {
1235
+ count: true
1236
+ });
1237
+ expect3(c.get(s)?.count).toBe(3);
1238
+ expect3(c.get(s)?.names).toBeUndefined();
1239
+ expect3(c.get(s)?.tail).toBeUndefined();
1240
+ const n = await store.query_stats([s], {
1241
+ names: true
1242
+ });
1243
+ expect3(n.get(s)?.names).toEqual({ Incremented: 2, Decremented: 1 });
1244
+ expect3(n.get(s)?.count).toBeUndefined();
1245
+ expect3(n.get(s)?.tail).toBeUndefined();
1246
+ const t = await store.query_stats([s], { tail: true });
1247
+ expect3(t.get(s)?.tail?.name).toBe("Incremented");
1248
+ expect3((t.get(s)?.tail?.data).amount).toBe(1);
1249
+ expect3(t.get(s)?.count).toBeUndefined();
1250
+ expect3(t.get(s)?.names).toBeUndefined();
1251
+ });
1252
+ });
1022
1253
  describe3("query_streams anchor contract", () => {
1023
1254
  it3("plain regex without anchors is a substring match", async () => {
1024
1255
  const tag = uid();