@lde/distribution-monitor 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/schema.d.ts CHANGED
@@ -99,102 +99,107 @@ export declare const observations: import("drizzle-orm/pg-core").PgTableWithColu
99
99
  dialect: "pg";
100
100
  }>;
101
101
  /**
102
- * SQL for refreshing the materialized view.
102
+ * Latest observation per monitor, keyed by `monitor`. Kept current by an upsert
103
+ * on every write (see `PostgresObservationStore.store`) rather than a
104
+ * materialized view: reads are always current, and there is no periodic
105
+ * full-table `REFRESH` to fall behind, contend on locks, or scan the whole
106
+ * `observations` history every cycle.
103
107
  */
104
- export declare const refreshLatestObservationsViewSql: import("drizzle-orm").SQL<unknown>;
105
- /**
106
- * Materialized view for the latest observation per monitor.
107
- */
108
- export declare const latestObservations: import("drizzle-orm/pg-core").PgMaterializedViewWithSelection<"latest_observations", false, {
109
- id: import("drizzle-orm/pg-core").PgBuildColumn<"latest_observations", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgUUIDBuilder>, {
110
- name: string;
111
- tableName: "latest_observations";
112
- dataType: "string uuid";
113
- data: string;
114
- driverParam: string;
115
- notNull: true;
116
- hasDefault: false;
117
- isPrimaryKey: false;
118
- isAutoincrement: false;
119
- hasRuntimeDefault: false;
120
- enumValues: undefined;
121
- identity: undefined;
122
- generated: undefined;
123
- }>;
124
- monitor: import("drizzle-orm/pg-core").PgBuildColumn<"latest_observations", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>>, {
125
- name: string;
126
- tableName: "latest_observations";
127
- dataType: "string";
128
- data: string;
129
- driverParam: string;
130
- notNull: true;
131
- hasDefault: false;
132
- isPrimaryKey: false;
133
- isAutoincrement: false;
134
- hasRuntimeDefault: false;
135
- enumValues: undefined;
136
- identity: undefined;
137
- generated: undefined;
138
- }>;
139
- observedAt: import("drizzle-orm/pg-core").PgBuildColumn<"latest_observations", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTimestampBuilder>, {
140
- name: string;
141
- tableName: "latest_observations";
142
- dataType: "object date";
143
- data: Date;
144
- driverParam: string;
145
- notNull: true;
146
- hasDefault: false;
147
- isPrimaryKey: false;
148
- isAutoincrement: false;
149
- hasRuntimeDefault: false;
150
- enumValues: undefined;
151
- identity: undefined;
152
- generated: undefined;
153
- }>;
154
- success: import("drizzle-orm/pg-core").PgBuildColumn<"latest_observations", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgBooleanBuilder>, {
155
- name: string;
156
- tableName: "latest_observations";
157
- dataType: "boolean";
158
- data: boolean;
159
- driverParam: boolean;
160
- notNull: true;
161
- hasDefault: false;
162
- isPrimaryKey: false;
163
- isAutoincrement: false;
164
- hasRuntimeDefault: false;
165
- enumValues: undefined;
166
- identity: undefined;
167
- generated: undefined;
168
- }>;
169
- responseTimeMs: import("drizzle-orm/pg-core").PgBuildColumn<"latest_observations", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgIntegerBuilder>, {
170
- name: string;
171
- tableName: "latest_observations";
172
- dataType: "number int32";
173
- data: number;
174
- driverParam: string | number;
175
- notNull: true;
176
- hasDefault: false;
177
- isPrimaryKey: false;
178
- isAutoincrement: false;
179
- hasRuntimeDefault: false;
180
- enumValues: undefined;
181
- identity: undefined;
182
- generated: undefined;
183
- }>;
184
- errorMessage: import("drizzle-orm/pg-core").PgBuildColumn<"latest_observations", import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>, {
185
- name: string;
186
- tableName: "latest_observations";
187
- dataType: "string";
188
- data: string;
189
- driverParam: string;
190
- notNull: false;
191
- hasDefault: false;
192
- isPrimaryKey: false;
193
- isAutoincrement: false;
194
- hasRuntimeDefault: false;
195
- enumValues: undefined;
196
- identity: undefined;
197
- generated: undefined;
198
- }>;
108
+ export declare const latestObservations: import("drizzle-orm/pg-core").PgTableWithColumns<{
109
+ name: "latest_observations";
110
+ schema: undefined;
111
+ columns: {
112
+ id: import("drizzle-orm/pg-core").PgBuildColumn<"latest_observations", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgUUIDBuilder>, {
113
+ name: string;
114
+ tableName: "latest_observations";
115
+ dataType: "string uuid";
116
+ data: string;
117
+ driverParam: string;
118
+ notNull: true;
119
+ hasDefault: false;
120
+ isPrimaryKey: false;
121
+ isAutoincrement: false;
122
+ hasRuntimeDefault: false;
123
+ enumValues: undefined;
124
+ identity: undefined;
125
+ generated: undefined;
126
+ }>;
127
+ monitor: import("drizzle-orm/pg-core").PgBuildColumn<"latest_observations", import("drizzle-orm/pg-core").SetIsPrimaryKey<import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>>, {
128
+ name: string;
129
+ tableName: "latest_observations";
130
+ dataType: "string";
131
+ data: string;
132
+ driverParam: string;
133
+ notNull: true;
134
+ hasDefault: false;
135
+ isPrimaryKey: false;
136
+ isAutoincrement: false;
137
+ hasRuntimeDefault: false;
138
+ enumValues: undefined;
139
+ identity: undefined;
140
+ generated: undefined;
141
+ }>;
142
+ observedAt: import("drizzle-orm/pg-core").PgBuildColumn<"latest_observations", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTimestampBuilder>, {
143
+ name: string;
144
+ tableName: "latest_observations";
145
+ dataType: "object date";
146
+ data: Date;
147
+ driverParam: string;
148
+ notNull: true;
149
+ hasDefault: false;
150
+ isPrimaryKey: false;
151
+ isAutoincrement: false;
152
+ hasRuntimeDefault: false;
153
+ enumValues: undefined;
154
+ identity: undefined;
155
+ generated: undefined;
156
+ }>;
157
+ success: import("drizzle-orm/pg-core").PgBuildColumn<"latest_observations", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgBooleanBuilder>, {
158
+ name: string;
159
+ tableName: "latest_observations";
160
+ dataType: "boolean";
161
+ data: boolean;
162
+ driverParam: boolean;
163
+ notNull: true;
164
+ hasDefault: false;
165
+ isPrimaryKey: false;
166
+ isAutoincrement: false;
167
+ hasRuntimeDefault: false;
168
+ enumValues: undefined;
169
+ identity: undefined;
170
+ generated: undefined;
171
+ }>;
172
+ responseTimeMs: import("drizzle-orm/pg-core").PgBuildColumn<"latest_observations", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgIntegerBuilder>, {
173
+ name: string;
174
+ tableName: "latest_observations";
175
+ dataType: "number int32";
176
+ data: number;
177
+ driverParam: string | number;
178
+ notNull: true;
179
+ hasDefault: false;
180
+ isPrimaryKey: false;
181
+ isAutoincrement: false;
182
+ hasRuntimeDefault: false;
183
+ enumValues: undefined;
184
+ identity: undefined;
185
+ generated: undefined;
186
+ }>;
187
+ errorMessage: import("drizzle-orm/pg-core").PgBuildColumn<"latest_observations", import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>, {
188
+ name: string;
189
+ tableName: "latest_observations";
190
+ dataType: "string";
191
+ data: string;
192
+ driverParam: string;
193
+ notNull: false;
194
+ hasDefault: false;
195
+ isPrimaryKey: false;
196
+ isAutoincrement: false;
197
+ hasRuntimeDefault: false;
198
+ enumValues: undefined;
199
+ identity: undefined;
200
+ generated: undefined;
201
+ }>;
202
+ };
203
+ dialect: "pg";
199
204
  }>;
200
205
  //# sourceMappingURL=schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAqBA;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAexB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,gCAAgC,oCAE5C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAO7B,CAAC"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAoBA;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAexB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAU7B,CAAC"}
package/dist/schema.js CHANGED
@@ -1,4 +1,4 @@
1
- import { boolean, index, integer, pgMaterializedView, pgTable, text, timestamp, uuid, } from 'drizzle-orm/pg-core';
1
+ import { boolean, index, integer, pgTable, text, timestamp, uuid, } from 'drizzle-orm/pg-core';
2
2
  import { sql } from 'drizzle-orm';
3
3
  const columns = {
4
4
  id: uuid('id').notNull(),
@@ -21,16 +21,20 @@ export const observations = pgTable('observations', {
21
21
  index('observations_monitor_observed_at_idx').on(table.monitor, sql `${table.observedAt} DESC`),
22
22
  ]);
23
23
  /**
24
- * SQL for refreshing the materialized view.
24
+ * Latest observation per monitor, keyed by `monitor`. Kept current by an upsert
25
+ * on every write (see `PostgresObservationStore.store`) rather than a
26
+ * materialized view: reads are always current, and there is no periodic
27
+ * full-table `REFRESH` to fall behind, contend on locks, or scan the whole
28
+ * `observations` history every cycle.
25
29
  */
26
- export const refreshLatestObservationsViewSql = sql `
27
- REFRESH MATERIALIZED VIEW CONCURRENTLY latest_observations
28
- `;
29
- /**
30
- * Materialized view for the latest observation per monitor.
31
- */
32
- export const latestObservations = pgMaterializedView('latest_observations', columns).as(sql `
33
- SELECT DISTINCT ON (monitor) *
34
- FROM ${observations}
35
- ORDER BY monitor, observed_at DESC
36
- `);
30
+ export const latestObservations = pgTable('latest_observations', {
31
+ // Fresh column builders (not the shared `columns`): drizzle binds a builder
32
+ // instance to one table, so reusing `columns` across two tables would drop
33
+ // this primary key — which `store`'s upsert relies on for ON CONFLICT.
34
+ id: uuid('id').notNull(),
35
+ monitor: text('monitor').primaryKey(),
36
+ observedAt: timestamp('observed_at', { mode: 'date' }).notNull(),
37
+ success: boolean('success').notNull(),
38
+ responseTimeMs: integer('response_time_ms').notNull(),
39
+ errorMessage: text('error_message'),
40
+ });
package/dist/service.d.ts CHANGED
@@ -66,7 +66,6 @@ export declare class MonitorService {
66
66
  */
67
67
  private secondsToCron;
68
68
  private performCheck;
69
- private refreshView;
70
69
  }
71
70
  /**
72
71
  * Collapse a {@link ProbeResultType} into a {@link CheckResult}. Network
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,EAEL,KAAK,eAAe,EAErB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE/E;;;GAGG;AACH,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC;AAEjC,MAAM,WAAW,qBAAqB;IACpC,yCAAyC;IACzC,KAAK,EAAE,gBAAgB,CAAC;IACxB,8BAA8B;IAC9B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,kDAAkD;IAClD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAID;;GAEG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmB;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAC1C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAU;IACnC,OAAO,CAAC,GAAG,CAAwB;gBAEvB,OAAO,EAAE,qBAAqB;IAU1C;;OAEG;IACG,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASjD;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAK/B;;OAEG;IACH,KAAK,IAAI,IAAI;IAQb;;OAEG;IACH,IAAI,IAAI,IAAI;IAOZ;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;OAEG;IACH,OAAO,CAAC,aAAa;YAYP,YAAY;YAYZ,WAAW;CAO1B;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,eAAe,EACvB,UAAU,EAAE,IAAI,GACf,WAAW,CA+Bb"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,EAEL,KAAK,eAAe,EAErB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE/E;;;GAGG;AACH,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC;AAEjC,MAAM,WAAW,qBAAqB;IACpC,yCAAyC;IACzC,KAAK,EAAE,gBAAgB,CAAC;IACxB,8BAA8B;IAC9B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,kDAAkD;IAClD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAID;;GAEG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmB;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAC1C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAU;IACnC,OAAO,CAAC,GAAG,CAAwB;gBAEvB,OAAO,EAAE,qBAAqB;IAU1C;;OAEG;IACG,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQjD;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAI/B;;OAEG;IACH,KAAK,IAAI,IAAI;IAQb;;OAEG;IACH,IAAI,IAAI,IAAI;IAOZ;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;OAEG;IACH,OAAO,CAAC,aAAa;YAYP,YAAY;CAW3B;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,eAAe,EACvB,UAAU,EAAE,IAAI,GACf,WAAW,CA+Bb"}
package/dist/service.js CHANGED
@@ -31,14 +31,12 @@ export class MonitorService {
31
31
  throw new Error(`Monitor not found: ${identifier}`);
32
32
  }
33
33
  await this.performCheck(config);
34
- await this.refreshView();
35
34
  }
36
35
  /**
37
36
  * Perform an immediate check for all monitors.
38
37
  */
39
38
  async checkAll() {
40
39
  await Promise.all(this.configs.map((config) => this.performCheck(config)));
41
- await this.refreshView();
42
40
  }
43
41
  /**
44
42
  * Start monitoring all configured distributions.
@@ -92,14 +90,6 @@ export class MonitorService {
92
90
  const checkResult = mapProbeResult(result, observedAt);
93
91
  await this.store.store({ monitor: config.identifier, ...checkResult });
94
92
  }
95
- async refreshView() {
96
- try {
97
- await this.store.refreshLatestObservationsView();
98
- }
99
- catch {
100
- // View refresh failure is not critical
101
- }
102
- }
103
93
  }
104
94
  /**
105
95
  * Collapse a {@link ProbeResultType} into a {@link CheckResult}. Network
package/dist/store.d.ts CHANGED
@@ -22,6 +22,5 @@ export declare class PostgresObservationStore implements ObservationStore {
22
22
  getLatest(): Promise<Map<string, Observation>>;
23
23
  get(id: string): Promise<Observation | null>;
24
24
  store(observation: Omit<Observation, 'id'>): Promise<Observation>;
25
- refreshLatestObservationsView(): Promise<void>;
26
25
  }
27
26
  //# sourceMappingURL=store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAgBhE;;GAEG;AACH,qBAAa,wBAAyB,YAAW,gBAAgB;IAC/D,OAAO,CAAC,EAAE,CAAqB;IAE/B,OAAO;IAYP;;;;;;;;;;;OAWG;WACU,MAAM,CACjB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,wBAAwB,CAAC;IA6C9B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAKtB,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAK9C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAS5C,KAAK,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC;IAQjE,6BAA6B,IAAI,OAAO,CAAC,IAAI,CAAC;CAGrD"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAchE;;GAEG;AACH,qBAAa,wBAAyB,YAAW,gBAAgB;IAC/D,OAAO,CAAC,EAAE,CAAqB;IAE/B,OAAO;IAYP;;;;;;;;;;;OAWG;WACU,MAAM,CACjB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,wBAAwB,CAAC;IAqD9B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAKtB,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAK9C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAS5C,KAAK,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC;CA4BxE"}
package/dist/store.js CHANGED
@@ -1,14 +1,13 @@
1
1
  import { drizzle } from 'drizzle-orm/postgres-js';
2
2
  import { eq, sql } from 'drizzle-orm';
3
3
  import * as schema from './schema.js';
4
- const { observations, latestObservations, refreshLatestObservationsViewSql } = schema;
4
+ const { observations, latestObservations } = schema;
5
5
  /**
6
- * Per-session Postgres timeouts applied to every pooled connection. A statement
7
- * that cannot acquire its lock within `lock_timeout` most importantly the
8
- * periodic `REFRESH MATERIALIZED VIEW CONCURRENTLY`, and the boot-time index
9
- * check fails fast and is retried on the next cycle, rather than hanging
10
- * indefinitely on a contended lock (which once stalled startup for hours).
11
- * `statement_timeout` is a generous backstop against a single runaway query.
6
+ * Per-session Postgres timeouts applied to every pooled connection, so a
7
+ * statement that blocks on a lock or runs away fails fast rather than hanging
8
+ * indefinitely (which once stalled startup for hours). `lock_timeout` bounds
9
+ * lock waits; `statement_timeout` is a generous backstop against a single
10
+ * runaway query.
12
11
  */
13
12
  const LOCK_TIMEOUT_MS = 30_000;
14
13
  const STATEMENT_TIMEOUT_MS = 60_000;
@@ -42,6 +41,21 @@ export class PostgresObservationStore {
42
41
  */
43
42
  static async create(connectionString) {
44
43
  const store = new PostgresObservationStore(connectionString);
44
+ // Migrate away from the former `latest_observations` MATERIALIZED VIEW: it
45
+ // was a derived cache (every value also lives in `observations`), so dropping
46
+ // it loses nothing. Done before pushSchema so the declarative push can create
47
+ // the replacement table without tripping the destructive-change guard below.
48
+ // Guarded on pg_matviews, so it is a no-op once the table has taken over.
49
+ await store.db.execute(sql `
50
+ DO $$
51
+ BEGIN
52
+ IF EXISTS (
53
+ SELECT 1 FROM pg_matviews WHERE matviewname = 'latest_observations'
54
+ ) THEN
55
+ DROP MATERIALIZED VIEW latest_observations;
56
+ END IF;
57
+ END $$;
58
+ `);
45
59
  const { pushSchema } = await import('drizzle-kit/api-postgres');
46
60
  // #5293 shim: drizzle-kit reads `.rows` off the execute() result, which the
47
61
  // postgres-js driver returns as a bare array. Wrap it back into `{ rows }`.
@@ -62,12 +76,6 @@ export class PostgresObservationStore {
62
76
  for (const statement of sqlStatements) {
63
77
  await store.db.execute(sql.raw(statement));
64
78
  }
65
- // The unique index on the materialized view is required for
66
- // REFRESH ... CONCURRENTLY but is not modelled by drizzle's
67
- // `pgMaterializedView`, so `pushSchema` never emits it. Create it
68
- // idempotently — `IF NOT EXISTS` matches by name and skips (no rebuild) when
69
- // it already exists.
70
- await store.db.execute(sql `CREATE UNIQUE INDEX IF NOT EXISTS latest_observations_monitor_idx ON latest_observations (monitor)`);
71
79
  return store;
72
80
  }
73
81
  async close() {
@@ -87,13 +95,29 @@ export class PostgresObservationStore {
87
95
  return rows[0] ?? null;
88
96
  }
89
97
  async store(observation) {
90
- const rows = await this.db
91
- .insert(observations)
92
- .values(observation)
93
- .returning();
94
- return rows[0];
95
- }
96
- async refreshLatestObservationsView() {
97
- await this.db.execute(refreshLatestObservationsViewSql);
98
+ return this.db.transaction(async (tx) => {
99
+ const [inserted] = await tx
100
+ .insert(observations)
101
+ .values(observation)
102
+ .returning();
103
+ // Keep the latest-per-monitor row current in the same transaction. The
104
+ // `setWhere` guard skips the update when a newer observation is already
105
+ // recorded, so an out-of-order write can never move `latest` backwards.
106
+ await tx
107
+ .insert(latestObservations)
108
+ .values(inserted)
109
+ .onConflictDoUpdate({
110
+ target: latestObservations.monitor,
111
+ set: {
112
+ id: inserted.id,
113
+ observedAt: inserted.observedAt,
114
+ success: inserted.success,
115
+ responseTimeMs: inserted.responseTimeMs,
116
+ errorMessage: inserted.errorMessage,
117
+ },
118
+ setWhere: sql `${latestObservations.observedAt} <= excluded.observed_at`,
119
+ });
120
+ return inserted;
121
+ });
98
122
  }
99
123
  }
package/dist/types.d.ts CHANGED
@@ -56,13 +56,9 @@ export interface ObservationStore {
56
56
  */
57
57
  get(id: string): Promise<Observation | null>;
58
58
  /**
59
- * Save a new observation.
59
+ * Save a new observation. Also updates the latest-per-monitor record.
60
60
  */
61
61
  store(observation: Omit<Observation, 'id'>): Promise<Observation>;
62
- /**
63
- * Refresh the latest_observations materialized view.
64
- */
65
- refreshLatestObservationsView(): Promise<void>;
66
62
  /**
67
63
  * Close the database connection.
68
64
  */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,YAAY,EAAE,YAAY,CAAC;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,uDAAuD;IACvD,OAAO,EAAE,OAAO,CAAC;IACjB,qCAAqC;IACrC,cAAc,EAAE,MAAM,CAAC;IACvB,yCAAyC;IACzC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,sDAAsD;IACtD,UAAU,EAAE,IAAI,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,IAAI,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;IAE/C;;OAEG;IACH,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAE7C;;OAEG;IACH,KAAK,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAElE;;OAEG;IACH,6BAA6B,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/C;;OAEG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,YAAY,EAAE,YAAY,CAAC;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,uDAAuD;IACvD,OAAO,EAAE,OAAO,CAAC;IACjB,qCAAqC;IACrC,cAAc,EAAE,MAAM,CAAC;IACvB,yCAAyC;IACzC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,sDAAsD;IACtD,UAAU,EAAE,IAAI,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,IAAI,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;IAE/C;;OAEG;IACH,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAE7C;;OAEG;IACH,KAAK,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAElE;;OAEG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/distribution-monitor",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Monitor DCAT distributions (SPARQL endpoints and data dumps) with periodic probes",
5
5
  "repository": {
6
6
  "url": "git+https://github.com/ldelements/lde.git",