@mongrov/analytics 0.1.0-alpha.0 → 0.1.0-alpha.7
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 +456 -0
- package/dist/core/context.d.ts +24 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +30 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/engine.d.ts +56 -0
- package/dist/core/engine.d.ts.map +1 -0
- package/dist/core/engine.js +119 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js.map +1 -1
- package/dist/core/extensions.d.ts +33 -0
- package/dist/core/extensions.d.ts.map +1 -0
- package/dist/core/extensions.js +58 -0
- package/dist/core/extensions.js.map +1 -0
- package/dist/core/factory.d.ts +22 -5
- package/dist/core/factory.d.ts.map +1 -1
- package/dist/core/factory.js +231 -6
- package/dist/core/factory.js.map +1 -1
- package/dist/core/hooks.d.ts +32 -13
- package/dist/core/hooks.d.ts.map +1 -1
- package/dist/core/hooks.js +137 -15
- package/dist/core/hooks.js.map +1 -1
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/logger.d.ts +25 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +33 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/machine.d.ts +174 -0
- package/dist/core/machine.d.ts.map +1 -0
- package/dist/core/machine.js +228 -0
- package/dist/core/machine.js.map +1 -0
- package/dist/core/metric_metadata.d.ts +128 -0
- package/dist/core/metric_metadata.d.ts.map +1 -0
- package/dist/core/metric_metadata.js +47 -0
- package/dist/core/metric_metadata.js.map +1 -0
- package/dist/core/migrations.d.ts +52 -0
- package/dist/core/migrations.d.ts.map +1 -0
- package/dist/core/migrations.js +66 -0
- package/dist/core/migrations.js.map +1 -0
- package/dist/core/persistence.d.ts +50 -0
- package/dist/core/persistence.d.ts.map +1 -0
- package/dist/core/persistence.js +80 -0
- package/dist/core/persistence.js.map +1 -0
- package/dist/core/retention.d.ts +90 -0
- package/dist/core/retention.d.ts.map +1 -0
- package/dist/core/retention.js +119 -0
- package/dist/core/retention.js.map +1 -0
- package/dist/core/schemas.d.ts +35 -0
- package/dist/core/schemas.d.ts.map +1 -0
- package/dist/core/schemas.js +209 -0
- package/dist/core/schemas.js.map +1 -0
- package/dist/core/types.d.ts +10 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/warehouse.d.ts +58 -0
- package/dist/core/warehouse.d.ts.map +1 -0
- package/dist/core/warehouse.js +116 -0
- package/dist/core/warehouse.js.map +1 -0
- package/dist/sync/buffer.d.ts +51 -0
- package/dist/sync/buffer.d.ts.map +1 -0
- package/dist/sync/buffer.js +188 -0
- package/dist/sync/buffer.js.map +1 -0
- package/dist/sync/context.d.ts +17 -0
- package/dist/sync/context.d.ts.map +1 -0
- package/dist/sync/context.js +23 -0
- package/dist/sync/context.js.map +1 -0
- package/dist/sync/errors.d.ts +19 -0
- package/dist/sync/errors.d.ts.map +1 -0
- package/dist/sync/errors.js +21 -0
- package/dist/sync/errors.js.map +1 -0
- package/dist/sync/events.d.ts +49 -0
- package/dist/sync/events.d.ts.map +1 -0
- package/dist/sync/events.js +46 -0
- package/dist/sync/events.js.map +1 -0
- package/dist/sync/factory.d.ts +57 -0
- package/dist/sync/factory.d.ts.map +1 -0
- package/dist/sync/factory.js +249 -0
- package/dist/sync/factory.js.map +1 -0
- package/dist/sync/fetcher.d.ts +74 -0
- package/dist/sync/fetcher.d.ts.map +1 -0
- package/dist/sync/fetcher.js +166 -0
- package/dist/sync/fetcher.js.map +1 -0
- package/dist/sync/flusher.d.ts +93 -0
- package/dist/sync/flusher.d.ts.map +1 -0
- package/dist/sync/flusher.js +234 -0
- package/dist/sync/flusher.js.map +1 -0
- package/dist/sync/hooks.d.ts +17 -0
- package/dist/sync/hooks.d.ts.map +1 -0
- package/dist/sync/hooks.js +48 -0
- package/dist/sync/hooks.js.map +1 -0
- package/dist/sync/index.d.ts +32 -1
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +36 -1
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/manager.d.ts +48 -0
- package/dist/sync/manager.d.ts.map +1 -0
- package/dist/sync/manager.js +9 -0
- package/dist/sync/manager.js.map +1 -0
- package/dist/sync/mapper/activity.d.ts +31 -0
- package/dist/sync/mapper/activity.d.ts.map +1 -0
- package/dist/sync/mapper/activity.js +56 -0
- package/dist/sync/mapper/activity.js.map +1 -0
- package/dist/sync/mapper/battery.d.ts +12 -0
- package/dist/sync/mapper/battery.d.ts.map +1 -0
- package/dist/sync/mapper/battery.js +22 -0
- package/dist/sync/mapper/battery.js.map +1 -0
- package/dist/sync/mapper/firmware.d.ts +28 -0
- package/dist/sync/mapper/firmware.d.ts.map +1 -0
- package/dist/sync/mapper/firmware.js +43 -0
- package/dist/sync/mapper/firmware.js.map +1 -0
- package/dist/sync/mapper/heart-rate.d.ts +10 -0
- package/dist/sync/mapper/heart-rate.d.ts.map +1 -0
- package/dist/sync/mapper/heart-rate.js +19 -0
- package/dist/sync/mapper/heart-rate.js.map +1 -0
- package/dist/sync/mapper/hrv.d.ts +22 -0
- package/dist/sync/mapper/hrv.d.ts.map +1 -0
- package/dist/sync/mapper/hrv.js +56 -0
- package/dist/sync/mapper/hrv.js.map +1 -0
- package/dist/sync/mapper/ring-config.d.ts +33 -0
- package/dist/sync/mapper/ring-config.d.ts.map +1 -0
- package/dist/sync/mapper/ring-config.js +48 -0
- package/dist/sync/mapper/ring-config.js.map +1 -0
- package/dist/sync/mapper/sleep.d.ts +32 -0
- package/dist/sync/mapper/sleep.d.ts.map +1 -0
- package/dist/sync/mapper/sleep.js +103 -0
- package/dist/sync/mapper/sleep.js.map +1 -0
- package/dist/sync/mapper/spo2.d.ts +9 -0
- package/dist/sync/mapper/spo2.d.ts.map +1 -0
- package/dist/sync/mapper/spo2.js +18 -0
- package/dist/sync/mapper/spo2.js.map +1 -0
- package/dist/sync/mapper/temperature.d.ts +10 -0
- package/dist/sync/mapper/temperature.d.ts.map +1 -0
- package/dist/sync/mapper/temperature.js +19 -0
- package/dist/sync/mapper/temperature.js.map +1 -0
- package/dist/sync/mapper/time.d.ts +49 -0
- package/dist/sync/mapper/time.d.ts.map +1 -0
- package/dist/sync/mapper/time.js +107 -0
- package/dist/sync/mapper/time.js.map +1 -0
- package/dist/sync/mapper/types.d.ts +152 -0
- package/dist/sync/mapper/types.d.ts.map +1 -0
- package/dist/sync/mapper/types.js +15 -0
- package/dist/sync/mapper/types.js.map +1 -0
- package/dist/sync/overflow.d.ts +30 -0
- package/dist/sync/overflow.d.ts.map +1 -0
- package/dist/sync/overflow.js +114 -0
- package/dist/sync/overflow.js.map +1 -0
- package/dist/sync/pusher.d.ts +59 -0
- package/dist/sync/pusher.d.ts.map +1 -0
- package/dist/sync/pusher.js +115 -0
- package/dist/sync/pusher.js.map +1 -0
- package/dist/sync/scheduler.d.ts +84 -0
- package/dist/sync/scheduler.d.ts.map +1 -0
- package/dist/sync/scheduler.js +132 -0
- package/dist/sync/scheduler.js.map +1 -0
- package/dist/sync/triggers.d.ts +64 -0
- package/dist/sync/triggers.d.ts.map +1 -0
- package/dist/sync/triggers.js +113 -0
- package/dist/sync/triggers.js.map +1 -0
- package/dist/sync/types.d.ts +50 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +8 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/sync/watermark.d.ts +32 -0
- package/dist/sync/watermark.d.ts.map +1 -0
- package/dist/sync/watermark.js +61 -0
- package/dist/sync/watermark.js.map +1 -0
- package/package.json +27 -16
- package/LICENSE +0 -21
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XState v5 state machine that drives the analytics engine lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* `idle → opening → ready → attaching → attached → detaching → error`
|
|
5
|
+
*
|
|
6
|
+
* The machine owns lifecycle transitions only; side effects live in injected
|
|
7
|
+
* actors (`openEngine`, `attachEngine`, `detachEngine`, `refreshToken`) so
|
|
8
|
+
* tests can substitute fakes without a real DuckDB.
|
|
9
|
+
*
|
|
10
|
+
* Token refresh runs silently on `attached` via a delayed self-transition
|
|
11
|
+
* to `refreshing`, computed as 75% of the remaining TTL (spec §Token
|
|
12
|
+
* refresh policy). Refresh success re-enters `attached` and re-arms the
|
|
13
|
+
* timer; failure lands in `error`.
|
|
14
|
+
*
|
|
15
|
+
* Consumers (T-10 factory + T-11 hook) drive the machine via `send` and
|
|
16
|
+
* observe via `subscribe`.
|
|
17
|
+
*/
|
|
18
|
+
import { assign, fromPromise, setup } from 'xstate';
|
|
19
|
+
import { AnalyticsError } from './errors';
|
|
20
|
+
// -------------------- token TTL math --------------------
|
|
21
|
+
/**
|
|
22
|
+
* Compute the delay before firing `TOKEN_REFRESH_TICK`. Spec calls for 75%
|
|
23
|
+
* TTL. Falls back to a 30-minute default when the vendor's `expiresAt` is
|
|
24
|
+
* missing or already in the past — the refresh will fail loudly rather than
|
|
25
|
+
* silently skipping.
|
|
26
|
+
*/
|
|
27
|
+
export function computeRefreshDelay(now, expiresAt) {
|
|
28
|
+
if (typeof expiresAt !== 'number' || expiresAt <= now) {
|
|
29
|
+
return 30 * 60 * 1000;
|
|
30
|
+
}
|
|
31
|
+
const ttl = expiresAt - now;
|
|
32
|
+
return Math.max(Math.floor(ttl * 0.75), 1);
|
|
33
|
+
}
|
|
34
|
+
// -------------------- error normalisation --------------------
|
|
35
|
+
function toAnalyticsError(cause, fallbackCode) {
|
|
36
|
+
if (cause instanceof AnalyticsError) {
|
|
37
|
+
return cause;
|
|
38
|
+
}
|
|
39
|
+
return new AnalyticsError(fallbackCode, `machine actor failed: ${String(cause)}`, cause);
|
|
40
|
+
}
|
|
41
|
+
// -------------------- machine --------------------
|
|
42
|
+
export const analyticsMachine = setup({
|
|
43
|
+
types: {
|
|
44
|
+
context: {},
|
|
45
|
+
events: {},
|
|
46
|
+
input: {},
|
|
47
|
+
},
|
|
48
|
+
actors: {
|
|
49
|
+
openEngine: fromPromise(async ({ input }) => {
|
|
50
|
+
await input.deps.openEngine();
|
|
51
|
+
}),
|
|
52
|
+
attachEngine: fromPromise(async ({ input }) => input.deps.attachEngine({ ctx: input.ctx })),
|
|
53
|
+
detachEngine: fromPromise(async ({ input }) => {
|
|
54
|
+
await input.deps.detachEngine({ ctx: input.ctx });
|
|
55
|
+
}),
|
|
56
|
+
refreshToken: fromPromise(async ({ input }) => input.deps.refreshToken({ ctx: input.ctx })),
|
|
57
|
+
},
|
|
58
|
+
actions: {
|
|
59
|
+
assignAttachCtx: assign(({ event }) => {
|
|
60
|
+
if (event.type !== 'ATTACH')
|
|
61
|
+
return {};
|
|
62
|
+
return { ctx: event.ctx };
|
|
63
|
+
}),
|
|
64
|
+
assignAttachSuccess: assign((_, params) => ({
|
|
65
|
+
warehouseSecret: params.output.warehouseSecret,
|
|
66
|
+
tokenExpiresAt: params.output.tokenExpiresAt,
|
|
67
|
+
lastError: undefined,
|
|
68
|
+
})),
|
|
69
|
+
assignRefreshSuccess: assign((_, params) => ({
|
|
70
|
+
tokenExpiresAt: params.output.tokenExpiresAt,
|
|
71
|
+
lastError: undefined,
|
|
72
|
+
})),
|
|
73
|
+
assignOpenError: assign((_, params) => ({
|
|
74
|
+
lastError: toAnalyticsError(params.error, 'engine_open_failed'),
|
|
75
|
+
})),
|
|
76
|
+
assignAttachError: assign((_, params) => ({
|
|
77
|
+
lastError: toAnalyticsError(params.error, 'attach_failed'),
|
|
78
|
+
})),
|
|
79
|
+
assignRefreshError: assign((_, params) => ({
|
|
80
|
+
lastError: toAnalyticsError(params.error, 'token_vendor_failed'),
|
|
81
|
+
})),
|
|
82
|
+
clearError: assign({ lastError: undefined }),
|
|
83
|
+
clearAttachState: assign({
|
|
84
|
+
ctx: undefined,
|
|
85
|
+
tokenExpiresAt: undefined,
|
|
86
|
+
warehouseSecret: undefined,
|
|
87
|
+
}),
|
|
88
|
+
},
|
|
89
|
+
guards: {
|
|
90
|
+
hasAttachCtx: ({ context }) => context.ctx !== undefined,
|
|
91
|
+
},
|
|
92
|
+
delays: {
|
|
93
|
+
tokenRefresh: ({ context }) => computeRefreshDelay(Date.now(), context.tokenExpiresAt),
|
|
94
|
+
},
|
|
95
|
+
}).createMachine({
|
|
96
|
+
id: 'analytics',
|
|
97
|
+
initial: 'idle',
|
|
98
|
+
context: ({ input }) => ({
|
|
99
|
+
deps: input.deps,
|
|
100
|
+
ctx: undefined,
|
|
101
|
+
tokenExpiresAt: undefined,
|
|
102
|
+
warehouseSecret: undefined,
|
|
103
|
+
lastError: undefined,
|
|
104
|
+
}),
|
|
105
|
+
on: {
|
|
106
|
+
CLOSE: {
|
|
107
|
+
target: '.idle',
|
|
108
|
+
actions: ['clearAttachState', 'clearError'],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
states: {
|
|
112
|
+
idle: {
|
|
113
|
+
on: {
|
|
114
|
+
OPEN: 'opening',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
opening: {
|
|
118
|
+
invoke: {
|
|
119
|
+
src: 'openEngine',
|
|
120
|
+
input: ({ context }) => ({ deps: context.deps }),
|
|
121
|
+
onDone: 'ready',
|
|
122
|
+
onError: {
|
|
123
|
+
target: 'error',
|
|
124
|
+
actions: {
|
|
125
|
+
type: 'assignOpenError',
|
|
126
|
+
params: ({ event }) => ({ error: event.error }),
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
ready: {
|
|
132
|
+
on: {
|
|
133
|
+
ATTACH: {
|
|
134
|
+
target: 'attaching',
|
|
135
|
+
actions: 'assignAttachCtx',
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
attaching: {
|
|
140
|
+
invoke: {
|
|
141
|
+
src: 'attachEngine',
|
|
142
|
+
input: ({ context }) => ({ deps: context.deps, ctx: context.ctx }),
|
|
143
|
+
onDone: {
|
|
144
|
+
target: 'attached',
|
|
145
|
+
actions: {
|
|
146
|
+
type: 'assignAttachSuccess',
|
|
147
|
+
params: ({ event }) => ({ output: event.output }),
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
onError: {
|
|
151
|
+
target: 'error',
|
|
152
|
+
actions: {
|
|
153
|
+
type: 'assignAttachError',
|
|
154
|
+
params: ({ event }) => ({ error: event.error }),
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
attached: {
|
|
160
|
+
after: {
|
|
161
|
+
tokenRefresh: {
|
|
162
|
+
target: 'refreshing',
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
on: {
|
|
166
|
+
TOKEN_REFRESH_TICK: 'refreshing',
|
|
167
|
+
DETACH: 'detaching',
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
refreshing: {
|
|
171
|
+
invoke: {
|
|
172
|
+
src: 'refreshToken',
|
|
173
|
+
input: ({ context }) => ({ deps: context.deps, ctx: context.ctx }),
|
|
174
|
+
onDone: {
|
|
175
|
+
target: 'attached',
|
|
176
|
+
actions: {
|
|
177
|
+
type: 'assignRefreshSuccess',
|
|
178
|
+
params: ({ event }) => ({ output: event.output }),
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
onError: {
|
|
182
|
+
target: 'error',
|
|
183
|
+
actions: {
|
|
184
|
+
type: 'assignRefreshError',
|
|
185
|
+
params: ({ event }) => ({ error: event.error }),
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
detaching: {
|
|
191
|
+
invoke: {
|
|
192
|
+
src: 'detachEngine',
|
|
193
|
+
input: ({ context }) => ({ deps: context.deps, ctx: context.ctx }),
|
|
194
|
+
onDone: {
|
|
195
|
+
target: 'ready',
|
|
196
|
+
actions: 'clearAttachState',
|
|
197
|
+
},
|
|
198
|
+
onError: {
|
|
199
|
+
target: 'error',
|
|
200
|
+
actions: {
|
|
201
|
+
type: 'assignAttachError',
|
|
202
|
+
params: ({ event }) => ({ error: event.error }),
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
error: {
|
|
208
|
+
on: {
|
|
209
|
+
OPEN: {
|
|
210
|
+
target: 'opening',
|
|
211
|
+
actions: 'clearError',
|
|
212
|
+
},
|
|
213
|
+
ATTACH: [
|
|
214
|
+
{
|
|
215
|
+
guard: 'hasAttachCtx',
|
|
216
|
+
target: 'attaching',
|
|
217
|
+
actions: ['assignAttachCtx', 'clearError'],
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
target: 'attaching',
|
|
221
|
+
actions: ['assignAttachCtx', 'clearError'],
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
//# sourceMappingURL=machine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine.js","sourceRoot":"","sources":["../../src/core/machine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metric registry (spec §Metric metadata).
|
|
3
|
+
*
|
|
4
|
+
* `METRIC_METADATA` maps every consumer-facing metric id to the warehouse
|
|
5
|
+
* column that stores it, the sampling cadence, and the exposure tier
|
|
6
|
+
* (`full` vs `collected_only`). Rules engine + UI decide which metrics to
|
|
7
|
+
* surface based on `exposure`; retention logic (T-14) uses the same table
|
|
8
|
+
* mapping to sweep by column.
|
|
9
|
+
*/
|
|
10
|
+
import type { TableName } from './schemas';
|
|
11
|
+
export type MetricExposure = 'full' | 'collected_only';
|
|
12
|
+
/**
|
|
13
|
+
* Sampling cadence in minutes, or the sentinel `'per_session'` for metrics
|
|
14
|
+
* emitted once per sleep session. Keeps the discriminant explicit rather
|
|
15
|
+
* than shoehorning it into a number.
|
|
16
|
+
*/
|
|
17
|
+
export type MetricSamplingMinutes = number | 'per_session';
|
|
18
|
+
export interface MetricMetadataEntry {
|
|
19
|
+
table: TableName;
|
|
20
|
+
column: string;
|
|
21
|
+
sampling_minutes: MetricSamplingMinutes;
|
|
22
|
+
exposure: MetricExposure;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Frozen metric registry, verbatim from spec §Metric metadata.
|
|
26
|
+
*
|
|
27
|
+
* v0.1.0 apps consume this for exposure gating; adding metrics is a spec
|
|
28
|
+
* change (bump metadata; add DDL column if new).
|
|
29
|
+
*/
|
|
30
|
+
export declare const METRIC_METADATA: Readonly<{
|
|
31
|
+
readonly hrv_ms: {
|
|
32
|
+
readonly table: "hrv";
|
|
33
|
+
readonly column: "hrv_ms";
|
|
34
|
+
readonly sampling_minutes: 60;
|
|
35
|
+
readonly exposure: "full";
|
|
36
|
+
};
|
|
37
|
+
readonly stress: {
|
|
38
|
+
readonly table: "hrv";
|
|
39
|
+
readonly column: "stress";
|
|
40
|
+
readonly sampling_minutes: 60;
|
|
41
|
+
readonly exposure: "full";
|
|
42
|
+
};
|
|
43
|
+
readonly systolic_bp: {
|
|
44
|
+
readonly table: "hrv";
|
|
45
|
+
readonly column: "systolic_bp";
|
|
46
|
+
readonly sampling_minutes: 60;
|
|
47
|
+
readonly exposure: "collected_only";
|
|
48
|
+
};
|
|
49
|
+
readonly diastolic_bp: {
|
|
50
|
+
readonly table: "hrv";
|
|
51
|
+
readonly column: "diastolic_bp";
|
|
52
|
+
readonly sampling_minutes: 60;
|
|
53
|
+
readonly exposure: "collected_only";
|
|
54
|
+
};
|
|
55
|
+
readonly vascular_aging: {
|
|
56
|
+
readonly table: "hrv";
|
|
57
|
+
readonly column: "vascular_aging";
|
|
58
|
+
readonly sampling_minutes: 60;
|
|
59
|
+
readonly exposure: "collected_only";
|
|
60
|
+
};
|
|
61
|
+
readonly hr_bpm: {
|
|
62
|
+
readonly table: "heart_rate";
|
|
63
|
+
readonly column: "bpm";
|
|
64
|
+
readonly sampling_minutes: 10;
|
|
65
|
+
readonly exposure: "full";
|
|
66
|
+
};
|
|
67
|
+
readonly spo2: {
|
|
68
|
+
readonly table: "spo2";
|
|
69
|
+
readonly column: "spo2";
|
|
70
|
+
readonly sampling_minutes: 30;
|
|
71
|
+
readonly exposure: "full";
|
|
72
|
+
};
|
|
73
|
+
readonly temp_c: {
|
|
74
|
+
readonly table: "temperature";
|
|
75
|
+
readonly column: "temp_c";
|
|
76
|
+
readonly sampling_minutes: 30;
|
|
77
|
+
readonly exposure: "full";
|
|
78
|
+
};
|
|
79
|
+
readonly activity_steps: {
|
|
80
|
+
readonly table: "activity";
|
|
81
|
+
readonly column: "steps";
|
|
82
|
+
readonly sampling_minutes: 1;
|
|
83
|
+
readonly exposure: "full";
|
|
84
|
+
};
|
|
85
|
+
readonly calories: {
|
|
86
|
+
readonly table: "activity_bucket";
|
|
87
|
+
readonly column: "calories";
|
|
88
|
+
readonly sampling_minutes: 10;
|
|
89
|
+
readonly exposure: "full";
|
|
90
|
+
};
|
|
91
|
+
readonly distance_km: {
|
|
92
|
+
readonly table: "activity_bucket";
|
|
93
|
+
readonly column: "distance_km";
|
|
94
|
+
readonly sampling_minutes: 10;
|
|
95
|
+
readonly exposure: "full";
|
|
96
|
+
};
|
|
97
|
+
readonly sleep_total_minutes: {
|
|
98
|
+
readonly table: "sleep_session";
|
|
99
|
+
readonly column: "total_minutes";
|
|
100
|
+
readonly sampling_minutes: "per_session";
|
|
101
|
+
readonly exposure: "full";
|
|
102
|
+
};
|
|
103
|
+
readonly sleep_score: {
|
|
104
|
+
readonly table: "sleep_session";
|
|
105
|
+
readonly column: "avg_confidence";
|
|
106
|
+
readonly sampling_minutes: "per_session";
|
|
107
|
+
readonly exposure: "full";
|
|
108
|
+
};
|
|
109
|
+
readonly device_battery: {
|
|
110
|
+
readonly table: "device_event";
|
|
111
|
+
readonly column: "payload";
|
|
112
|
+
readonly sampling_minutes: 240;
|
|
113
|
+
readonly exposure: "full";
|
|
114
|
+
};
|
|
115
|
+
}>;
|
|
116
|
+
export type MetricId = keyof typeof METRIC_METADATA;
|
|
117
|
+
/**
|
|
118
|
+
* Whether an app should surface this metric to end users. Metrics marked
|
|
119
|
+
* `collected_only` (BP, vascular aging) are captured for later research but
|
|
120
|
+
* kept out of consumer UI in v0.1.0.
|
|
121
|
+
*/
|
|
122
|
+
export declare function isMetricExposed(id: MetricId): boolean;
|
|
123
|
+
/**
|
|
124
|
+
* List of every metric id where `exposure === 'full'` — convenience for
|
|
125
|
+
* UI code that iterates the visible surface.
|
|
126
|
+
*/
|
|
127
|
+
export declare function getExposedMetricIds(): MetricId[];
|
|
128
|
+
//# sourceMappingURL=metric_metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metric_metadata.d.ts","sourceRoot":"","sources":["../../src/core/metric_metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAE1C,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,gBAAgB,CAAA;AAEtD;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,aAAa,CAAA;AAE1D,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,SAAS,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,gBAAgB,EAAE,qBAAqB,CAAA;IACvC,QAAQ,EAAE,cAAc,CAAA;CACzB;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAeuC,CAAA;AAEnE,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,eAAe,CAAA;AAEnD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,CAErD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,QAAQ,EAAE,CAEhD"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metric registry (spec §Metric metadata).
|
|
3
|
+
*
|
|
4
|
+
* `METRIC_METADATA` maps every consumer-facing metric id to the warehouse
|
|
5
|
+
* column that stores it, the sampling cadence, and the exposure tier
|
|
6
|
+
* (`full` vs `collected_only`). Rules engine + UI decide which metrics to
|
|
7
|
+
* surface based on `exposure`; retention logic (T-14) uses the same table
|
|
8
|
+
* mapping to sweep by column.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Frozen metric registry, verbatim from spec §Metric metadata.
|
|
12
|
+
*
|
|
13
|
+
* v0.1.0 apps consume this for exposure gating; adding metrics is a spec
|
|
14
|
+
* change (bump metadata; add DDL column if new).
|
|
15
|
+
*/
|
|
16
|
+
export const METRIC_METADATA = Object.freeze({
|
|
17
|
+
hrv_ms: { table: 'hrv', column: 'hrv_ms', sampling_minutes: 60, exposure: 'full' },
|
|
18
|
+
stress: { table: 'hrv', column: 'stress', sampling_minutes: 60, exposure: 'full' },
|
|
19
|
+
systolic_bp: { table: 'hrv', column: 'systolic_bp', sampling_minutes: 60, exposure: 'collected_only' },
|
|
20
|
+
diastolic_bp: { table: 'hrv', column: 'diastolic_bp', sampling_minutes: 60, exposure: 'collected_only' },
|
|
21
|
+
vascular_aging: { table: 'hrv', column: 'vascular_aging', sampling_minutes: 60, exposure: 'collected_only' },
|
|
22
|
+
hr_bpm: { table: 'heart_rate', column: 'bpm', sampling_minutes: 10, exposure: 'full' },
|
|
23
|
+
spo2: { table: 'spo2', column: 'spo2', sampling_minutes: 30, exposure: 'full' },
|
|
24
|
+
temp_c: { table: 'temperature', column: 'temp_c', sampling_minutes: 30, exposure: 'full' },
|
|
25
|
+
activity_steps: { table: 'activity', column: 'steps', sampling_minutes: 1, exposure: 'full' },
|
|
26
|
+
calories: { table: 'activity_bucket', column: 'calories', sampling_minutes: 10, exposure: 'full' },
|
|
27
|
+
distance_km: { table: 'activity_bucket', column: 'distance_km', sampling_minutes: 10, exposure: 'full' },
|
|
28
|
+
sleep_total_minutes: { table: 'sleep_session', column: 'total_minutes', sampling_minutes: 'per_session', exposure: 'full' },
|
|
29
|
+
sleep_score: { table: 'sleep_session', column: 'avg_confidence', sampling_minutes: 'per_session', exposure: 'full' },
|
|
30
|
+
device_battery: { table: 'device_event', column: 'payload', sampling_minutes: 240, exposure: 'full' },
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Whether an app should surface this metric to end users. Metrics marked
|
|
34
|
+
* `collected_only` (BP, vascular aging) are captured for later research but
|
|
35
|
+
* kept out of consumer UI in v0.1.0.
|
|
36
|
+
*/
|
|
37
|
+
export function isMetricExposed(id) {
|
|
38
|
+
return METRIC_METADATA[id].exposure === 'full';
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* List of every metric id where `exposure === 'full'` — convenience for
|
|
42
|
+
* UI code that iterates the visible surface.
|
|
43
|
+
*/
|
|
44
|
+
export function getExposedMetricIds() {
|
|
45
|
+
return Object.keys(METRIC_METADATA).filter(isMetricExposed);
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=metric_metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metric_metadata.js","sourceRoot":"","sources":["../../src/core/metric_metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAoBH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,CAAC;IAC3C,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;IAClF,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;IAClF,WAAW,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,gBAAgB,EAAE,EAAE,EAAE,QAAQ,EAAE,gBAAgB,EAAE;IACtG,YAAY,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,gBAAgB,EAAE,EAAE,EAAE,QAAQ,EAAE,gBAAgB,EAAE;IACxG,cAAc,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,EAAE,EAAE,QAAQ,EAAE,gBAAgB,EAAE;IAC5G,MAAM,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;IACtF,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;IAC/E,MAAM,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;IAC1F,cAAc,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE;IAC7F,QAAQ,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,EAAE,UAAU,EAAE,gBAAgB,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;IAClG,WAAW,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,EAAE,aAAa,EAAE,gBAAgB,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;IACxG,mBAAmB,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,eAAe,EAAE,gBAAgB,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE;IAC3H,WAAW,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE;IACpH,cAAc,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,gBAAgB,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE;CACrC,CAAC,CAAA;AAInE;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,EAAY;IAC1C,OAAO,eAAe,CAAC,EAAE,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAA;AAChD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAQ,MAAM,CAAC,IAAI,CAAC,eAAe,CAAgB,CAAC,MAAM,CAAC,eAAe,CAAC,CAAA;AAC7E,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema migrations for the warehouse.
|
|
3
|
+
*
|
|
4
|
+
* Version is tracked per `(brand, tenantId)` in KVStore. v0.1.0 has a single
|
|
5
|
+
* baseline migration (step 1: `ensureSchemas`) — future migrations append to
|
|
6
|
+
* `MIGRATIONS` in order; each `up(db)` receives the attached catalog name.
|
|
7
|
+
*
|
|
8
|
+
* `ensureMigrations` is idempotent and safe to call on every attach.
|
|
9
|
+
*/
|
|
10
|
+
import type { HybridDuckDB } from './engine';
|
|
11
|
+
import type { KVStore } from './types';
|
|
12
|
+
export interface MigrationContext {
|
|
13
|
+
brand: string;
|
|
14
|
+
tenantId: string;
|
|
15
|
+
}
|
|
16
|
+
export interface Migration {
|
|
17
|
+
/** Sequential integer, starting at 1. */
|
|
18
|
+
version: number;
|
|
19
|
+
/** Human name shown in errors + logs. */
|
|
20
|
+
name: string;
|
|
21
|
+
/** Idempotent DDL — called with the attached catalog alias. */
|
|
22
|
+
up(db: HybridDuckDB, catalog: string): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Ordered list of migrations. Each entry's `version` must equal its
|
|
26
|
+
* (1-based) position; `CURRENT_VERSION` is derived from the tail.
|
|
27
|
+
*/
|
|
28
|
+
export declare const MIGRATIONS: readonly Migration[];
|
|
29
|
+
/** Version of the latest known migration — target for every attach. */
|
|
30
|
+
export declare const CURRENT_VERSION: number;
|
|
31
|
+
/**
|
|
32
|
+
* Compute the KV key holding the applied schema version for a
|
|
33
|
+
* `(brand, tenantId)` pair.
|
|
34
|
+
*/
|
|
35
|
+
export declare function schemaVersionKey(brand: string, tenantId: string): string;
|
|
36
|
+
export interface EnsureMigrationsResult {
|
|
37
|
+
from: number;
|
|
38
|
+
to: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Run every migration between the persisted version and `CURRENT_VERSION`.
|
|
42
|
+
*
|
|
43
|
+
* - First launch: `from = 0`, applies every migration.
|
|
44
|
+
* - Same-version rerun: no-op, KV unchanged, returns `from === to`.
|
|
45
|
+
* - Upgrade path: applies only newer migrations in order.
|
|
46
|
+
* - Any failure at step N surfaces as
|
|
47
|
+
* `AnalyticsError('migration_failed', 'step-<n>', cause)`. KV is only
|
|
48
|
+
* advanced after each successful step so a partial upgrade leaves the
|
|
49
|
+
* version at the last successful step (subsequent runs retry from there).
|
|
50
|
+
*/
|
|
51
|
+
export declare function ensureMigrations(db: HybridDuckDB, kv: KVStore, ctx: MigrationContext, catalog: string): Promise<EnsureMigrationsResult>;
|
|
52
|
+
//# sourceMappingURL=migrations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/core/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAE5C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAEtC,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAA;IACf,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,+DAA+D;IAC/D,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACrD;AAED;;;GAGG;AACH,eAAO,MAAM,UAAU,EAAE,SAAS,SAAS,EAQzC,CAAA;AAEF,uEAAuE;AACvE,eAAO,MAAM,eAAe,EAAE,MAA0B,CAAA;AAExD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAExE;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;CACX;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,YAAY,EAChB,EAAE,EAAE,OAAO,EACX,GAAG,EAAE,gBAAgB,EACrB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,sBAAsB,CAAC,CA2BjC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema migrations for the warehouse.
|
|
3
|
+
*
|
|
4
|
+
* Version is tracked per `(brand, tenantId)` in KVStore. v0.1.0 has a single
|
|
5
|
+
* baseline migration (step 1: `ensureSchemas`) — future migrations append to
|
|
6
|
+
* `MIGRATIONS` in order; each `up(db)` receives the attached catalog name.
|
|
7
|
+
*
|
|
8
|
+
* `ensureMigrations` is idempotent and safe to call on every attach.
|
|
9
|
+
*/
|
|
10
|
+
import { AnalyticsError } from './errors';
|
|
11
|
+
import { ensureSchemas } from './schemas';
|
|
12
|
+
/**
|
|
13
|
+
* Ordered list of migrations. Each entry's `version` must equal its
|
|
14
|
+
* (1-based) position; `CURRENT_VERSION` is derived from the tail.
|
|
15
|
+
*/
|
|
16
|
+
export const MIGRATIONS = Object.freeze([
|
|
17
|
+
{
|
|
18
|
+
version: 1,
|
|
19
|
+
name: 'baseline schemas',
|
|
20
|
+
async up(db, catalog) {
|
|
21
|
+
await ensureSchemas(db, catalog);
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
]);
|
|
25
|
+
/** Version of the latest known migration — target for every attach. */
|
|
26
|
+
export const CURRENT_VERSION = MIGRATIONS.length;
|
|
27
|
+
/**
|
|
28
|
+
* Compute the KV key holding the applied schema version for a
|
|
29
|
+
* `(brand, tenantId)` pair.
|
|
30
|
+
*/
|
|
31
|
+
export function schemaVersionKey(brand, tenantId) {
|
|
32
|
+
return `analytics:schema-version:${brand}:${tenantId}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Run every migration between the persisted version and `CURRENT_VERSION`.
|
|
36
|
+
*
|
|
37
|
+
* - First launch: `from = 0`, applies every migration.
|
|
38
|
+
* - Same-version rerun: no-op, KV unchanged, returns `from === to`.
|
|
39
|
+
* - Upgrade path: applies only newer migrations in order.
|
|
40
|
+
* - Any failure at step N surfaces as
|
|
41
|
+
* `AnalyticsError('migration_failed', 'step-<n>', cause)`. KV is only
|
|
42
|
+
* advanced after each successful step so a partial upgrade leaves the
|
|
43
|
+
* version at the last successful step (subsequent runs retry from there).
|
|
44
|
+
*/
|
|
45
|
+
export async function ensureMigrations(db, kv, ctx, catalog) {
|
|
46
|
+
const key = schemaVersionKey(ctx.brand, ctx.tenantId);
|
|
47
|
+
const stored = await kv.get(key);
|
|
48
|
+
const from = typeof stored === 'number' ? stored : 0;
|
|
49
|
+
if (from >= CURRENT_VERSION) {
|
|
50
|
+
return { from, to: from };
|
|
51
|
+
}
|
|
52
|
+
for (const migration of MIGRATIONS) {
|
|
53
|
+
if (migration.version <= from) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
await migration.up(db, catalog);
|
|
58
|
+
}
|
|
59
|
+
catch (cause) {
|
|
60
|
+
throw new AnalyticsError('migration_failed', `migration step-${migration.version} (${migration.name}) failed`, cause);
|
|
61
|
+
}
|
|
62
|
+
await kv.set(key, migration.version);
|
|
63
|
+
}
|
|
64
|
+
return { from, to: CURRENT_VERSION };
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=migrations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/core/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAEzC,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AAiBzC;;;GAGG;AACH,MAAM,CAAC,MAAM,UAAU,GAAyB,MAAM,CAAC,MAAM,CAAC;IAC5D;QACE,OAAO,EAAE,CAAC;QACV,IAAI,EAAE,kBAAkB;QACxB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,OAAO;YAClB,MAAM,aAAa,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA;QAClC,CAAC;KACF;CACF,CAAC,CAAA;AAEF,uEAAuE;AACvE,MAAM,CAAC,MAAM,eAAe,GAAW,UAAU,CAAC,MAAM,CAAA;AAExD;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAa,EAAE,QAAgB;IAC9D,OAAO,4BAA4B,KAAK,IAAI,QAAQ,EAAE,CAAA;AACxD,CAAC;AAOD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,EAAgB,EAChB,EAAW,EACX,GAAqB,EACrB,OAAe;IAEf,MAAM,GAAG,GAAG,gBAAgB,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAA;IACrD,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,CAAS,GAAG,CAAC,CAAA;IACxC,MAAM,IAAI,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;IAEpD,IAAI,IAAI,IAAI,eAAe,EAAE,CAAC;QAC5B,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;IAC3B,CAAC;IAED,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,SAAS,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC;YAC9B,SAAQ;QACV,CAAC;QACD,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA;QACjC,CAAC;QACD,OAAO,KAAK,EAAE,CAAC;YACb,MAAM,IAAI,cAAc,CACtB,kBAAkB,EAClB,kBAAkB,SAAS,CAAC,OAAO,KAAK,SAAS,CAAC,IAAI,UAAU,EAChE,KAAK,CACN,CAAA;QACH,CAAC;QACD,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,OAAO,CAAC,CAAA;IACtC,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,eAAe,EAAE,CAAA;AACtC,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KVStore-backed persistence for analytics state.
|
|
3
|
+
*
|
|
4
|
+
* Two concerns:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Last-attach ctx** — persisted on every successful attach and cleared
|
|
7
|
+
* on detach. `loadLastAttach` returns the ctx only if it was written
|
|
8
|
+
* within `LAST_ATTACH_TTL_MS` (24h per spec §Testing) so apps can safely
|
|
9
|
+
* auto-reattach after a crash / cold start without silently reviving a
|
|
10
|
+
* stale multi-day-old context.
|
|
11
|
+
*
|
|
12
|
+
* 2. **Retention override** — `setRetention(days)` persists the user
|
|
13
|
+
* override layer per spec §Retention precedence at
|
|
14
|
+
* `analytics:retention:override:{brand}:{tenantId}:{userId}`. v0.1.0
|
|
15
|
+
* resolves via `resolveEffectiveRetention` (retention.ts) but does not
|
|
16
|
+
* surface UX to write it (`analytics.setRetention` is the escape hatch).
|
|
17
|
+
*
|
|
18
|
+
* All keys use the `analytics:` prefix so consumers can namespace-clear.
|
|
19
|
+
*/
|
|
20
|
+
import type { AttachContext, KVStore } from './types';
|
|
21
|
+
/** 24h in ms — freshness bound for the persisted attach ctx. */
|
|
22
|
+
export declare const LAST_ATTACH_TTL_MS: number;
|
|
23
|
+
/**
|
|
24
|
+
* Persist the ctx that just successfully attached. Overwrites any prior
|
|
25
|
+
* entry for the same brand.
|
|
26
|
+
*/
|
|
27
|
+
export declare function saveLastAttach(kv: KVStore, ctx: AttachContext, now?: number): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Load the last attach ctx for `brand` if it was written within the TTL.
|
|
30
|
+
* Returns `null` for missing entries or entries older than
|
|
31
|
+
* `LAST_ATTACH_TTL_MS`. Stale entries are also deleted as a side effect so
|
|
32
|
+
* repeated reads don't keep resurfacing them.
|
|
33
|
+
*/
|
|
34
|
+
export declare function loadLastAttach(kv: KVStore, brand: string, now?: number): Promise<AttachContext | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Clear the persisted attach ctx for `brand`. Invoked on `detach()` so a
|
|
37
|
+
* subsequent open doesn't auto-restore a context the app deliberately left.
|
|
38
|
+
*/
|
|
39
|
+
export declare function clearLastAttach(kv: KVStore, brand: string): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Persist the user-override retention for the current attach context.
|
|
42
|
+
* Overwrites any prior override for the same `(brand, tenantId, userId)`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function saveRetentionOverride(kv: KVStore, ctx: AttachContext, days: number, now?: number): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Load the user-override retention (days) for `(brand, tenantId, userId)`,
|
|
47
|
+
* or `null` when none is stored.
|
|
48
|
+
*/
|
|
49
|
+
export declare function loadRetentionOverride(kv: KVStore, ctx: AttachContext): Promise<number | null>;
|
|
50
|
+
//# sourceMappingURL=persistence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../src/core/persistence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAIrD,gEAAgE;AAChE,eAAO,MAAM,kBAAkB,QAAsB,CAAA;AAiBrD;;;GAGG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,OAAO,EACX,GAAG,EAAE,aAAa,EAClB,GAAG,GAAE,MAAmB,GACvB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,OAAO,EACX,KAAK,EAAE,MAAM,EACb,GAAG,GAAE,MAAmB,GACvB,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAY/B;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/E;AASD;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,OAAO,EACX,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,MAAM,EACZ,GAAG,GAAE,MAAmB,GACvB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,OAAO,EACX,GAAG,EAAE,aAAa,GACjB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKxB"}
|