@iann29/rastro 0.1.0-alpha.7 → 0.1.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -16
- package/agent/integration.md +14 -6
- package/agent/manifest.json +9 -7
- package/agent/manifest.schema.json +8 -5
- package/dist/client/federation.d.ts +13 -8
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +4 -1
- package/dist/client/federation.js.map +1 -1
- package/dist/client/index.d.ts +412 -10
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +55 -2
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +68 -4
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/constants.d.ts +4 -0
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +11 -0
- package/dist/component/constants.js.map +1 -1
- package/dist/component/coverage.d.ts +1 -0
- package/dist/component/coverage.d.ts.map +1 -1
- package/dist/component/coverage.js +13 -2
- package/dist/component/coverage.js.map +1 -1
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +1 -0
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +144 -2
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/reports.d.ts +75 -1
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +275 -3
- package/dist/component/reports.js.map +1 -1
- package/dist/component/retention.d.ts +4 -4
- package/dist/component/retention.d.ts.map +1 -1
- package/dist/component/retention.js +12 -2
- package/dist/component/retention.js.map +1 -1
- package/dist/component/sanitize.d.ts +9 -0
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +25 -0
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +65 -3
- package/dist/component/schema.js +25 -0
- package/dist/component/schema.js.map +1 -1
- package/docs/federation-setup.md +2 -0
- package/docs/federation.md +4 -0
- package/docs/upgrading.md +53 -10
- package/llms.txt +2 -0
- package/package.json +1 -1
- package/src/component/_generated/component.ts +55 -1
- package/src/component/constants.ts +11 -0
- package/src/component/coverage.ts +12 -1
- package/src/component/http.ts +1 -0
- package/src/component/ingest.ts +227 -0
- package/src/component/reports.ts +364 -2
- package/src/component/retention.ts +16 -1
- package/src/component/sanitize.ts +31 -0
- package/src/component/schema.ts +26 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { paginationOptsValidator, paginationResultValidator, } from "convex/server";
|
|
2
2
|
import { v } from "convex/values";
|
|
3
3
|
import { query } from "./_generated/server.js";
|
|
4
|
-
import { AGGREGATE_SHARDS, DAY_MS, FEATURE_REPORT_BYTE_RESERVE, FEATURE_REPORT_DOCUMENT_RESERVE, HOUR_MS, HOURLY_AGGREGATE_SHARDS, MAX_AFFILIATES_PER_SITE, MAX_FUNNELS_PER_SITE, MAX_FEATURE_ROLLUP_DAYS, MAX_FEATURE_ROLLUP_ROWS, MAX_GOALS_PER_SITE, MAX_JOURNEY_EVENTS, MAX_LIVE_VISITORS, MAX_OVERVIEW_AGGREGATE_ROWS, MAX_REPORT_BUCKETS, MAX_REPORT_RANGE_DAYS, MAX_REPORT_SITES, MAX_VITALS_REPORT_ROWS, REPORT_ROLLUP_SHARDS, VITALS_REPORT_PAGE_LIMIT, } from "./constants.js";
|
|
4
|
+
import { AGGREGATE_SHARDS, DAY_MS, FEATURE_REPORT_BYTE_RESERVE, FEATURE_REPORT_DOCUMENT_RESERVE, HOUR_MS, HOURLY_AGGREGATE_SHARDS, MAX_AFFILIATES_PER_SITE, MAX_FUNNELS_PER_SITE, MAX_FEATURE_ROLLUP_DAYS, MAX_FEATURE_ROLLUP_ROWS, MAX_GOALS_PER_SITE, MAX_JOURNEY_EVENTS, MAX_LIVE_VISITORS, MAX_OVERVIEW_AGGREGATE_ROWS, MAX_REPORT_BUCKETS, MAX_REPORT_RANGE_DAYS, MAX_REPORT_SITES, MAX_SITE_MAP_ROUTES_PER_DAY, MAX_SITE_MAP_ROWS, MAX_VITALS_REPORT_ROWS, REPORT_ROLLUP_SHARDS, ROUTE_TRANSITION_SLOTS, SITE_MAP_OTHER_ROUTE, VITALS_REPORT_PAGE_LIMIT, } from "./constants.js";
|
|
5
5
|
import { createVisitorSketch, estimateVisitors, unionVisitorSketches, } from "./cardinality.js";
|
|
6
6
|
import { fail } from "./errors.js";
|
|
7
7
|
import { compareJourneyEvents, hydrateEventContexts, loadSessionJourneyEvents, loadVisitorJourneyEvents, } from "./eventStore.js";
|
|
@@ -37,6 +37,8 @@ const liveSessionDocumentValidator = v.object({
|
|
|
37
37
|
device: v.string(),
|
|
38
38
|
pageviewCount: v.number(),
|
|
39
39
|
eventCount: v.number(),
|
|
40
|
+
previousPath: v.optional(v.string()),
|
|
41
|
+
movedAt: v.optional(v.number()),
|
|
40
42
|
});
|
|
41
43
|
const conversionDocumentValidator = v.object({
|
|
42
44
|
_id: v.id("conversions"),
|
|
@@ -334,9 +336,241 @@ export const liveVisitors = query({
|
|
|
334
336
|
device: row.device,
|
|
335
337
|
pageviewCount: row.pageviewCount,
|
|
336
338
|
eventCount: row.eventCount,
|
|
339
|
+
...(row.previousPath ? { previousPath: row.previousPath } : {}),
|
|
340
|
+
...(row.movedAt !== undefined ? { movedAt: row.movedAt } : {}),
|
|
337
341
|
}));
|
|
338
342
|
},
|
|
339
343
|
});
|
|
344
|
+
const siteMapRouteValidator = v.object({
|
|
345
|
+
route: v.string(),
|
|
346
|
+
pageviews: v.number(),
|
|
347
|
+
visitors: v.number(),
|
|
348
|
+
entries: v.number(),
|
|
349
|
+
exits: v.number(),
|
|
350
|
+
bounces: v.number(),
|
|
351
|
+
transitionsIn: v.number(),
|
|
352
|
+
transitionsOut: v.number(),
|
|
353
|
+
dwellMs: v.number(),
|
|
354
|
+
dwellSamples: v.number(),
|
|
355
|
+
daily: v.array(v.object({ bucketStart: v.number(), pageviews: v.number() })),
|
|
356
|
+
});
|
|
357
|
+
const siteMapTransitionValidator = v.object({
|
|
358
|
+
from: v.string(),
|
|
359
|
+
to: v.string(),
|
|
360
|
+
count: v.number(),
|
|
361
|
+
});
|
|
362
|
+
const siteMapCoverageDataset = "siteMapRollups";
|
|
363
|
+
/**
|
|
364
|
+
* Every route the sites served in the range and the transitions between
|
|
365
|
+
* them, read from the daily route rollups newest day first. Exits and
|
|
366
|
+
* bounces are derived rather than counted: a session enters once, leaves
|
|
367
|
+
* once, and every later pageview is a transition, so per route
|
|
368
|
+
* `exits = entries + transitionsIn - transitionsOut` and
|
|
369
|
+
* `bounces = entries - continued` hold without a session-end hook. The
|
|
370
|
+
* report never falls back to raw telemetry; days it could not cover are
|
|
371
|
+
* declared through `coverage` and `window`.
|
|
372
|
+
*/
|
|
373
|
+
export const siteMap = query({
|
|
374
|
+
args: {
|
|
375
|
+
siteIds: v.array(v.id("sites")),
|
|
376
|
+
from: v.number(),
|
|
377
|
+
to: v.number(),
|
|
378
|
+
},
|
|
379
|
+
returns: v.object({
|
|
380
|
+
from: v.number(),
|
|
381
|
+
to: v.number(),
|
|
382
|
+
window: v.union(v.object({ from: v.number(), to: v.number() }), v.null()),
|
|
383
|
+
routes: v.array(siteMapRouteValidator),
|
|
384
|
+
transitions: v.array(siteMapTransitionValidator),
|
|
385
|
+
truncated: v.object({ routes: v.boolean(), transitions: v.boolean() }),
|
|
386
|
+
metadata: reportMetadataValidator.extend({
|
|
387
|
+
visitors: v.object({
|
|
388
|
+
basis: v.literal("estimatedUnique"),
|
|
389
|
+
exact: v.boolean(),
|
|
390
|
+
}),
|
|
391
|
+
limits: v.object({
|
|
392
|
+
routesPerDay: v.number(),
|
|
393
|
+
transitionSlots: v.number(),
|
|
394
|
+
rows: v.number(),
|
|
395
|
+
}),
|
|
396
|
+
}),
|
|
397
|
+
}),
|
|
398
|
+
handler: async (ctx, args) => {
|
|
399
|
+
const siteIds = validateSiteIds(args.siteIds);
|
|
400
|
+
validateRange(args.from, args.to, MAX_FEATURE_ROLLUP_DAYS);
|
|
401
|
+
requireAlignedRange(args.from, args.to, DAY_MS, "site map");
|
|
402
|
+
const controls = await Promise.all(siteIds.map((siteId) => ctx.db
|
|
403
|
+
.query("analyticsCoverage")
|
|
404
|
+
.withIndex("by_siteId", (range) => range.eq("siteId", siteId))
|
|
405
|
+
.unique()));
|
|
406
|
+
const since = controls.every((control) => control?.siteMapSince !== undefined)
|
|
407
|
+
? Math.max(...controls.map((control) => control.siteMapSince))
|
|
408
|
+
: null;
|
|
409
|
+
const retainedBefore = controls.reduce((latest, control) => {
|
|
410
|
+
const before = control?.retained.find((item) => item.dataset === siteMapCoverageDataset)?.before;
|
|
411
|
+
return before === undefined ? latest : Math.max(latest ?? 0, before);
|
|
412
|
+
}, null);
|
|
413
|
+
const metadata = (state) => ({
|
|
414
|
+
basis: "dailyRollup",
|
|
415
|
+
coverage: {
|
|
416
|
+
state,
|
|
417
|
+
generation: 1,
|
|
418
|
+
availableFrom: since,
|
|
419
|
+
availableThrough: since === null ? null : args.to,
|
|
420
|
+
retainedBefore,
|
|
421
|
+
},
|
|
422
|
+
range: rangeSemantics(args.from, args.to),
|
|
423
|
+
visitors: { basis: "estimatedUnique", exact: false },
|
|
424
|
+
limits: {
|
|
425
|
+
routesPerDay: MAX_SITE_MAP_ROUTES_PER_DAY,
|
|
426
|
+
transitionSlots: ROUTE_TRANSITION_SLOTS,
|
|
427
|
+
rows: MAX_SITE_MAP_ROWS,
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
if (since === null ||
|
|
431
|
+
args.to < since ||
|
|
432
|
+
(retainedBefore !== null && args.to < retainedBefore)) {
|
|
433
|
+
return {
|
|
434
|
+
from: args.from,
|
|
435
|
+
to: args.to,
|
|
436
|
+
window: null,
|
|
437
|
+
routes: [],
|
|
438
|
+
transitions: [],
|
|
439
|
+
truncated: { routes: false, transitions: false },
|
|
440
|
+
metadata: metadata("unavailable"),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const readFrom = Math.max(args.from, Math.floor(since / DAY_MS) * DAY_MS, retainedBefore ?? 0);
|
|
444
|
+
let budget = MAX_SITE_MAP_ROWS;
|
|
445
|
+
let completeFrom = readFrom;
|
|
446
|
+
const rows = [];
|
|
447
|
+
for (const siteId of siteIds) {
|
|
448
|
+
const read = await readRouteRollups(ctx, siteId, readFrom, args.to, budget);
|
|
449
|
+
budget -= read.rows.length;
|
|
450
|
+
completeFrom = Math.max(completeFrom, read.completeFrom);
|
|
451
|
+
rows.push(...read.rows);
|
|
452
|
+
}
|
|
453
|
+
const windowFrom = Math.max(args.from, completeFrom, since, retainedBefore ?? 0);
|
|
454
|
+
const routes = new Map();
|
|
455
|
+
const totalsFor = (route) => {
|
|
456
|
+
let current = routes.get(route);
|
|
457
|
+
if (!current) {
|
|
458
|
+
current = {
|
|
459
|
+
pageviews: 0,
|
|
460
|
+
entries: 0,
|
|
461
|
+
continued: 0,
|
|
462
|
+
dwellMs: 0,
|
|
463
|
+
dwellSamples: 0,
|
|
464
|
+
transitionsIn: 0,
|
|
465
|
+
transitionsOut: 0,
|
|
466
|
+
sketch: createVisitorSketch(),
|
|
467
|
+
daily: new Map(),
|
|
468
|
+
};
|
|
469
|
+
routes.set(route, current);
|
|
470
|
+
}
|
|
471
|
+
return current;
|
|
472
|
+
};
|
|
473
|
+
const edges = new Map();
|
|
474
|
+
for (const row of rows) {
|
|
475
|
+
if (row.bucketStart < completeFrom)
|
|
476
|
+
continue;
|
|
477
|
+
const current = totalsFor(row.route);
|
|
478
|
+
current.pageviews += row.pageviews;
|
|
479
|
+
current.entries += row.entries;
|
|
480
|
+
current.continued += row.continued;
|
|
481
|
+
current.dwellMs += row.dwellMs;
|
|
482
|
+
current.dwellSamples += row.dwellSamples;
|
|
483
|
+
current.sketch = unionVisitorSketches(current.sketch, row.visitorSketch);
|
|
484
|
+
current.daily.set(row.bucketStart, (current.daily.get(row.bucketStart) ?? 0) + row.pageviews);
|
|
485
|
+
for (const target of row.outgoing) {
|
|
486
|
+
const key = `${row.route}\u0000${target.route}`;
|
|
487
|
+
const edge = edges.get(key) ?? {
|
|
488
|
+
from: row.route,
|
|
489
|
+
to: target.route,
|
|
490
|
+
count: 0,
|
|
491
|
+
};
|
|
492
|
+
edge.count += target.count;
|
|
493
|
+
edges.set(key, edge);
|
|
494
|
+
current.transitionsOut += target.count;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// A target without a row of its own in this window — folded past the
|
|
498
|
+
// daily cap, or already retained — counts under "(other)".
|
|
499
|
+
const transitions = new Map();
|
|
500
|
+
for (const edge of edges.values()) {
|
|
501
|
+
const to = routes.has(edge.to) ? edge.to : SITE_MAP_OTHER_ROUTE;
|
|
502
|
+
const key = `${edge.from}\u0000${to}`;
|
|
503
|
+
const merged = transitions.get(key) ?? { from: edge.from, to, count: 0 };
|
|
504
|
+
merged.count += edge.count;
|
|
505
|
+
transitions.set(key, merged);
|
|
506
|
+
totalsFor(to).transitionsIn += edge.count;
|
|
507
|
+
}
|
|
508
|
+
const routeRows = [...routes.entries()]
|
|
509
|
+
.map(([route, totals]) => ({
|
|
510
|
+
route,
|
|
511
|
+
pageviews: totals.pageviews,
|
|
512
|
+
visitors: estimateVisitors(totals.sketch),
|
|
513
|
+
entries: totals.entries,
|
|
514
|
+
exits: Math.max(0, totals.entries + totals.transitionsIn - totals.transitionsOut),
|
|
515
|
+
bounces: Math.max(0, totals.entries - totals.continued),
|
|
516
|
+
transitionsIn: totals.transitionsIn,
|
|
517
|
+
transitionsOut: totals.transitionsOut,
|
|
518
|
+
dwellMs: totals.dwellMs,
|
|
519
|
+
dwellSamples: totals.dwellSamples,
|
|
520
|
+
daily: [...totals.daily.entries()]
|
|
521
|
+
.sort(([left], [right]) => left - right)
|
|
522
|
+
.map(([bucketStart, pageviews]) => ({ bucketStart, pageviews })),
|
|
523
|
+
}))
|
|
524
|
+
.sort((left, right) => right.pageviews - left.pageviews ||
|
|
525
|
+
left.route.localeCompare(right.route));
|
|
526
|
+
const transitionRows = [...transitions.values()].sort((left, right) => right.count - left.count ||
|
|
527
|
+
left.from.localeCompare(right.from) ||
|
|
528
|
+
left.to.localeCompare(right.to));
|
|
529
|
+
return {
|
|
530
|
+
from: args.from,
|
|
531
|
+
to: args.to,
|
|
532
|
+
window: completeFrom > args.to ? null : { from: windowFrom, to: args.to },
|
|
533
|
+
routes: routeRows,
|
|
534
|
+
transitions: transitionRows,
|
|
535
|
+
truncated: {
|
|
536
|
+
routes: routes.has(SITE_MAP_OTHER_ROUTE),
|
|
537
|
+
transitions: transitionRows.some((transition) => transition.from === SITE_MAP_OTHER_ROUTE ||
|
|
538
|
+
transition.to === SITE_MAP_OTHER_ROUTE),
|
|
539
|
+
},
|
|
540
|
+
metadata: metadata(windowFrom > args.from ? "partial" : "complete"),
|
|
541
|
+
};
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
/**
|
|
545
|
+
* Reads a site's route rollups newest day first until the row budget or the
|
|
546
|
+
* transaction's read headroom runs out, then drops the day it could only
|
|
547
|
+
* read in part so every day it returns is complete.
|
|
548
|
+
*/
|
|
549
|
+
async function readRouteRollups(ctx, siteId, from, to, budget) {
|
|
550
|
+
const rows = [];
|
|
551
|
+
let cutAt = null;
|
|
552
|
+
const query = ctx.db
|
|
553
|
+
.query("routeDailyRollups")
|
|
554
|
+
.withIndex("by_siteId_and_bucketStart", (range) => range
|
|
555
|
+
.eq("siteId", siteId)
|
|
556
|
+
.gte("bucketStart", from)
|
|
557
|
+
.lte("bucketStart", to))
|
|
558
|
+
.order("desc");
|
|
559
|
+
for await (const row of query) {
|
|
560
|
+
if (rows.length >= budget ||
|
|
561
|
+
(rows.length % 256 === 255 && !(await hasFeatureReadHeadroom(ctx)))) {
|
|
562
|
+
cutAt = row.bucketStart;
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
rows.push(row);
|
|
566
|
+
}
|
|
567
|
+
if (cutAt === null)
|
|
568
|
+
return { rows, completeFrom: from };
|
|
569
|
+
while (rows.length > 0 && rows[rows.length - 1].bucketStart === cutAt) {
|
|
570
|
+
rows.pop();
|
|
571
|
+
}
|
|
572
|
+
return { rows, completeFrom: cutAt + DAY_MS };
|
|
573
|
+
}
|
|
340
574
|
export const listSessions = query({
|
|
341
575
|
args: {
|
|
342
576
|
siteId: v.id("sites"),
|
|
@@ -386,7 +620,7 @@ export const listSessions = query({
|
|
|
386
620
|
.order("desc")
|
|
387
621
|
.take(take)));
|
|
388
622
|
}
|
|
389
|
-
const publicRows = rows.map(({ geoLookupAttemptedAt: _attemptedAt, ...row }) => row);
|
|
623
|
+
const publicRows = rows.map(({ geoLookupAttemptedAt: _attemptedAt, lastPageviewAt: _lastPageviewAt, ...row }) => row);
|
|
390
624
|
return keysetPaginationResult(publicRows, pagination, (row) => ({
|
|
391
625
|
timestamp: row.lastSeenAt,
|
|
392
626
|
creationTime: row._creationTime,
|
|
@@ -923,7 +1157,7 @@ export const vitalsReport = query({
|
|
|
923
1157
|
};
|
|
924
1158
|
},
|
|
925
1159
|
});
|
|
926
|
-
const coverageDatasetValidator = v.union(v.literal("overviewHour"), v.literal("overviewDay"), v.literal("events"), v.literal("sessions"), v.literal("conversions"), v.literal("goals"), v.literal("funnels"), v.literal("affiliates"), v.literal("vitals"));
|
|
1160
|
+
const coverageDatasetValidator = v.union(v.literal("overviewHour"), v.literal("overviewDay"), v.literal("events"), v.literal("sessions"), v.literal("conversions"), v.literal("goals"), v.literal("funnels"), v.literal("affiliates"), v.literal("vitals"), v.literal("siteMap"));
|
|
927
1161
|
export const dataCoverage = query({
|
|
928
1162
|
args: {
|
|
929
1163
|
siteId: v.id("sites"),
|
|
@@ -951,6 +1185,7 @@ export const dataCoverage = query({
|
|
|
951
1185
|
"funnels",
|
|
952
1186
|
"affiliates",
|
|
953
1187
|
"vitals",
|
|
1188
|
+
"siteMap",
|
|
954
1189
|
];
|
|
955
1190
|
return {
|
|
956
1191
|
siteId: args.siteId,
|
|
@@ -1046,6 +1281,11 @@ function requireFeatureRollupBudget(entities, days, dataset) {
|
|
|
1046
1281
|
});
|
|
1047
1282
|
}
|
|
1048
1283
|
}
|
|
1284
|
+
async function hasFeatureReadHeadroom(ctx) {
|
|
1285
|
+
const metrics = await ctx.meta.getTransactionMetrics();
|
|
1286
|
+
return (metrics.bytesRead.remaining >= FEATURE_REPORT_BYTE_RESERVE &&
|
|
1287
|
+
metrics.documentsRead.remaining >= FEATURE_REPORT_DOCUMENT_RESERVE);
|
|
1288
|
+
}
|
|
1049
1289
|
async function requireFeatureReadHeadroom(ctx, dataset) {
|
|
1050
1290
|
const metrics = await ctx.meta.getTransactionMetrics();
|
|
1051
1291
|
if (metrics.bytesRead.remaining < FEATURE_REPORT_BYTE_RESERVE ||
|
|
@@ -1135,6 +1375,9 @@ async function assertSourceAvailable(ctx, siteIds, sources, from) {
|
|
|
1135
1375
|
}
|
|
1136
1376
|
}
|
|
1137
1377
|
async function datasetCoverageForRange(ctx, siteId, dataset, from, to) {
|
|
1378
|
+
if (dataset === "siteMap") {
|
|
1379
|
+
return siteMapDatasetCoverage(ctx, siteId, from, to);
|
|
1380
|
+
}
|
|
1138
1381
|
// Vitals have no legacy fallback source; the rollup either covers the range
|
|
1139
1382
|
// or the coverage states say how much of it exists.
|
|
1140
1383
|
if (dataset === "vitals") {
|
|
@@ -1166,6 +1409,35 @@ async function datasetCoverageForRange(ctx, siteId, dataset, from, to) {
|
|
|
1166
1409
|
}))).filter((option) => option !== null);
|
|
1167
1410
|
return options.sort((left, right) => coverageRank(right.state) - coverageRank(left.state))[0];
|
|
1168
1411
|
}
|
|
1412
|
+
/**
|
|
1413
|
+
* The site map's coverage without reading its rows: it starts at the site's
|
|
1414
|
+
* `siteMapSince` and ends where retention cut it. The report itself may still
|
|
1415
|
+
* narrow the window when the range exceeds its row budget.
|
|
1416
|
+
*/
|
|
1417
|
+
async function siteMapDatasetCoverage(ctx, siteId, from, to) {
|
|
1418
|
+
const control = await ctx.db
|
|
1419
|
+
.query("analyticsCoverage")
|
|
1420
|
+
.withIndex("by_siteId", (range) => range.eq("siteId", siteId))
|
|
1421
|
+
.unique();
|
|
1422
|
+
const since = control?.siteMapSince ?? null;
|
|
1423
|
+
const retainedBefore = control?.retained.find((item) => item.dataset === siteMapCoverageDataset)
|
|
1424
|
+
?.before ?? null;
|
|
1425
|
+
const unavailable = since === null ||
|
|
1426
|
+
to < since ||
|
|
1427
|
+
(retainedBefore !== null && to < retainedBefore);
|
|
1428
|
+
const state = unavailable
|
|
1429
|
+
? "unavailable"
|
|
1430
|
+
: from < Math.max(since, retainedBefore ?? 0)
|
|
1431
|
+
? "partial"
|
|
1432
|
+
: "complete";
|
|
1433
|
+
return {
|
|
1434
|
+
state,
|
|
1435
|
+
generation: 1,
|
|
1436
|
+
availableFrom: since,
|
|
1437
|
+
availableThrough: since === null ? null : to,
|
|
1438
|
+
retainedBefore,
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1169
1441
|
async function sourceSetRange(ctx, siteId, sources) {
|
|
1170
1442
|
const ranges = (await Promise.all(sources.map((source) => sourceRange(ctx, siteId, source)))).filter((range) => range !== null);
|
|
1171
1443
|
if (ranges.length === 0)
|