@mongrov/analytics 0.1.0-alpha.1 → 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.
Files changed (171) hide show
  1. package/README.md +456 -0
  2. package/dist/core/context.d.ts +24 -0
  3. package/dist/core/context.d.ts.map +1 -0
  4. package/dist/core/context.js +30 -0
  5. package/dist/core/context.js.map +1 -0
  6. package/dist/core/engine.d.ts +56 -0
  7. package/dist/core/engine.d.ts.map +1 -0
  8. package/dist/core/engine.js +119 -0
  9. package/dist/core/engine.js.map +1 -0
  10. package/dist/core/errors.d.ts +1 -1
  11. package/dist/core/errors.d.ts.map +1 -1
  12. package/dist/core/errors.js.map +1 -1
  13. package/dist/core/extensions.d.ts +33 -0
  14. package/dist/core/extensions.d.ts.map +1 -0
  15. package/dist/core/extensions.js +58 -0
  16. package/dist/core/extensions.js.map +1 -0
  17. package/dist/core/factory.d.ts +22 -5
  18. package/dist/core/factory.d.ts.map +1 -1
  19. package/dist/core/factory.js +231 -6
  20. package/dist/core/factory.js.map +1 -1
  21. package/dist/core/hooks.d.ts +32 -13
  22. package/dist/core/hooks.d.ts.map +1 -1
  23. package/dist/core/hooks.js +137 -15
  24. package/dist/core/hooks.js.map +1 -1
  25. package/dist/core/index.d.ts +3 -0
  26. package/dist/core/index.d.ts.map +1 -1
  27. package/dist/core/index.js +3 -1
  28. package/dist/core/index.js.map +1 -1
  29. package/dist/core/logger.d.ts +25 -0
  30. package/dist/core/logger.d.ts.map +1 -0
  31. package/dist/core/logger.js +33 -0
  32. package/dist/core/logger.js.map +1 -0
  33. package/dist/core/machine.d.ts +174 -0
  34. package/dist/core/machine.d.ts.map +1 -0
  35. package/dist/core/machine.js +228 -0
  36. package/dist/core/machine.js.map +1 -0
  37. package/dist/core/metric_metadata.d.ts +128 -0
  38. package/dist/core/metric_metadata.d.ts.map +1 -0
  39. package/dist/core/metric_metadata.js +47 -0
  40. package/dist/core/metric_metadata.js.map +1 -0
  41. package/dist/core/migrations.d.ts +52 -0
  42. package/dist/core/migrations.d.ts.map +1 -0
  43. package/dist/core/migrations.js +66 -0
  44. package/dist/core/migrations.js.map +1 -0
  45. package/dist/core/persistence.d.ts +50 -0
  46. package/dist/core/persistence.d.ts.map +1 -0
  47. package/dist/core/persistence.js +80 -0
  48. package/dist/core/persistence.js.map +1 -0
  49. package/dist/core/retention.d.ts +90 -0
  50. package/dist/core/retention.d.ts.map +1 -0
  51. package/dist/core/retention.js +119 -0
  52. package/dist/core/retention.js.map +1 -0
  53. package/dist/core/schemas.d.ts +35 -0
  54. package/dist/core/schemas.d.ts.map +1 -0
  55. package/dist/core/schemas.js +209 -0
  56. package/dist/core/schemas.js.map +1 -0
  57. package/dist/core/types.d.ts +10 -0
  58. package/dist/core/types.d.ts.map +1 -1
  59. package/dist/core/warehouse.d.ts +58 -0
  60. package/dist/core/warehouse.d.ts.map +1 -0
  61. package/dist/core/warehouse.js +116 -0
  62. package/dist/core/warehouse.js.map +1 -0
  63. package/dist/sync/buffer.d.ts +51 -0
  64. package/dist/sync/buffer.d.ts.map +1 -0
  65. package/dist/sync/buffer.js +188 -0
  66. package/dist/sync/buffer.js.map +1 -0
  67. package/dist/sync/context.d.ts +17 -0
  68. package/dist/sync/context.d.ts.map +1 -0
  69. package/dist/sync/context.js +23 -0
  70. package/dist/sync/context.js.map +1 -0
  71. package/dist/sync/errors.d.ts +19 -0
  72. package/dist/sync/errors.d.ts.map +1 -0
  73. package/dist/sync/errors.js +21 -0
  74. package/dist/sync/errors.js.map +1 -0
  75. package/dist/sync/events.d.ts +49 -0
  76. package/dist/sync/events.d.ts.map +1 -0
  77. package/dist/sync/events.js +46 -0
  78. package/dist/sync/events.js.map +1 -0
  79. package/dist/sync/factory.d.ts +57 -0
  80. package/dist/sync/factory.d.ts.map +1 -0
  81. package/dist/sync/factory.js +249 -0
  82. package/dist/sync/factory.js.map +1 -0
  83. package/dist/sync/fetcher.d.ts +74 -0
  84. package/dist/sync/fetcher.d.ts.map +1 -0
  85. package/dist/sync/fetcher.js +166 -0
  86. package/dist/sync/fetcher.js.map +1 -0
  87. package/dist/sync/flusher.d.ts +93 -0
  88. package/dist/sync/flusher.d.ts.map +1 -0
  89. package/dist/sync/flusher.js +234 -0
  90. package/dist/sync/flusher.js.map +1 -0
  91. package/dist/sync/hooks.d.ts +17 -0
  92. package/dist/sync/hooks.d.ts.map +1 -0
  93. package/dist/sync/hooks.js +48 -0
  94. package/dist/sync/hooks.js.map +1 -0
  95. package/dist/sync/index.d.ts +32 -1
  96. package/dist/sync/index.d.ts.map +1 -1
  97. package/dist/sync/index.js +36 -1
  98. package/dist/sync/index.js.map +1 -1
  99. package/dist/sync/manager.d.ts +48 -0
  100. package/dist/sync/manager.d.ts.map +1 -0
  101. package/dist/sync/manager.js +9 -0
  102. package/dist/sync/manager.js.map +1 -0
  103. package/dist/sync/mapper/activity.d.ts +31 -0
  104. package/dist/sync/mapper/activity.d.ts.map +1 -0
  105. package/dist/sync/mapper/activity.js +56 -0
  106. package/dist/sync/mapper/activity.js.map +1 -0
  107. package/dist/sync/mapper/battery.d.ts +12 -0
  108. package/dist/sync/mapper/battery.d.ts.map +1 -0
  109. package/dist/sync/mapper/battery.js +22 -0
  110. package/dist/sync/mapper/battery.js.map +1 -0
  111. package/dist/sync/mapper/firmware.d.ts +28 -0
  112. package/dist/sync/mapper/firmware.d.ts.map +1 -0
  113. package/dist/sync/mapper/firmware.js +43 -0
  114. package/dist/sync/mapper/firmware.js.map +1 -0
  115. package/dist/sync/mapper/heart-rate.d.ts +10 -0
  116. package/dist/sync/mapper/heart-rate.d.ts.map +1 -0
  117. package/dist/sync/mapper/heart-rate.js +19 -0
  118. package/dist/sync/mapper/heart-rate.js.map +1 -0
  119. package/dist/sync/mapper/hrv.d.ts +22 -0
  120. package/dist/sync/mapper/hrv.d.ts.map +1 -0
  121. package/dist/sync/mapper/hrv.js +56 -0
  122. package/dist/sync/mapper/hrv.js.map +1 -0
  123. package/dist/sync/mapper/ring-config.d.ts +33 -0
  124. package/dist/sync/mapper/ring-config.d.ts.map +1 -0
  125. package/dist/sync/mapper/ring-config.js +48 -0
  126. package/dist/sync/mapper/ring-config.js.map +1 -0
  127. package/dist/sync/mapper/sleep.d.ts +32 -0
  128. package/dist/sync/mapper/sleep.d.ts.map +1 -0
  129. package/dist/sync/mapper/sleep.js +103 -0
  130. package/dist/sync/mapper/sleep.js.map +1 -0
  131. package/dist/sync/mapper/spo2.d.ts +9 -0
  132. package/dist/sync/mapper/spo2.d.ts.map +1 -0
  133. package/dist/sync/mapper/spo2.js +18 -0
  134. package/dist/sync/mapper/spo2.js.map +1 -0
  135. package/dist/sync/mapper/temperature.d.ts +10 -0
  136. package/dist/sync/mapper/temperature.d.ts.map +1 -0
  137. package/dist/sync/mapper/temperature.js +19 -0
  138. package/dist/sync/mapper/temperature.js.map +1 -0
  139. package/dist/sync/mapper/time.d.ts +49 -0
  140. package/dist/sync/mapper/time.d.ts.map +1 -0
  141. package/dist/sync/mapper/time.js +107 -0
  142. package/dist/sync/mapper/time.js.map +1 -0
  143. package/dist/sync/mapper/types.d.ts +152 -0
  144. package/dist/sync/mapper/types.d.ts.map +1 -0
  145. package/dist/sync/mapper/types.js +15 -0
  146. package/dist/sync/mapper/types.js.map +1 -0
  147. package/dist/sync/overflow.d.ts +30 -0
  148. package/dist/sync/overflow.d.ts.map +1 -0
  149. package/dist/sync/overflow.js +114 -0
  150. package/dist/sync/overflow.js.map +1 -0
  151. package/dist/sync/pusher.d.ts +59 -0
  152. package/dist/sync/pusher.d.ts.map +1 -0
  153. package/dist/sync/pusher.js +115 -0
  154. package/dist/sync/pusher.js.map +1 -0
  155. package/dist/sync/scheduler.d.ts +84 -0
  156. package/dist/sync/scheduler.d.ts.map +1 -0
  157. package/dist/sync/scheduler.js +132 -0
  158. package/dist/sync/scheduler.js.map +1 -0
  159. package/dist/sync/triggers.d.ts +64 -0
  160. package/dist/sync/triggers.d.ts.map +1 -0
  161. package/dist/sync/triggers.js +113 -0
  162. package/dist/sync/triggers.js.map +1 -0
  163. package/dist/sync/types.d.ts +50 -0
  164. package/dist/sync/types.d.ts.map +1 -0
  165. package/dist/sync/types.js +8 -0
  166. package/dist/sync/types.js.map +1 -0
  167. package/dist/sync/watermark.d.ts +32 -0
  168. package/dist/sync/watermark.d.ts.map +1 -0
  169. package/dist/sync/watermark.js +61 -0
  170. package/dist/sync/watermark.js.map +1 -0
  171. package/package.json +12 -2
@@ -0,0 +1,84 @@
1
+ /**
2
+ * T-22 + T-23 + T-24 — SyncScheduler.
3
+ *
4
+ * Wraps `expo-background-task` via an injected `BackgroundTaskPort` so unit
5
+ * tests can drive registration/dispatch without touching the native module.
6
+ *
7
+ * A cycle runs: constraint check → flush → push → fetch → (optional rules).
8
+ * Constraints (wifi + charging) are queried via `ConstraintPort`; failing
9
+ * checks emit a debug log and skip the cycle with `SyncError('constraint_not_met')`.
10
+ * `triggerNow()` bypasses constraints and always runs (T-24).
11
+ *
12
+ * Design.md §sync/scheduler.ts is the source of truth for the surface.
13
+ */
14
+ import { SyncError } from './errors';
15
+ import type { AttachContext } from '../core/types';
16
+ /**
17
+ * Injected wrapper around `expo-background-task`. Kept minimal — start/stop
18
+ * plus a fire-once dispatch hook the caller uses to run the cycle for tests.
19
+ */
20
+ export interface BackgroundTaskPort {
21
+ register: (taskName: string, handler: () => Promise<void>) => Promise<void>;
22
+ unregister: (taskName: string) => Promise<void>;
23
+ isRegistered: (taskName: string) => Promise<boolean>;
24
+ }
25
+ /** Battery + network gates that decide whether a scheduled cycle runs. */
26
+ export interface ConstraintPort {
27
+ isCharging: () => Promise<boolean>;
28
+ isOnWifi: () => Promise<boolean>;
29
+ }
30
+ /** Sync operations the scheduler drives per cycle. */
31
+ export interface SyncCoordinator {
32
+ flushAll: () => Promise<void>;
33
+ pushAll: (tables: string[], ctx: AttachContext) => Promise<Array<{
34
+ ok: boolean;
35
+ }>>;
36
+ fetchIncremental: (ctx: AttachContext) => Promise<Array<{
37
+ ok: boolean;
38
+ }>>;
39
+ onCycleComplete?: () => Promise<void>;
40
+ }
41
+ /** Debug/info logger — matches `@mongrov/core` shape narrowly. */
42
+ export interface SchedulerLogger {
43
+ debug: (msg: string, meta?: Record<string, unknown>) => void;
44
+ info: (msg: string, meta?: Record<string, unknown>) => void;
45
+ warn: (msg: string, meta?: Record<string, unknown>) => void;
46
+ }
47
+ export interface SchedulerConstraints {
48
+ /** Require Wi-Fi (skip on cellular). Default: true. */
49
+ requireWifi?: boolean;
50
+ /** Require the device to be charging. Default: false. */
51
+ requireCharging?: boolean;
52
+ }
53
+ export interface SyncSchedulerConfig {
54
+ backgroundTask: BackgroundTaskPort;
55
+ constraints: ConstraintPort;
56
+ coordinator: SyncCoordinator;
57
+ /** Tables to push each cycle. */
58
+ tables: string[];
59
+ /** Attach context to run against. */
60
+ ctx: AttachContext;
61
+ /** Task name registered with the background scheduler. */
62
+ taskName?: string;
63
+ constraintPolicy?: SchedulerConstraints;
64
+ logger?: SchedulerLogger;
65
+ }
66
+ export type SchedulerState = 'idle' | 'running' | 'error';
67
+ export interface CycleResult {
68
+ ok: boolean;
69
+ skipped: boolean;
70
+ reason?: 'constraint_not_met';
71
+ error?: SyncError;
72
+ }
73
+ export declare class SyncScheduler {
74
+ #private;
75
+ constructor(config: SyncSchedulerConfig);
76
+ get state(): SchedulerState;
77
+ subscribe(fn: (state: SchedulerState) => void): () => void;
78
+ start(): Promise<void>;
79
+ stop(): Promise<void>;
80
+ isRegistered(): Promise<boolean>;
81
+ /** T-24 — manual trigger bypasses constraints. */
82
+ triggerNow(): Promise<CycleResult>;
83
+ }
84
+ //# sourceMappingURL=scheduler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../src/sync/scheduler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AACpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAElD;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3E,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/C,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CACrD;AAED,0EAA0E;AAC1E,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;IAClC,QAAQ,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CACjC;AAED,sDAAsD;AACtD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7B,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,KAAK,CAAC;QAAE,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC,CAAA;IAClF,gBAAgB,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,KAAK,CAAC;QAAE,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC,CAAA;IACzE,eAAe,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACtC;AAED,kEAAkE;AAClE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IAC5D,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IAC3D,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;CAC5D;AAED,MAAM,WAAW,oBAAoB;IACnC,uDAAuD;IACvD,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,yDAAyD;IACzD,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,kBAAkB,CAAA;IAClC,WAAW,EAAE,cAAc,CAAA;IAC3B,WAAW,EAAE,eAAe,CAAA;IAC5B,iCAAiC;IACjC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,qCAAqC;IACrC,GAAG,EAAE,aAAa,CAAA;IAClB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gBAAgB,CAAC,EAAE,oBAAoB,CAAA;IACvC,MAAM,CAAC,EAAE,eAAe,CAAA;CACzB;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAA;AAEzD,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,OAAO,CAAA;IACX,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,CAAC,EAAE,oBAAoB,CAAA;IAC7B,KAAK,CAAC,EAAE,SAAS,CAAA;CAClB;AAUD,qBAAa,aAAa;;gBAQZ,MAAM,EAAE,mBAAmB;IAgBvC,IAAI,KAAK,IAAI,cAAc,CAE1B;IAED,SAAS,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,MAAM,IAAI;IAKpD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAOtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAKrB,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC;IAItC,kDAAkD;IAC5C,UAAU,IAAI,OAAO,CAAC,WAAW,CAAC;CA6DzC"}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * T-22 + T-23 + T-24 — SyncScheduler.
3
+ *
4
+ * Wraps `expo-background-task` via an injected `BackgroundTaskPort` so unit
5
+ * tests can drive registration/dispatch without touching the native module.
6
+ *
7
+ * A cycle runs: constraint check → flush → push → fetch → (optional rules).
8
+ * Constraints (wifi + charging) are queried via `ConstraintPort`; failing
9
+ * checks emit a debug log and skip the cycle with `SyncError('constraint_not_met')`.
10
+ * `triggerNow()` bypasses constraints and always runs (T-24).
11
+ *
12
+ * Design.md §sync/scheduler.ts is the source of truth for the surface.
13
+ */
14
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
15
+ if (kind === "m") throw new TypeError("Private method is not writable");
16
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
17
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
18
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
19
+ };
20
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
21
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
22
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
23
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
24
+ };
25
+ var _SyncScheduler_instances, _SyncScheduler_config, _SyncScheduler_state, _SyncScheduler_subscribers, _SyncScheduler_runCycle, _SyncScheduler_checkConstraints, _SyncScheduler_setState;
26
+ import { SyncError } from './errors';
27
+ const DEFAULT_TASK_NAME = '@mongrov/analytics:sync';
28
+ const noopLogger = {
29
+ debug: () => { },
30
+ info: () => { },
31
+ warn: () => { },
32
+ };
33
+ export class SyncScheduler {
34
+ constructor(config) {
35
+ _SyncScheduler_instances.add(this);
36
+ _SyncScheduler_config.set(this, void 0);
37
+ _SyncScheduler_state.set(this, 'idle');
38
+ _SyncScheduler_subscribers.set(this, new Set());
39
+ __classPrivateFieldSet(this, _SyncScheduler_config, {
40
+ backgroundTask: config.backgroundTask,
41
+ constraints: config.constraints,
42
+ coordinator: config.coordinator,
43
+ tables: config.tables,
44
+ ctx: config.ctx,
45
+ taskName: config.taskName ?? DEFAULT_TASK_NAME,
46
+ logger: config.logger ?? noopLogger,
47
+ constraintPolicy: {
48
+ requireWifi: config.constraintPolicy?.requireWifi ?? true,
49
+ requireCharging: config.constraintPolicy?.requireCharging ?? false,
50
+ },
51
+ }, "f");
52
+ }
53
+ get state() {
54
+ return __classPrivateFieldGet(this, _SyncScheduler_state, "f");
55
+ }
56
+ subscribe(fn) {
57
+ __classPrivateFieldGet(this, _SyncScheduler_subscribers, "f").add(fn);
58
+ return () => __classPrivateFieldGet(this, _SyncScheduler_subscribers, "f").delete(fn);
59
+ }
60
+ async start() {
61
+ await __classPrivateFieldGet(this, _SyncScheduler_config, "f").backgroundTask.register(__classPrivateFieldGet(this, _SyncScheduler_config, "f").taskName, async () => {
62
+ await __classPrivateFieldGet(this, _SyncScheduler_instances, "m", _SyncScheduler_runCycle).call(this, false);
63
+ });
64
+ __classPrivateFieldGet(this, _SyncScheduler_config, "f").logger.info('scheduler.start', { taskName: __classPrivateFieldGet(this, _SyncScheduler_config, "f").taskName });
65
+ }
66
+ async stop() {
67
+ await __classPrivateFieldGet(this, _SyncScheduler_config, "f").backgroundTask.unregister(__classPrivateFieldGet(this, _SyncScheduler_config, "f").taskName);
68
+ __classPrivateFieldGet(this, _SyncScheduler_config, "f").logger.info('scheduler.stop', { taskName: __classPrivateFieldGet(this, _SyncScheduler_config, "f").taskName });
69
+ }
70
+ async isRegistered() {
71
+ return __classPrivateFieldGet(this, _SyncScheduler_config, "f").backgroundTask.isRegistered(__classPrivateFieldGet(this, _SyncScheduler_config, "f").taskName);
72
+ }
73
+ /** T-24 — manual trigger bypasses constraints. */
74
+ async triggerNow() {
75
+ return __classPrivateFieldGet(this, _SyncScheduler_instances, "m", _SyncScheduler_runCycle).call(this, true);
76
+ }
77
+ }
78
+ _SyncScheduler_config = new WeakMap(), _SyncScheduler_state = new WeakMap(), _SyncScheduler_subscribers = new WeakMap(), _SyncScheduler_instances = new WeakSet(), _SyncScheduler_runCycle = async function _SyncScheduler_runCycle(bypassConstraints) {
79
+ if (__classPrivateFieldGet(this, _SyncScheduler_state, "f") === 'running') {
80
+ __classPrivateFieldGet(this, _SyncScheduler_config, "f").logger.debug('scheduler.cycle.skip', { reason: 'already_running' });
81
+ return { ok: false, skipped: true };
82
+ }
83
+ if (!bypassConstraints) {
84
+ const allowed = await __classPrivateFieldGet(this, _SyncScheduler_instances, "m", _SyncScheduler_checkConstraints).call(this);
85
+ if (!allowed) {
86
+ __classPrivateFieldGet(this, _SyncScheduler_config, "f").logger.debug('scheduler.cycle.skip', { reason: 'constraint_not_met' });
87
+ return {
88
+ ok: false,
89
+ skipped: true,
90
+ reason: 'constraint_not_met',
91
+ error: new SyncError('constraint_not_met', 'Battery/network constraints not satisfied'),
92
+ };
93
+ }
94
+ }
95
+ __classPrivateFieldGet(this, _SyncScheduler_instances, "m", _SyncScheduler_setState).call(this, 'running');
96
+ try {
97
+ await __classPrivateFieldGet(this, _SyncScheduler_config, "f").coordinator.flushAll();
98
+ await __classPrivateFieldGet(this, _SyncScheduler_config, "f").coordinator.pushAll(__classPrivateFieldGet(this, _SyncScheduler_config, "f").tables, __classPrivateFieldGet(this, _SyncScheduler_config, "f").ctx);
99
+ await __classPrivateFieldGet(this, _SyncScheduler_config, "f").coordinator.fetchIncremental(__classPrivateFieldGet(this, _SyncScheduler_config, "f").ctx);
100
+ await __classPrivateFieldGet(this, _SyncScheduler_config, "f").coordinator.onCycleComplete?.();
101
+ __classPrivateFieldGet(this, _SyncScheduler_config, "f").logger.debug('scheduler.cycle.ok');
102
+ __classPrivateFieldGet(this, _SyncScheduler_instances, "m", _SyncScheduler_setState).call(this, 'idle');
103
+ return { ok: true, skipped: false };
104
+ }
105
+ catch (cause) {
106
+ __classPrivateFieldGet(this, _SyncScheduler_config, "f").logger.warn('scheduler.cycle.error', { error: String(cause) });
107
+ __classPrivateFieldGet(this, _SyncScheduler_instances, "m", _SyncScheduler_setState).call(this, 'error');
108
+ return {
109
+ ok: false,
110
+ skipped: false,
111
+ error: new SyncError('flush_failed', 'Sync cycle failed', cause),
112
+ };
113
+ }
114
+ }, _SyncScheduler_checkConstraints = async function _SyncScheduler_checkConstraints() {
115
+ const { requireWifi, requireCharging } = __classPrivateFieldGet(this, _SyncScheduler_config, "f").constraintPolicy;
116
+ if (requireWifi) {
117
+ const wifi = await __classPrivateFieldGet(this, _SyncScheduler_config, "f").constraints.isOnWifi();
118
+ if (!wifi)
119
+ return false;
120
+ }
121
+ if (requireCharging) {
122
+ const charging = await __classPrivateFieldGet(this, _SyncScheduler_config, "f").constraints.isCharging();
123
+ if (!charging)
124
+ return false;
125
+ }
126
+ return true;
127
+ }, _SyncScheduler_setState = function _SyncScheduler_setState(next) {
128
+ __classPrivateFieldSet(this, _SyncScheduler_state, next, "f");
129
+ for (const fn of __classPrivateFieldGet(this, _SyncScheduler_subscribers, "f"))
130
+ fn(next);
131
+ };
132
+ //# sourceMappingURL=scheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../../src/sync/scheduler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Flush triggers (T-14).
3
+ *
4
+ * Observes the `SensorBuffer` + app lifecycle and decides when to enqueue a
5
+ * flush. Triggers are:
6
+ * - `row-count` → any table hits `maxRows`
7
+ * - `age` → oldest entry's `enqueuedAt` older than `maxAgeMs`
8
+ * - `foreground` → app-state 'active' transition
9
+ * - `background` → app-state 'background' transition (best-effort)
10
+ * - `manual` → caller-invoked
11
+ *
12
+ * All triggers ultimately funnel through `BatchFlusher.scheduleFlush(table,
13
+ * reason)`. Row-count / age evaluations are driven by an external ticker so
14
+ * the trigger controller stays synchronous + testable — the app wires a
15
+ * `setInterval` (or the sync scheduler) to `evaluate()`.
16
+ */
17
+ import type { BatchFlusher, FlushReason } from './flusher';
18
+ import type { SensorBuffer } from './buffer';
19
+ export interface FlushTriggersConfig {
20
+ buffer: SensorBuffer;
21
+ flusher: BatchFlusher;
22
+ /** Row count threshold — any table hitting this triggers a flush. */
23
+ maxRows?: number;
24
+ /** Age threshold in ms — oldest entry older than this triggers a flush. */
25
+ maxAgeMs?: number;
26
+ /** Test-injected clock. */
27
+ now?: () => number;
28
+ }
29
+ /** Default from spec §Flush triggers. */
30
+ export declare const DEFAULT_MAX_ROWS = 500;
31
+ export declare const DEFAULT_MAX_AGE_MS = 30000;
32
+ export declare class FlushTriggers {
33
+ #private;
34
+ constructor(config: FlushTriggersConfig);
35
+ /**
36
+ * Record the oldest-entry age for a table. Called by the buffer's push
37
+ * pipeline (via the sync manager) so we can evaluate the age trigger
38
+ * without traversing the ring on every check.
39
+ */
40
+ noteEnqueue(table: string, enqueuedAt: number): void;
41
+ /** Called after a drain empties a table so the age tracker restarts. */
42
+ noteDrain(table: string): void;
43
+ /**
44
+ * Poll the buffer and enqueue flushes for any table that trips a trigger.
45
+ * Returns the tables that were scheduled so tests can assert directly.
46
+ */
47
+ evaluate(): Promise<{
48
+ table: string;
49
+ reason: FlushReason;
50
+ }[]>;
51
+ /**
52
+ * App-state transition to `foreground`. Schedules a flush on every tracked
53
+ * table at priority 0.
54
+ */
55
+ onForeground(): void;
56
+ /**
57
+ * App-state transition to `background`. Best-effort background flush at
58
+ * lowest priority.
59
+ */
60
+ onBackground(): void;
61
+ /** Manual `flush()` — the caller decides the table set. */
62
+ manual(table: string): Promise<unknown>;
63
+ }
64
+ //# sourceMappingURL=triggers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"triggers.d.ts","sourceRoot":"","sources":["../../src/sync/triggers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAE5C,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,YAAY,CAAA;IACpB,OAAO,EAAE,YAAY,CAAA;IACrB,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2BAA2B;IAC3B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,yCAAyC;AACzC,eAAO,MAAM,gBAAgB,MAAM,CAAA;AACnC,eAAO,MAAM,kBAAkB,QAAS,CAAA;AAExC,qBAAa,aAAa;;gBASZ,MAAM,EAAE,mBAAmB;IAQvC;;;;OAIG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IAMpD,wEAAwE;IACxE,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI9B;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,WAAW,CAAA;KAAE,EAAE,CAAC;IAyBnE;;;OAGG;IACH,YAAY,IAAI,IAAI;IAMpB;;;OAGG;IACH,YAAY,IAAI,IAAI;IAMpB,2DAA2D;IAC3D,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAGxC"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Flush triggers (T-14).
3
+ *
4
+ * Observes the `SensorBuffer` + app lifecycle and decides when to enqueue a
5
+ * flush. Triggers are:
6
+ * - `row-count` → any table hits `maxRows`
7
+ * - `age` → oldest entry's `enqueuedAt` older than `maxAgeMs`
8
+ * - `foreground` → app-state 'active' transition
9
+ * - `background` → app-state 'background' transition (best-effort)
10
+ * - `manual` → caller-invoked
11
+ *
12
+ * All triggers ultimately funnel through `BatchFlusher.scheduleFlush(table,
13
+ * reason)`. Row-count / age evaluations are driven by an external ticker so
14
+ * the trigger controller stays synchronous + testable — the app wires a
15
+ * `setInterval` (or the sync scheduler) to `evaluate()`.
16
+ */
17
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
18
+ if (kind === "m") throw new TypeError("Private method is not writable");
19
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
20
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
21
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
22
+ };
23
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
24
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
25
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
26
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
27
+ };
28
+ var _FlushTriggers_buffer, _FlushTriggers_flusher, _FlushTriggers_maxRows, _FlushTriggers_maxAgeMs, _FlushTriggers_now, _FlushTriggers_oldestByTable;
29
+ /** Default from spec §Flush triggers. */
30
+ export const DEFAULT_MAX_ROWS = 500;
31
+ export const DEFAULT_MAX_AGE_MS = 30000;
32
+ export class FlushTriggers {
33
+ constructor(config) {
34
+ _FlushTriggers_buffer.set(this, void 0);
35
+ _FlushTriggers_flusher.set(this, void 0);
36
+ _FlushTriggers_maxRows.set(this, void 0);
37
+ _FlushTriggers_maxAgeMs.set(this, void 0);
38
+ _FlushTriggers_now.set(this, void 0);
39
+ /** Tracks the enqueuedAt of the oldest entry per table, seeded on first observation. */
40
+ _FlushTriggers_oldestByTable.set(this, new Map());
41
+ __classPrivateFieldSet(this, _FlushTriggers_buffer, config.buffer, "f");
42
+ __classPrivateFieldSet(this, _FlushTriggers_flusher, config.flusher, "f");
43
+ __classPrivateFieldSet(this, _FlushTriggers_maxRows, config.maxRows ?? DEFAULT_MAX_ROWS, "f");
44
+ __classPrivateFieldSet(this, _FlushTriggers_maxAgeMs, config.maxAgeMs ?? DEFAULT_MAX_AGE_MS, "f");
45
+ __classPrivateFieldSet(this, _FlushTriggers_now, config.now ?? (() => Date.now()), "f");
46
+ }
47
+ /**
48
+ * Record the oldest-entry age for a table. Called by the buffer's push
49
+ * pipeline (via the sync manager) so we can evaluate the age trigger
50
+ * without traversing the ring on every check.
51
+ */
52
+ noteEnqueue(table, enqueuedAt) {
53
+ if (!__classPrivateFieldGet(this, _FlushTriggers_oldestByTable, "f").has(table)) {
54
+ __classPrivateFieldGet(this, _FlushTriggers_oldestByTable, "f").set(table, enqueuedAt);
55
+ }
56
+ }
57
+ /** Called after a drain empties a table so the age tracker restarts. */
58
+ noteDrain(table) {
59
+ __classPrivateFieldGet(this, _FlushTriggers_oldestByTable, "f").delete(table);
60
+ }
61
+ /**
62
+ * Poll the buffer and enqueue flushes for any table that trips a trigger.
63
+ * Returns the tables that were scheduled so tests can assert directly.
64
+ */
65
+ async evaluate() {
66
+ const scheduled = [];
67
+ const size = await __classPrivateFieldGet(this, _FlushTriggers_buffer, "f").size();
68
+ // We evaluate per known table by consulting per-table size(). Rather than
69
+ // introspect the buffer's internals, use the tables the triggers already
70
+ // know about (via noteEnqueue) plus any table with a positive total.
71
+ const candidates = new Set(__classPrivateFieldGet(this, _FlushTriggers_oldestByTable, "f").keys());
72
+ for (const t of candidates) {
73
+ const perTable = await __classPrivateFieldGet(this, _FlushTriggers_buffer, "f").size(t);
74
+ if (perTable.inMemory >= __classPrivateFieldGet(this, _FlushTriggers_maxRows, "f")) {
75
+ scheduled.push({ table: t, reason: 'row-count' });
76
+ __classPrivateFieldGet(this, _FlushTriggers_flusher, "f").scheduleFlush(t, 'row-count');
77
+ continue;
78
+ }
79
+ const oldest = __classPrivateFieldGet(this, _FlushTriggers_oldestByTable, "f").get(t);
80
+ if (oldest !== undefined && __classPrivateFieldGet(this, _FlushTriggers_now, "f").call(this) - oldest >= __classPrivateFieldGet(this, _FlushTriggers_maxAgeMs, "f")) {
81
+ scheduled.push({ table: t, reason: 'age' });
82
+ __classPrivateFieldGet(this, _FlushTriggers_flusher, "f").scheduleFlush(t, 'age');
83
+ }
84
+ }
85
+ // Suppress the unused var warning for size — it may be surfaced later.
86
+ void size;
87
+ return scheduled;
88
+ }
89
+ /**
90
+ * App-state transition to `foreground`. Schedules a flush on every tracked
91
+ * table at priority 0.
92
+ */
93
+ onForeground() {
94
+ for (const t of __classPrivateFieldGet(this, _FlushTriggers_oldestByTable, "f").keys()) {
95
+ __classPrivateFieldGet(this, _FlushTriggers_flusher, "f").scheduleFlush(t, 'foreground');
96
+ }
97
+ }
98
+ /**
99
+ * App-state transition to `background`. Best-effort background flush at
100
+ * lowest priority.
101
+ */
102
+ onBackground() {
103
+ for (const t of __classPrivateFieldGet(this, _FlushTriggers_oldestByTable, "f").keys()) {
104
+ __classPrivateFieldGet(this, _FlushTriggers_flusher, "f").scheduleFlush(t, 'background');
105
+ }
106
+ }
107
+ /** Manual `flush()` — the caller decides the table set. */
108
+ manual(table) {
109
+ return __classPrivateFieldGet(this, _FlushTriggers_flusher, "f").scheduleFlush(table, 'manual');
110
+ }
111
+ }
112
+ _FlushTriggers_buffer = new WeakMap(), _FlushTriggers_flusher = new WeakMap(), _FlushTriggers_maxRows = new WeakMap(), _FlushTriggers_maxAgeMs = new WeakMap(), _FlushTriggers_now = new WeakMap(), _FlushTriggers_oldestByTable = new WeakMap();
113
+ //# sourceMappingURL=triggers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"triggers.js","sourceRoot":"","sources":["../../src/sync/triggers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;;;;;;;;;;;;;AAgBH,yCAAyC;AACzC,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAA;AACnC,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAM,CAAA;AAExC,MAAM,OAAO,aAAa;IASxB,YAAY,MAA2B;QAR9B,wCAAqB;QACrB,yCAAsB;QACtB,yCAAgB;QAChB,0CAAiB;QACjB,qCAAkB;QAC3B,wFAAwF;QACxF,uCAAiB,IAAI,GAAG,EAAkB,EAAA;QAGxC,uBAAA,IAAI,yBAAW,MAAM,CAAC,MAAM,MAAA,CAAA;QAC5B,uBAAA,IAAI,0BAAY,MAAM,CAAC,OAAO,MAAA,CAAA;QAC9B,uBAAA,IAAI,0BAAY,MAAM,CAAC,OAAO,IAAI,gBAAgB,MAAA,CAAA;QAClD,uBAAA,IAAI,2BAAa,MAAM,CAAC,QAAQ,IAAI,kBAAkB,MAAA,CAAA;QACtD,uBAAA,IAAI,sBAAQ,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,MAAA,CAAA;IAC9C,CAAC;IAED;;;;OAIG;IACH,WAAW,CAAC,KAAa,EAAE,UAAkB;QAC3C,IAAI,CAAC,uBAAA,IAAI,oCAAe,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YACpC,uBAAA,IAAI,oCAAe,CAAC,GAAG,CAAC,KAAK,EAAE,UAAU,CAAC,CAAA;QAC5C,CAAC;IACH,CAAC;IAED,wEAAwE;IACxE,SAAS,CAAC,KAAa;QACrB,uBAAA,IAAI,oCAAe,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IACnC,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,QAAQ;QACZ,MAAM,SAAS,GAA6C,EAAE,CAAA;QAC9D,MAAM,IAAI,GAAG,MAAM,uBAAA,IAAI,6BAAQ,CAAC,IAAI,EAAE,CAAA;QACtC,0EAA0E;QAC1E,yEAAyE;QACzE,qEAAqE;QACrE,MAAM,UAAU,GAAG,IAAI,GAAG,CAAS,uBAAA,IAAI,oCAAe,CAAC,IAAI,EAAE,CAAC,CAAA;QAC9D,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;YAC3B,MAAM,QAAQ,GAAG,MAAM,uBAAA,IAAI,6BAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YAC3C,IAAI,QAAQ,CAAC,QAAQ,IAAI,uBAAA,IAAI,8BAAS,EAAE,CAAC;gBACvC,SAAS,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAA;gBACjD,uBAAA,IAAI,8BAAS,CAAC,aAAa,CAAC,CAAC,EAAE,WAAW,CAAC,CAAA;gBAC3C,SAAQ;YACV,CAAC;YACD,MAAM,MAAM,GAAG,uBAAA,IAAI,oCAAe,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;YACzC,IAAI,MAAM,KAAK,SAAS,IAAI,uBAAA,IAAI,0BAAK,MAAT,IAAI,CAAO,GAAG,MAAM,IAAI,uBAAA,IAAI,+BAAU,EAAE,CAAC;gBACnE,SAAS,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAA;gBAC3C,uBAAA,IAAI,8BAAS,CAAC,aAAa,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;YACvC,CAAC;QACH,CAAC;QACD,uEAAuE;QACvE,KAAK,IAAI,CAAA;QACT,OAAO,SAAS,CAAA;IAClB,CAAC;IAED;;;OAGG;IACH,YAAY;QACV,KAAK,MAAM,CAAC,IAAI,uBAAA,IAAI,oCAAe,CAAC,IAAI,EAAE,EAAE,CAAC;YAC3C,uBAAA,IAAI,8BAAS,CAAC,aAAa,CAAC,CAAC,EAAE,YAAY,CAAC,CAAA;QAC9C,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,YAAY;QACV,KAAK,MAAM,CAAC,IAAI,uBAAA,IAAI,oCAAe,CAAC,IAAI,EAAE,EAAE,CAAC;YAC3C,uBAAA,IAAI,8BAAS,CAAC,aAAa,CAAC,CAAC,EAAE,YAAY,CAAC,CAAA;QAC9C,CAAC;IACH,CAAC;IAED,2DAA2D;IAC3D,MAAM,CAAC,KAAa;QAClB,OAAO,uBAAA,IAAI,8BAAS,CAAC,aAAa,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;IACrD,CAAC;CACF"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Sync-specific types shared across buffer/flusher/pusher/fetcher/scheduler.
3
+ *
4
+ * Mapper types live under `sync/mapper/types.ts` — this file collects the
5
+ * runtime infrastructure types (buffer entries, flush results, policies).
6
+ */
7
+ /**
8
+ * A raw non-firmware batch of rows for a single table. Emitted by adapters
9
+ * that already speak the warehouse row shape (e.g. custom telemetry).
10
+ */
11
+ export interface SensorBatch {
12
+ table: string;
13
+ brand: string;
14
+ familyId: string;
15
+ userId: string;
16
+ deviceId: string;
17
+ rows: Record<string, unknown>[];
18
+ }
19
+ /**
20
+ * A buffered entry: the row payload plus enough context to route the flush.
21
+ * Rows keep their per-metric context via the mapper output; SensorBatch rows
22
+ * inherit context from the enclosing batch, and we snapshot it at push time.
23
+ */
24
+ export interface BufferEntry {
25
+ rows: Record<string, unknown>[];
26
+ brand: string;
27
+ familyId: string;
28
+ userId: string;
29
+ deviceId: string;
30
+ /** Millis since epoch when the entry entered the buffer — used for age triggers. */
31
+ enqueuedAt: number;
32
+ /** Approximate byte size — used for byte-budget accounting. */
33
+ byteSize: number;
34
+ }
35
+ export type OverflowPolicy = 'drop-oldest' | 'drop-newest' | 'block';
36
+ export interface BufferSize {
37
+ /** Rows currently in the in-memory ring. */
38
+ inMemory: number;
39
+ /** Rows currently persisted in overflow. */
40
+ overflow: number;
41
+ /** Total bytes accounted for in-memory (excludes overflow, which is off-heap). */
42
+ inMemoryBytes: number;
43
+ }
44
+ export interface FlushResult {
45
+ table: string;
46
+ rowsFlushed: number;
47
+ ok: boolean;
48
+ error?: Error;
49
+ }
50
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/sync/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;CAChC;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,oFAAoF;IACpF,UAAU,EAAE,MAAM,CAAA;IAClB,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,CAAA;CACjB;AAID,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,aAAa,GAAG,OAAO,CAAA;AAEpE,MAAM,WAAW,UAAU;IACzB,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAA;IAChB,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAA;IAChB,kFAAkF;IAClF,aAAa,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,EAAE,EAAE,OAAO,CAAA;IACX,KAAK,CAAC,EAAE,KAAK,CAAA;CACd"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Sync-specific types shared across buffer/flusher/pusher/fetcher/scheduler.
3
+ *
4
+ * Mapper types live under `sync/mapper/types.ts` — this file collects the
5
+ * runtime infrastructure types (buffer entries, flush results, policies).
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/sync/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Watermark store (T-16).
3
+ *
4
+ * Two independent cursors per `(brand, familyId, table)` pair:
5
+ * - `push` — highest `ts` we've successfully written to R2. Advances on
6
+ * every successful pusher round.
7
+ * - `fetch` — highest `ts` we've pulled from R2 into the local warehouse.
8
+ * Advances on every successful fetcher round.
9
+ *
10
+ * Key convention (spec §Watermark):
11
+ * `analytics:watermark:{brand}:{familyId}:{table}:{kind}` → ISO string
12
+ *
13
+ * Missing key defaults to `now() - retentionMs`. Default retention is 30 days
14
+ * so a fresh install pushes/fetches at most the last month rather than
15
+ * unbounded history.
16
+ */
17
+ import type { KVStore } from '../core/types';
18
+ export type WatermarkKind = 'push' | 'fetch';
19
+ export interface WatermarkStoreConfig {
20
+ kv: KVStore;
21
+ /** Injected clock for tests. Defaults to `() => new Date()`. */
22
+ now?: () => Date;
23
+ /** Fresh-install horizon. Defaults to 30 days. */
24
+ defaultRetentionMs?: number;
25
+ }
26
+ export declare class WatermarkStore {
27
+ #private;
28
+ constructor(config: WatermarkStoreConfig);
29
+ get(brand: string, familyId: string, table: string, kind: WatermarkKind): Promise<Date>;
30
+ advance(brand: string, familyId: string, table: string, kind: WatermarkKind, ts: Date): Promise<void>;
31
+ }
32
+ //# sourceMappingURL=watermark.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watermark.d.ts","sourceRoot":"","sources":["../../src/sync/watermark.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAK5C,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,CAAA;AAE5C,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,OAAO,CAAA;IACX,gEAAgE;IAChE,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;IAChB,kDAAkD;IAClD,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAWD,qBAAa,cAAc;;gBAKb,MAAM,EAAE,oBAAoB;IAMlC,GAAG,CACP,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC;IASV,OAAO,CACX,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,aAAa,EACnB,EAAE,EAAE,IAAI,GACP,OAAO,CAAC,IAAI,CAAC;CAMjB"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Watermark store (T-16).
3
+ *
4
+ * Two independent cursors per `(brand, familyId, table)` pair:
5
+ * - `push` — highest `ts` we've successfully written to R2. Advances on
6
+ * every successful pusher round.
7
+ * - `fetch` — highest `ts` we've pulled from R2 into the local warehouse.
8
+ * Advances on every successful fetcher round.
9
+ *
10
+ * Key convention (spec §Watermark):
11
+ * `analytics:watermark:{brand}:{familyId}:{table}:{kind}` → ISO string
12
+ *
13
+ * Missing key defaults to `now() - retentionMs`. Default retention is 30 days
14
+ * so a fresh install pushes/fetches at most the last month rather than
15
+ * unbounded history.
16
+ */
17
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
18
+ if (kind === "m") throw new TypeError("Private method is not writable");
19
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
20
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
21
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
22
+ };
23
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
24
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
25
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
26
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
27
+ };
28
+ var _WatermarkStore_kv, _WatermarkStore_now, _WatermarkStore_defaultRetentionMs;
29
+ const KEY_PREFIX = 'analytics:watermark';
30
+ const DEFAULT_RETENTION_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
31
+ function key(brand, familyId, table, kind) {
32
+ return `${KEY_PREFIX}:${brand}:${familyId}:${table}:${kind}`;
33
+ }
34
+ export class WatermarkStore {
35
+ constructor(config) {
36
+ _WatermarkStore_kv.set(this, void 0);
37
+ _WatermarkStore_now.set(this, void 0);
38
+ _WatermarkStore_defaultRetentionMs.set(this, void 0);
39
+ __classPrivateFieldSet(this, _WatermarkStore_kv, config.kv, "f");
40
+ __classPrivateFieldSet(this, _WatermarkStore_now, config.now ?? (() => new Date()), "f");
41
+ __classPrivateFieldSet(this, _WatermarkStore_defaultRetentionMs, config.defaultRetentionMs ?? DEFAULT_RETENTION_MS, "f");
42
+ }
43
+ async get(brand, familyId, table, kind) {
44
+ const iso = await __classPrivateFieldGet(this, _WatermarkStore_kv, "f").get(key(brand, familyId, table, kind));
45
+ if (iso && typeof iso === 'string') {
46
+ const parsed = new Date(iso);
47
+ if (!Number.isNaN(parsed.getTime()))
48
+ return parsed;
49
+ }
50
+ return new Date(__classPrivateFieldGet(this, _WatermarkStore_now, "f").call(this).getTime() - __classPrivateFieldGet(this, _WatermarkStore_defaultRetentionMs, "f"));
51
+ }
52
+ async advance(brand, familyId, table, kind, ts) {
53
+ // Never regress — only accept advances forward.
54
+ const current = await this.get(brand, familyId, table, kind);
55
+ if (ts <= current)
56
+ return;
57
+ await __classPrivateFieldGet(this, _WatermarkStore_kv, "f").set(key(brand, familyId, table, kind), ts.toISOString());
58
+ }
59
+ }
60
+ _WatermarkStore_kv = new WeakMap(), _WatermarkStore_now = new WeakMap(), _WatermarkStore_defaultRetentionMs = new WeakMap();
61
+ //# sourceMappingURL=watermark.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watermark.js","sourceRoot":"","sources":["../../src/sync/watermark.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;;;;;;;;;;;;;AAIH,MAAM,UAAU,GAAG,qBAAqB,CAAA;AACxC,MAAM,oBAAoB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA,CAAC,UAAU;AAYhE,SAAS,GAAG,CACV,KAAa,EACb,QAAgB,EAChB,KAAa,EACb,IAAmB;IAEnB,OAAO,GAAG,UAAU,IAAI,KAAK,IAAI,QAAQ,IAAI,KAAK,IAAI,IAAI,EAAE,CAAA;AAC9D,CAAC;AAED,MAAM,OAAO,cAAc;IAKzB,YAAY,MAA4B;QAJ/B,qCAAY;QACZ,sCAAgB;QAChB,qDAA2B;QAGlC,uBAAA,IAAI,sBAAO,MAAM,CAAC,EAAE,MAAA,CAAA;QACpB,uBAAA,IAAI,uBAAQ,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,MAAA,CAAA;QAC5C,uBAAA,IAAI,sCAAuB,MAAM,CAAC,kBAAkB,IAAI,oBAAoB,MAAA,CAAA;IAC9E,CAAC;IAED,KAAK,CAAC,GAAG,CACP,KAAa,EACb,QAAgB,EAChB,KAAa,EACb,IAAmB;QAEnB,MAAM,GAAG,GAAG,MAAM,uBAAA,IAAI,0BAAI,CAAC,GAAG,CAAS,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAA;QACzE,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;YACnC,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,CAAA;YAC5B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBAAE,OAAO,MAAM,CAAA;QACpD,CAAC;QACD,OAAO,IAAI,IAAI,CAAC,uBAAA,IAAI,2BAAK,MAAT,IAAI,CAAO,CAAC,OAAO,EAAE,GAAG,uBAAA,IAAI,0CAAoB,CAAC,CAAA;IACnE,CAAC;IAED,KAAK,CAAC,OAAO,CACX,KAAa,EACb,QAAgB,EAChB,KAAa,EACb,IAAmB,EACnB,EAAQ;QAER,gDAAgD;QAChD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA;QAC5D,IAAI,EAAE,IAAI,OAAO;YAAE,OAAM;QACzB,MAAM,uBAAA,IAAI,0BAAI,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,WAAW,EAAE,CAAC,CAAA;IACzE,CAAC;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongrov/analytics",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.7",
4
4
  "description": "DuckDB + R2 Iceberg local analytics for @mongrov apps",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -79,12 +79,22 @@
79
79
  "optional": true
80
80
  }
81
81
  },
82
+ "dependencies": {
83
+ "date-fns-tz": "^3.2.0",
84
+ "p-queue": "^8.0.1",
85
+ "xstate": "^5.30.0"
86
+ },
82
87
  "devDependencies": {
83
88
  "@mongrov/db": "workspace:*",
84
89
  "@mongrov/types": "workspace:*",
90
+ "@testing-library/react": "^14.3.1",
91
+ "@types/react": "^18.3.12",
92
+ "@types/react-dom": "^18.3.1",
93
+ "jsdom": "^25.0.1",
94
+ "react": "^18.3.1",
95
+ "react-dom": "^18.3.1",
85
96
  "typescript": "^5.9.3",
86
97
  "vitest": "^2.1.9",
87
- "xstate": "^5.30.0",
88
98
  "zod": "^3.23.8"
89
99
  }
90
100
  }