@clipin/convex-wearables 0.0.3 → 0.1.1
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 +395 -0
- package/dist/client/index.d.ts +38 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +34 -3
- package/dist/client/index.js.map +1 -1
- package/dist/client/types.d.ts +83 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/types.js.map +1 -1
- package/dist/component/dataPoints.d.ts +148 -34
- package/dist/component/dataPoints.d.ts.map +1 -1
- package/dist/component/dataPoints.js +1048 -139
- package/dist/component/dataPoints.js.map +1 -1
- package/dist/component/lifecycle.d.ts.map +1 -1
- package/dist/component/lifecycle.js +37 -1
- package/dist/component/lifecycle.js.map +1 -1
- package/dist/component/schema.d.ts +166 -2
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +89 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/component/timeSeriesPolicyUtils.d.ts +97 -0
- package/dist/component/timeSeriesPolicyUtils.d.ts.map +1 -0
- package/dist/component/timeSeriesPolicyUtils.js +163 -0
- package/dist/component/timeSeriesPolicyUtils.js.map +1 -0
- package/dist/test.d.ts +162 -2
- package/dist/test.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/index.test.ts +179 -0
- package/src/client/index.ts +87 -7
- package/src/client/types.ts +99 -0
- package/src/component/dataPoints.test.ts +546 -1
- package/src/component/dataPoints.ts +1526 -155
- package/src/component/lifecycle.ts +42 -1
- package/src/component/schema.ts +106 -0
- package/src/component/timeSeriesPolicyUtils.ts +243 -0
|
@@ -52,6 +52,29 @@ export const deleteAllUserData = mutation({
|
|
|
52
52
|
.withIndex("by_source_type_time", (idx) => idx.eq("dataSourceId", source._id))
|
|
53
53
|
.take(500);
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
let rollups = await ctx.db
|
|
57
|
+
.query("timeSeriesRollups")
|
|
58
|
+
.withIndex("by_source_type_bucket", (idx) => idx.eq("dataSourceId", source._id))
|
|
59
|
+
.take(500);
|
|
60
|
+
while (rollups.length > 0) {
|
|
61
|
+
for (const rollup of rollups) {
|
|
62
|
+
await ctx.db.delete(rollup._id);
|
|
63
|
+
}
|
|
64
|
+
rollups = await ctx.db
|
|
65
|
+
.query("timeSeriesRollups")
|
|
66
|
+
.withIndex("by_source_type_bucket", (idx) => idx.eq("dataSourceId", source._id))
|
|
67
|
+
.take(500);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const seriesStates = await ctx.db
|
|
71
|
+
.query("timeSeriesSeriesState")
|
|
72
|
+
.withIndex("by_source_series", (idx) => idx.eq("dataSourceId", source._id))
|
|
73
|
+
.collect();
|
|
74
|
+
for (const state of seriesStates) {
|
|
75
|
+
await ctx.db.delete(state._id);
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
await ctx.db.delete(source._id);
|
|
56
79
|
}
|
|
57
80
|
|
|
@@ -73,7 +96,25 @@ export const deleteAllUserData = mutation({
|
|
|
73
96
|
await ctx.db.delete(summary._id);
|
|
74
97
|
}
|
|
75
98
|
|
|
76
|
-
// 5. Delete
|
|
99
|
+
// 5. Delete menstrual cycles
|
|
100
|
+
const cycles = await ctx.db
|
|
101
|
+
.query("menstrualCycles")
|
|
102
|
+
.withIndex("by_user_date", (idx) => idx.eq("userId", userId))
|
|
103
|
+
.collect();
|
|
104
|
+
for (const cycle of cycles) {
|
|
105
|
+
await ctx.db.delete(cycle._id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 6. Delete user-level time-series policy assignments
|
|
109
|
+
const policyAssignments = await ctx.db
|
|
110
|
+
.query("timeSeriesPolicyAssignments")
|
|
111
|
+
.withIndex("by_user", (idx) => idx.eq("userId", userId))
|
|
112
|
+
.collect();
|
|
113
|
+
for (const assignment of policyAssignments) {
|
|
114
|
+
await ctx.db.delete(assignment._id);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 7. Delete sync jobs
|
|
77
118
|
const jobs = await ctx.db
|
|
78
119
|
.query("syncJobs")
|
|
79
120
|
.withIndex("by_user", (idx) => idx.eq("userId", userId))
|
package/src/component/schema.ts
CHANGED
|
@@ -53,6 +53,17 @@ export const backfillStatus = v.union(
|
|
|
53
53
|
v.literal("canceled"),
|
|
54
54
|
);
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Supported rollup aggregations.
|
|
58
|
+
*/
|
|
59
|
+
export const timeSeriesAggregation = v.union(
|
|
60
|
+
v.literal("avg"),
|
|
61
|
+
v.literal("min"),
|
|
62
|
+
v.literal("max"),
|
|
63
|
+
v.literal("last"),
|
|
64
|
+
v.literal("count"),
|
|
65
|
+
);
|
|
66
|
+
|
|
56
67
|
// ---------------------------------------------------------------------------
|
|
57
68
|
// Schema
|
|
58
69
|
// ---------------------------------------------------------------------------
|
|
@@ -108,6 +119,28 @@ export default defineSchema({
|
|
|
108
119
|
.index("by_source_type_time", ["dataSourceId", "seriesType", "recordedAt"])
|
|
109
120
|
.index("by_type_time", ["seriesType", "recordedAt"]),
|
|
110
121
|
|
|
122
|
+
// -------------------------------------------------------------------------
|
|
123
|
+
// Time-Series Rollups — bucketed storage for dense historical series
|
|
124
|
+
// -------------------------------------------------------------------------
|
|
125
|
+
timeSeriesRollups: defineTable({
|
|
126
|
+
dataSourceId: v.id("dataSources"),
|
|
127
|
+
seriesType: v.string(),
|
|
128
|
+
bucketMs: v.number(),
|
|
129
|
+
bucketStart: v.number(),
|
|
130
|
+
bucketEnd: v.number(),
|
|
131
|
+
avg: v.number(),
|
|
132
|
+
min: v.number(),
|
|
133
|
+
max: v.number(),
|
|
134
|
+
last: v.number(),
|
|
135
|
+
lastRecordedAt: v.number(),
|
|
136
|
+
count: v.number(),
|
|
137
|
+
updatedAt: v.number(),
|
|
138
|
+
})
|
|
139
|
+
.index("by_source_type_bucket", ["dataSourceId", "seriesType", "bucketStart"])
|
|
140
|
+
.index("by_source_type_bucket_size", ["dataSourceId", "seriesType", "bucketMs", "bucketStart"])
|
|
141
|
+
.index("by_source_bucket", ["dataSourceId", "bucketStart"])
|
|
142
|
+
.index("by_type_bucket", ["seriesType", "bucketStart"]),
|
|
143
|
+
|
|
111
144
|
// -------------------------------------------------------------------------
|
|
112
145
|
// Events — workouts and sleep sessions
|
|
113
146
|
// -------------------------------------------------------------------------
|
|
@@ -268,6 +301,79 @@ export default defineSchema({
|
|
|
268
301
|
updatedAt: v.optional(v.number()),
|
|
269
302
|
}).index("by_provider", ["provider"]),
|
|
270
303
|
|
|
304
|
+
// -------------------------------------------------------------------------
|
|
305
|
+
// Time-Series Policy Rules — default and preset-based storage rules
|
|
306
|
+
// -------------------------------------------------------------------------
|
|
307
|
+
timeSeriesPolicyRules: defineTable({
|
|
308
|
+
policySetKind: v.union(v.literal("default"), v.literal("preset")),
|
|
309
|
+
policySetKey: v.string(), // "__default__" or host-defined preset key
|
|
310
|
+
scopeKey: v.string(),
|
|
311
|
+
provider: v.optional(providerName),
|
|
312
|
+
seriesType: v.optional(v.string()),
|
|
313
|
+
tiers: v.array(
|
|
314
|
+
v.union(
|
|
315
|
+
v.object({
|
|
316
|
+
kind: v.literal("raw"),
|
|
317
|
+
fromAgeMs: v.number(),
|
|
318
|
+
toAgeMs: v.union(v.number(), v.null()),
|
|
319
|
+
}),
|
|
320
|
+
v.object({
|
|
321
|
+
kind: v.literal("rollup"),
|
|
322
|
+
fromAgeMs: v.number(),
|
|
323
|
+
toAgeMs: v.union(v.number(), v.null()),
|
|
324
|
+
bucketMs: v.number(),
|
|
325
|
+
aggregations: v.array(timeSeriesAggregation),
|
|
326
|
+
}),
|
|
327
|
+
),
|
|
328
|
+
),
|
|
329
|
+
updatedAt: v.number(),
|
|
330
|
+
})
|
|
331
|
+
.index("by_set", ["policySetKind", "policySetKey"])
|
|
332
|
+
.index("by_set_scope", ["policySetKind", "policySetKey", "provider", "seriesType"]),
|
|
333
|
+
|
|
334
|
+
// -------------------------------------------------------------------------
|
|
335
|
+
// Time-Series Policy Assignments — per-user preset selection
|
|
336
|
+
// -------------------------------------------------------------------------
|
|
337
|
+
timeSeriesPolicyAssignments: defineTable({
|
|
338
|
+
userId: v.string(),
|
|
339
|
+
presetKey: v.string(),
|
|
340
|
+
updatedAt: v.number(),
|
|
341
|
+
})
|
|
342
|
+
.index("by_user", ["userId"])
|
|
343
|
+
.index("by_preset", ["presetKey"]),
|
|
344
|
+
|
|
345
|
+
// -------------------------------------------------------------------------
|
|
346
|
+
// Time-Series Policy Settings — singleton settings for maintenance
|
|
347
|
+
// -------------------------------------------------------------------------
|
|
348
|
+
timeSeriesPolicySettings: defineTable({
|
|
349
|
+
key: v.string(),
|
|
350
|
+
maintenanceEnabled: v.boolean(),
|
|
351
|
+
maintenanceIntervalMs: v.number(),
|
|
352
|
+
scheduledAt: v.optional(v.number()),
|
|
353
|
+
lastRunAt: v.optional(v.number()),
|
|
354
|
+
lastError: v.optional(v.string()),
|
|
355
|
+
updatedAt: v.number(),
|
|
356
|
+
}).index("by_key", ["key"]),
|
|
357
|
+
|
|
358
|
+
// -------------------------------------------------------------------------
|
|
359
|
+
// Time-Series Series State — per source/series maintenance cursor
|
|
360
|
+
// -------------------------------------------------------------------------
|
|
361
|
+
timeSeriesSeriesState: defineTable({
|
|
362
|
+
dataSourceId: v.id("dataSources"),
|
|
363
|
+
connectionId: v.optional(v.id("connections")),
|
|
364
|
+
userId: v.string(),
|
|
365
|
+
provider: providerName,
|
|
366
|
+
seriesType: v.string(),
|
|
367
|
+
latestRecordedAt: v.number(),
|
|
368
|
+
lastIngestedAt: v.number(),
|
|
369
|
+
nextMaintenanceAt: v.number(),
|
|
370
|
+
lastMaintenanceAt: v.optional(v.number()),
|
|
371
|
+
updatedAt: v.number(),
|
|
372
|
+
})
|
|
373
|
+
.index("by_source_series", ["dataSourceId", "seriesType"])
|
|
374
|
+
.index("by_next_maintenance", ["nextMaintenanceAt"])
|
|
375
|
+
.index("by_user", ["userId"]),
|
|
376
|
+
|
|
271
377
|
// -------------------------------------------------------------------------
|
|
272
378
|
// Provider Priorities — sync order when multiple providers have same data
|
|
273
379
|
// -------------------------------------------------------------------------
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import type { Doc } from "./_generated/dataModel";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_POLICY_SET_KEY = "__default__";
|
|
4
|
+
export const DEFAULT_MAINTENANCE_INTERVAL_MS = 60 * 60 * 1000;
|
|
5
|
+
export const DEFAULT_TIME_SERIES_AGGREGATIONS = ["avg", "min", "max", "last", "count"] as const;
|
|
6
|
+
|
|
7
|
+
export type DurationInput = string | number;
|
|
8
|
+
export type TimeSeriesAggregation = Doc<"timeSeriesPolicyRules">["tiers"][number] extends infer Tier
|
|
9
|
+
? Tier extends { aggregations: infer Aggregations }
|
|
10
|
+
? Aggregations extends Array<infer Aggregation>
|
|
11
|
+
? Aggregation
|
|
12
|
+
: never
|
|
13
|
+
: never
|
|
14
|
+
: never;
|
|
15
|
+
|
|
16
|
+
export type TimeSeriesTierInput =
|
|
17
|
+
| {
|
|
18
|
+
kind: "raw";
|
|
19
|
+
fromAge: DurationInput;
|
|
20
|
+
toAge: DurationInput | null;
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
kind: "rollup";
|
|
24
|
+
fromAge: DurationInput;
|
|
25
|
+
toAge: DurationInput | null;
|
|
26
|
+
bucket: DurationInput;
|
|
27
|
+
aggregations?: TimeSeriesAggregation[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type NormalizedTimeSeriesTier = Doc<"timeSeriesPolicyRules">["tiers"][number];
|
|
31
|
+
|
|
32
|
+
export type TimeSeriesPolicyRuleInput = {
|
|
33
|
+
provider?: Doc<"timeSeriesPolicyRules">["provider"];
|
|
34
|
+
seriesType?: string;
|
|
35
|
+
tiers: TimeSeriesTierInput[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const DURATION_UNITS = {
|
|
39
|
+
ms: 1,
|
|
40
|
+
s: 1000,
|
|
41
|
+
m: 60 * 1000,
|
|
42
|
+
h: 60 * 60 * 1000,
|
|
43
|
+
d: 24 * 60 * 60 * 1000,
|
|
44
|
+
w: 7 * 24 * 60 * 60 * 1000,
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
export function parseDurationInput(input: DurationInput, label: string) {
|
|
48
|
+
if (typeof input === "number") {
|
|
49
|
+
if (!Number.isFinite(input) || input < 0) {
|
|
50
|
+
throw new Error(`${label} must be a non-negative duration`);
|
|
51
|
+
}
|
|
52
|
+
return Math.floor(input);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const trimmed = input.trim().toLowerCase();
|
|
56
|
+
const match = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)$/.exec(trimmed);
|
|
57
|
+
if (!match) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`${label} must be a duration like "30m", "24h", "7d", or a numeric millisecond value`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const value = Number(match[1]);
|
|
64
|
+
const unit = match[2] as keyof typeof DURATION_UNITS;
|
|
65
|
+
return Math.floor(value * DURATION_UNITS[unit]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createTimeSeriesPolicyScopeKey(provider?: string, seriesType?: string) {
|
|
69
|
+
return `${provider ?? "*"}::${seriesType ?? "*"}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function inferTimeSeriesPolicyScope(provider?: string, seriesType?: string) {
|
|
73
|
+
if (provider && seriesType) {
|
|
74
|
+
return "provider_series" as const;
|
|
75
|
+
}
|
|
76
|
+
if (seriesType) {
|
|
77
|
+
return "series" as const;
|
|
78
|
+
}
|
|
79
|
+
if (provider) {
|
|
80
|
+
return "provider" as const;
|
|
81
|
+
}
|
|
82
|
+
return "global" as const;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function normalizeAggregations(aggregations?: readonly TimeSeriesAggregation[]) {
|
|
86
|
+
const unique = new Set(aggregations ?? (DEFAULT_TIME_SERIES_AGGREGATIONS as readonly string[]));
|
|
87
|
+
if (unique.size === 0) {
|
|
88
|
+
throw new Error("Rollup tiers must include at least one aggregation");
|
|
89
|
+
}
|
|
90
|
+
return Array.from(unique) as TimeSeriesAggregation[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function normalizeTimeSeriesTierInputs(tiers: TimeSeriesTierInput[]) {
|
|
94
|
+
if (tiers.length === 0) {
|
|
95
|
+
throw new Error("A time-series policy rule must include at least one tier");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let rawTierCount = 0;
|
|
99
|
+
const normalized = tiers.map((tier, index) => {
|
|
100
|
+
const fromAgeMs = parseDurationInput(tier.fromAge, `tiers[${index}].fromAge`);
|
|
101
|
+
const toAgeMs =
|
|
102
|
+
tier.toAge === null ? null : parseDurationInput(tier.toAge, `tiers[${index}].toAge`);
|
|
103
|
+
|
|
104
|
+
if (toAgeMs !== null && toAgeMs <= fromAgeMs) {
|
|
105
|
+
throw new Error(`tiers[${index}] must have toAge greater than fromAge`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (tier.kind === "raw") {
|
|
109
|
+
rawTierCount += 1;
|
|
110
|
+
return {
|
|
111
|
+
kind: "raw" as const,
|
|
112
|
+
fromAgeMs,
|
|
113
|
+
toAgeMs,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const bucketMs = parseDurationInput(tier.bucket, `tiers[${index}].bucket`);
|
|
118
|
+
if (bucketMs <= 0 || bucketMs % (60 * 1000) !== 0) {
|
|
119
|
+
throw new Error(`tiers[${index}].bucket must be a positive whole-minute duration`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
kind: "rollup" as const,
|
|
124
|
+
fromAgeMs,
|
|
125
|
+
toAgeMs,
|
|
126
|
+
bucketMs,
|
|
127
|
+
aggregations: normalizeAggregations(tier.aggregations),
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (normalized[0].fromAgeMs !== 0) {
|
|
132
|
+
throw new Error("The first tier must start at age 0");
|
|
133
|
+
}
|
|
134
|
+
if (rawTierCount > 1) {
|
|
135
|
+
throw new Error("Only one raw tier is supported in a single policy rule");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (let index = 0; index < normalized.length - 1; index += 1) {
|
|
139
|
+
const current = normalized[index];
|
|
140
|
+
const next = normalized[index + 1];
|
|
141
|
+
|
|
142
|
+
if (current.toAgeMs === null) {
|
|
143
|
+
throw new Error("Open-ended tiers must be the final tier");
|
|
144
|
+
}
|
|
145
|
+
if (next.fromAgeMs !== current.toAgeMs) {
|
|
146
|
+
throw new Error("Policy tiers must be contiguous without gaps or overlap");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return normalized;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function normalizeTimeSeriesPolicyRuleInputs(rules: TimeSeriesPolicyRuleInput[]) {
|
|
154
|
+
const seenScopes = new Set<string>();
|
|
155
|
+
|
|
156
|
+
return rules.map((rule, index) => {
|
|
157
|
+
const scopeKey = createTimeSeriesPolicyScopeKey(rule.provider, rule.seriesType);
|
|
158
|
+
if (seenScopes.has(scopeKey)) {
|
|
159
|
+
throw new Error(`Duplicate time-series policy rule scope "${scopeKey}" at index ${index}`);
|
|
160
|
+
}
|
|
161
|
+
seenScopes.add(scopeKey);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
provider: rule.provider,
|
|
165
|
+
seriesType: rule.seriesType,
|
|
166
|
+
tiers: normalizeTimeSeriesTierInputs(rule.tiers),
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function findTierForAge(tiers: NormalizedTimeSeriesTier[], ageMs: number) {
|
|
172
|
+
return (
|
|
173
|
+
tiers.find(
|
|
174
|
+
(tier) => ageMs >= tier.fromAgeMs && (tier.toAgeMs === null || ageMs < tier.toAgeMs),
|
|
175
|
+
) ?? null
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function getRawTier(tiers: NormalizedTimeSeriesTier[]) {
|
|
180
|
+
return tiers.find((tier) => tier.kind === "raw") ?? null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function getRollupTiers(tiers: NormalizedTimeSeriesTier[]) {
|
|
184
|
+
return tiers.filter((tier) => tier.kind === "rollup");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function buildBuiltinFullTiers(): NormalizedTimeSeriesTier[] {
|
|
188
|
+
return [
|
|
189
|
+
{
|
|
190
|
+
kind: "raw",
|
|
191
|
+
fromAgeMs: 0,
|
|
192
|
+
toAgeMs: null,
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function resolveScopedPolicyRule<
|
|
198
|
+
Rule extends {
|
|
199
|
+
provider?: string;
|
|
200
|
+
seriesType?: string;
|
|
201
|
+
},
|
|
202
|
+
>(rules: Rule[], provider: string, seriesType: string) {
|
|
203
|
+
return (
|
|
204
|
+
rules.find((rule) => rule.provider === provider && rule.seriesType === seriesType) ??
|
|
205
|
+
rules.find((rule) => rule.provider === undefined && rule.seriesType === seriesType) ??
|
|
206
|
+
rules.find((rule) => rule.provider === provider && rule.seriesType === undefined) ??
|
|
207
|
+
rules.find((rule) => rule.provider === undefined && rule.seriesType === undefined) ??
|
|
208
|
+
null
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function comparePolicyScopes(
|
|
213
|
+
a: { provider?: string; seriesType?: string },
|
|
214
|
+
b: { provider?: string; seriesType?: string },
|
|
215
|
+
) {
|
|
216
|
+
const weight = (item: { provider?: string; seriesType?: string }) => {
|
|
217
|
+
const scope = inferTimeSeriesPolicyScope(item.provider, item.seriesType);
|
|
218
|
+
switch (scope) {
|
|
219
|
+
case "global":
|
|
220
|
+
return 0;
|
|
221
|
+
case "provider":
|
|
222
|
+
return 1;
|
|
223
|
+
case "series":
|
|
224
|
+
return 2;
|
|
225
|
+
case "provider_series":
|
|
226
|
+
return 3;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
weight(a) - weight(b) ||
|
|
232
|
+
(a.provider ?? "").localeCompare(b.provider ?? "") ||
|
|
233
|
+
(a.seriesType ?? "").localeCompare(b.seriesType ?? "")
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function getBucketStart(recordedAt: number, bucketMs: number) {
|
|
238
|
+
return Math.floor(recordedAt / bucketMs) * bucketMs;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function getBucketEnd(bucketStart: number, bucketMs: number) {
|
|
242
|
+
return bucketStart + bucketMs - 1;
|
|
243
|
+
}
|