@hotmeshio/hotmesh 0.14.2 → 0.14.4

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 (32) hide show
  1. package/build/package.json +5 -3
  2. package/build/services/durable/worker.js +4 -0
  3. package/build/services/engine/init.js +1 -1
  4. package/build/services/engine/schema.js +5 -1
  5. package/build/services/mapper/index.d.ts +57 -2
  6. package/build/services/mapper/index.js +57 -2
  7. package/build/services/pipe/index.d.ts +444 -10
  8. package/build/services/pipe/index.js +444 -10
  9. package/build/services/quorum/index.js +1 -1
  10. package/build/services/router/consumption/index.js +20 -2
  11. package/build/services/router/error-handling/index.js +1 -1
  12. package/build/services/store/factory.d.ts +1 -1
  13. package/build/services/store/factory.js +2 -2
  14. package/build/services/store/index.d.ts +1 -1
  15. package/build/services/store/providers/postgres/kvsql.d.ts +11 -1
  16. package/build/services/store/providers/postgres/kvsql.js +22 -12
  17. package/build/services/store/providers/postgres/kvtables.js +39 -6
  18. package/build/services/store/providers/postgres/kvtypes/hash/basic.js +6 -6
  19. package/build/services/store/providers/postgres/kvtypes/hash/scan.js +2 -1
  20. package/build/services/store/providers/postgres/kvtypes/list.js +7 -6
  21. package/build/services/store/providers/postgres/kvtypes/string.js +3 -3
  22. package/build/services/store/providers/postgres/kvtypes/zset.js +7 -7
  23. package/build/services/store/providers/postgres/postgres.d.ts +3 -2
  24. package/build/services/store/providers/postgres/postgres.js +55 -55
  25. package/build/services/store/providers/postgres/time-notify.js +18 -25
  26. package/build/services/store/providers/store-initializable.d.ts +1 -1
  27. package/build/services/stream/registry.d.ts +1 -0
  28. package/build/services/stream/registry.js +12 -8
  29. package/build/services/worker/index.js +3 -1
  30. package/build/types/hotmesh.d.ts +8 -0
  31. package/package.json +5 -3
  32. package/vitest.config.mts +1 -1
@@ -110,16 +110,12 @@ class KVSQL {
110
110
  return '';
111
111
  }
112
112
  /**
113
- * Resolves the table name when provided a key
113
+ * Resolves the table name when provided a key.
114
+ * Public tables (applications, connections) are no longer routed through
115
+ * the KV layer — they use direct SQL in postgres.ts.
114
116
  */
115
117
  tableForKey(key, stats_type) {
116
- if (key === key_1.HMNS) {
117
- return 'public.hotmesh_connections';
118
- }
119
118
  const [_, appName, abbrev, ...rest] = key.split(':');
120
- if (appName === 'a') {
121
- return 'public.hotmesh_applications';
122
- }
123
119
  const id = rest?.length ? rest.join(':') : '';
124
120
  const entity = key_1.KeyService.resolveEntityType(abbrev, id);
125
121
  if (this.safeName(this.appId) !== this.safeName(appName)) {
@@ -145,13 +141,27 @@ class KVSQL {
145
141
  if (entity === 'unknown_entity') {
146
142
  throw new Error(`Unknown entity type abbreviation: ${abbrev}`);
147
143
  }
148
- else if (entity === 'applications') {
149
- return 'public.hotmesh_applications';
150
- }
151
144
  else {
152
145
  return `${schemaName}.${entity}`;
153
146
  }
154
147
  }
148
+ /**
149
+ * Strips the `hmsh:<appId>:<entity>:` prefix from a full Redis-style key,
150
+ * keeping only the meaningful suffix for SQL storage. Applied only to the
151
+ * `key` column in SQL params — never to member values, field names, or values.
152
+ *
153
+ * Excluded tables (jobs, streams, job attributes) retain the full key.
154
+ */
155
+ storageKey(fullKey) {
156
+ const parts = fullKey.split(':');
157
+ if (parts.length < 3)
158
+ return fullKey;
159
+ const entity = parts[2];
160
+ // Excluded tables: jobs (j), streams (x), job attributes (d)
161
+ if (entity === 'j' || entity === 'x' || entity === 'd')
162
+ return fullKey;
163
+ return parts.slice(3).join(':');
164
+ }
155
165
  safeName(input, prefix = '') {
156
166
  if (!input) {
157
167
  return 'connections';
@@ -186,6 +196,7 @@ class KVSQL {
186
196
  AND (expired_at IS NULL OR expired_at > NOW())
187
197
  LIMIT 1;
188
198
  `;
199
+ return { sql, params: [key] };
189
200
  }
190
201
  else {
191
202
  sql = `
@@ -194,9 +205,8 @@ class KVSQL {
194
205
  AND (expiry IS NULL OR expiry > NOW())
195
206
  LIMIT 1;
196
207
  `;
208
+ return { sql, params: [this.storageKey(key)] };
197
209
  }
198
- const params = [key];
199
- return { sql, params };
200
210
  }
201
211
  }
202
212
  exports.KVSQL = KVSQL;
@@ -137,6 +137,39 @@ const KVTables = (context) => ({
137
137
  for (const tableDef of tableDefinitions) {
138
138
  const fullTableName = `${tableDef.schema}.${tableDef.name}`;
139
139
  switch (tableDef.type) {
140
+ case 'relational_app':
141
+ await client.query(`
142
+ CREATE TABLE IF NOT EXISTS ${fullTableName} (
143
+ app_id TEXT PRIMARY KEY,
144
+ version TEXT NOT NULL DEFAULT '1',
145
+ active BOOLEAN DEFAULT TRUE,
146
+ settings JSONB DEFAULT '{}',
147
+ created_at TIMESTAMPTZ DEFAULT NOW(),
148
+ updated_at TIMESTAMPTZ DEFAULT NOW()
149
+ );
150
+ `);
151
+ await client.query(`
152
+ CREATE TABLE IF NOT EXISTS public.hmsh_application_versions (
153
+ app_id TEXT NOT NULL REFERENCES public.hmsh_applications(app_id) ON DELETE CASCADE,
154
+ version TEXT NOT NULL,
155
+ status TEXT NOT NULL DEFAULT 'deployed',
156
+ deployed_at TIMESTAMPTZ DEFAULT NOW(),
157
+ PRIMARY KEY (app_id, version)
158
+ );
159
+ `);
160
+ break;
161
+ case 'relational_connection':
162
+ await client.query(`
163
+ CREATE TABLE IF NOT EXISTS ${fullTableName} (
164
+ guid TEXT NOT NULL,
165
+ app_id TEXT NOT NULL,
166
+ role TEXT NOT NULL,
167
+ version TEXT NOT NULL,
168
+ connected_at TIMESTAMPTZ DEFAULT NOW(),
169
+ PRIMARY KEY (guid, app_id)
170
+ );
171
+ `);
172
+ break;
140
173
  case 'string':
141
174
  await client.query(`
142
175
  CREATE TABLE IF NOT EXISTS ${fullTableName} (
@@ -375,8 +408,8 @@ const KVTables = (context) => ({
375
408
  },
376
409
  getTableNames(appName) {
377
410
  const tableNames = [];
378
- // Applications table (only hotmesh prefix)
379
- tableNames.push('hotmesh_applications', 'hotmesh_connections');
411
+ // Public relational tables
412
+ tableNames.push('public.hmsh_applications', 'public.hmsh_application_versions', 'public.hmsh_connections');
380
413
  // Other tables with appName
381
414
  const tablesWithAppName = [
382
415
  'throttles',
@@ -404,13 +437,13 @@ const KVTables = (context) => ({
404
437
  const tableDefinitions = [
405
438
  {
406
439
  schema: 'public',
407
- name: 'hotmesh_applications',
408
- type: 'hash',
440
+ name: 'hmsh_applications',
441
+ type: 'relational_app',
409
442
  },
410
443
  {
411
444
  schema: 'public',
412
- name: 'hotmesh_connections',
413
- type: 'hash',
445
+ name: 'hmsh_connections',
446
+ type: 'relational_connection',
414
447
  },
415
448
  {
416
449
  schema: schemaName,
@@ -351,7 +351,7 @@ function _hset(context, key, fields, options) {
351
351
  ${conflictAction}
352
352
  RETURNING 1 as count
353
353
  `;
354
- params.unshift(key); // Add key as the first parameter
354
+ params.unshift(context.storageKey(key)); // Add stripped key as the first parameter
355
355
  }
356
356
  return { sql, params };
357
357
  }
@@ -401,7 +401,7 @@ function _hget(context, key, field) {
401
401
  WHERE key = $1 AND field = $2
402
402
  `;
403
403
  const sql = context.appendExpiryClause(baseQuery, tableName);
404
- return { sql, params: [key, field] };
404
+ return { sql, params: [context.storageKey(key), field] };
405
405
  }
406
406
  }
407
407
  exports._hget = _hget;
@@ -438,7 +438,7 @@ function _hdel(context, key, fields) {
438
438
  )
439
439
  SELECT COUNT(*) as count FROM deleted
440
440
  `;
441
- return { sql, params: [key, ...fields] };
441
+ return { sql, params: [context.storageKey(key), ...fields] };
442
442
  }
443
443
  }
444
444
  exports._hdel = _hdel;
@@ -495,7 +495,7 @@ function _hmget(context, key, fields) {
495
495
  AND field = ANY($2::text[])
496
496
  `;
497
497
  const sql = context.appendExpiryClause(baseQuery, tableName);
498
- return { sql, params: [key, fields] };
498
+ return { sql, params: [context.storageKey(key), fields] };
499
499
  }
500
500
  }
501
501
  exports._hmget = _hmget;
@@ -538,7 +538,7 @@ function _hgetall(context, key) {
538
538
  FROM ${tableName}
539
539
  WHERE key = $1
540
540
  `, tableName);
541
- return { sql, params: [key] };
541
+ return { sql, params: [context.storageKey(key)] };
542
542
  }
543
543
  }
544
544
  exports._hgetall = _hgetall;
@@ -582,7 +582,7 @@ function _hincrbyfloat(context, key, field, increment) {
582
582
  SET value = ((COALESCE(${tableName}.value, '0')::double precision + $3::double precision)::text)
583
583
  RETURNING value
584
584
  `;
585
- return { sql, params: [key, field, increment] };
585
+ return { sql, params: [context.storageKey(key), field, increment] };
586
586
  }
587
587
  }
588
588
  exports._hincrbyfloat = _hincrbyfloat;
@@ -68,7 +68,8 @@ function createScanOperations(context) {
68
68
  exports.createScanOperations = createScanOperations;
69
69
  function _hscan(context, key, cursor, count, pattern) {
70
70
  const tableName = context.tableForKey(key, 'hash');
71
- const params = [key];
71
+ const isJobs = tableName.endsWith('jobs');
72
+ const params = [isJobs ? key : context.storageKey(key)];
72
73
  let sql = `
73
74
  SELECT field, value FROM ${tableName}
74
75
  WHERE key = $1 AND (expiry IS NULL OR expiry > NOW())
@@ -44,7 +44,7 @@ const listModule = (context) => ({
44
44
  WHERE rn BETWEEN indices.adjusted_start AND indices.adjusted_end
45
45
  ORDER BY rn ASC
46
46
  `;
47
- const params = [key, start, end];
47
+ const params = [context.storageKey(key), start, end];
48
48
  return { sql, params };
49
49
  },
50
50
  async rpush(key, value, multi) {
@@ -82,7 +82,7 @@ const listModule = (context) => ({
82
82
  )
83
83
  SELECT COUNT(*) as count FROM inserted
84
84
  `;
85
- const params = [key, ...values];
85
+ const params = [context.storageKey(key), ...values];
86
86
  return { sql, params };
87
87
  },
88
88
  async lpush(key, value, multi) {
@@ -120,7 +120,7 @@ const listModule = (context) => ({
120
120
  )
121
121
  SELECT COUNT(*) as count FROM inserted
122
122
  `;
123
- const params = [key, ...values];
123
+ const params = [context.storageKey(key), ...values];
124
124
  return { sql, params };
125
125
  },
126
126
  async lpop(key, multi) {
@@ -146,6 +146,7 @@ const listModule = (context) => ({
146
146
  },
147
147
  _lpop(key) {
148
148
  const tableName = context.tableForKey(key, 'list');
149
+ const sKey = context.storageKey(key);
149
150
  const sql = `
150
151
  DELETE FROM ${tableName}
151
152
  WHERE key = $1 AND "index" = (
@@ -153,7 +154,7 @@ const listModule = (context) => ({
153
154
  )
154
155
  RETURNING value
155
156
  `;
156
- const params = [key];
157
+ const params = [sKey];
157
158
  return { sql, params };
158
159
  },
159
160
  async lmove(source, destination, srcPosition, destPosition, multi) {
@@ -209,7 +210,7 @@ const listModule = (context) => ({
209
210
  )
210
211
  SELECT value FROM inserted
211
212
  `;
212
- const params = [source, destination];
213
+ const params = [context.storageKey(source), context.storageKey(destination)];
213
214
  return { sql, params };
214
215
  },
215
216
  async rename(oldKey, newKey, multi) {
@@ -245,7 +246,7 @@ const listModule = (context) => ({
245
246
  const sql = `
246
247
  UPDATE ${tableName} SET key = $2 WHERE key = $1;
247
248
  `;
248
- const params = [oldKey, newKey];
249
+ const params = [context.storageKey(oldKey), context.storageKey(newKey)];
249
250
  return { sql, params };
250
251
  },
251
252
  });
@@ -30,7 +30,7 @@ const stringModule = (context) => ({
30
30
  WHERE key = $1 AND (expiry IS NULL OR expiry > NOW())
31
31
  LIMIT 1
32
32
  `;
33
- const params = [key];
33
+ const params = [context.storageKey(key)];
34
34
  return { sql, params };
35
35
  },
36
36
  async setnx(key, value, multi) {
@@ -99,7 +99,7 @@ const stringModule = (context) => ({
99
99
  _set(key, value, options) {
100
100
  const tableName = context.tableForKey(key);
101
101
  let sql = '';
102
- const params = [key, value];
102
+ const params = [context.storageKey(key), value];
103
103
  let expiryClause = '';
104
104
  if (options?.ex) {
105
105
  expiryClause = ", expiry = NOW() + INTERVAL '" + options.ex + " seconds'";
@@ -158,7 +158,7 @@ const stringModule = (context) => ({
158
158
  )
159
159
  SELECT COUNT(*) as count FROM deleted
160
160
  `;
161
- const params = [key];
161
+ const params = [context.storageKey(key)];
162
162
  return { sql, params };
163
163
  },
164
164
  });
@@ -26,7 +26,7 @@ const zsetModule = (context) => ({
26
26
  _zadd(key, score, member, options) {
27
27
  const tableName = context.tableForKey(key, 'sorted_set');
28
28
  let sql = '';
29
- const params = [key, member, score];
29
+ const params = [context.storageKey(key), member, score];
30
30
  if (options?.nx) {
31
31
  sql = `
32
32
  INSERT INTO ${tableName} (key, member, score)
@@ -105,7 +105,7 @@ const zsetModule = (context) => ({
105
105
  AND LEAST(GREATEST(adjusted_stop, 0), max_index)
106
106
  ORDER BY rn ASC;
107
107
  `;
108
- const params = [key, start, stop];
108
+ const params = [context.storageKey(key), start, stop];
109
109
  return { sql, params };
110
110
  },
111
111
  async zscore(key, member, multi) {
@@ -142,7 +142,7 @@ const zsetModule = (context) => ({
142
142
  AND (expiry IS NULL OR expiry > NOW())
143
143
  LIMIT 1
144
144
  `;
145
- const params = [key, member];
145
+ const params = [context.storageKey(key), member];
146
146
  return { sql, params };
147
147
  },
148
148
  async zrangebyscore(key, min, max, multi) {
@@ -173,7 +173,7 @@ const zsetModule = (context) => ({
173
173
  WHERE key = $1 AND score BETWEEN $2 AND $3 AND (expiry IS NULL OR expiry > NOW())
174
174
  ORDER BY score ASC, member ASC
175
175
  `;
176
- const params = [key, min, max];
176
+ const params = [context.storageKey(key), min, max];
177
177
  return { sql, params };
178
178
  },
179
179
  async zrangebyscore_withscores(key, min, max, multi) {
@@ -204,7 +204,7 @@ const zsetModule = (context) => ({
204
204
  WHERE key = $1 AND score BETWEEN $2 AND $3 AND (expiry IS NULL OR expiry > NOW())
205
205
  ORDER BY score ASC, member ASC
206
206
  `;
207
- const params = [key, min, max];
207
+ const params = [context.storageKey(key), min, max];
208
208
  return { sql, params };
209
209
  },
210
210
  async zrem(key, member, multi) {
@@ -238,7 +238,7 @@ const zsetModule = (context) => ({
238
238
  )
239
239
  SELECT COUNT(*) as count FROM deleted
240
240
  `;
241
- const params = [key, member];
241
+ const params = [context.storageKey(key), member];
242
242
  return { sql, params };
243
243
  },
244
244
  async zrank(key, member, multi) {
@@ -277,7 +277,7 @@ const zsetModule = (context) => ({
277
277
  WHERE ms.key = $1 AND (expiry IS NULL OR expiry > NOW())
278
278
  AND (ms.score < member_score.score OR (ms.score = member_score.score AND ms.member < $2))
279
279
  `;
280
- const params = [key, member];
280
+ const params = [context.storageKey(key), member];
281
281
  return { sql, params };
282
282
  },
283
283
  });
@@ -21,7 +21,7 @@ declare class PostgresStoreService extends StoreService<ProviderClient, Provider
21
21
  isScout: boolean;
22
22
  transact(): ProviderTransaction;
23
23
  constructor(storeClient: ProviderClient);
24
- init(namespace: string, appId: string, logger: ILogger): Promise<HotMeshApps>;
24
+ init(namespace: string, appId: string, logger: ILogger, guid?: string, role?: string): Promise<HotMeshApps>;
25
25
  isSuccessful(result: any): boolean;
26
26
  delistSignalKey(key: string, target: string): Promise<void>;
27
27
  zAdd(key: string, score: number | string, value: string | number, transaction?: ProviderTransaction): Promise<any>;
@@ -40,8 +40,9 @@ declare class PostgresStoreService extends StoreService<ProviderClient, Provider
40
40
  */
41
41
  reserveScoutRole(scoutType: ScoutType, delay?: number): Promise<boolean>;
42
42
  releaseScoutRole(scoutType: ScoutType): Promise<boolean>;
43
- getSettings(bCreate?: boolean): Promise<HotMeshSettings>;
43
+ getSettings(bCreate?: boolean, guid?: string, role?: string): Promise<HotMeshSettings>;
44
44
  setSettings(manifest: HotMeshSettings): Promise<any>;
45
+ registerConnection(guid: string, role: string, version: string): Promise<void>;
45
46
  reserveSymbolRange(target: string, size: number, type: 'JOB' | 'ACTIVITY', tryCount?: number): Promise<[number, number, Symbols]>;
46
47
  getAllSymbols(): Promise<Symbols>;
47
48
  getSymbols(activityId: string): Promise<Symbols>;
@@ -50,7 +50,7 @@ class PostgresStoreService extends __1.StoreService {
50
50
  //kvTables will provision tables and indexes in the Postgres db as necessary
51
51
  this.kvTables = (0, kvtables_1.KVTables)(this);
52
52
  }
53
- async init(namespace = key_1.HMNS, appId, logger) {
53
+ async init(namespace = key_1.HMNS, appId, logger, guid, role) {
54
54
  //bind appId and namespace to storeClient once initialized
55
55
  // (it uses these values to construct keys for the store)
56
56
  this.storeClient.namespace = this.namespace = namespace;
@@ -61,7 +61,7 @@ class PostgresStoreService extends __1.StoreService {
61
61
  // Deploy time notification triggers
62
62
  await this.deployTimeNotificationTriggers(appId);
63
63
  //note: getSettings will contact db to confirm r/w access
64
- const settings = await this.getSettings(true);
64
+ const settings = await this.getSettings(true, guid, role);
65
65
  this.cache = new cache_1.Cache(appId, settings);
66
66
  this.serializer = new serializer_1.SerializerService();
67
67
  await this.getApp(appId);
@@ -119,7 +119,7 @@ class PostgresStoreService extends __1.StoreService {
119
119
  const success = await this.kvsql().del(key);
120
120
  return this.isSuccessful(success);
121
121
  }
122
- async getSettings(bCreate = false) {
122
+ async getSettings(bCreate = false, guid, role) {
123
123
  let settings = this.cache?.getSettings();
124
124
  if (settings) {
125
125
  return settings;
@@ -129,17 +129,25 @@ class PostgresStoreService extends __1.StoreService {
129
129
  const packageJson = await Promise.resolve().then(() => __importStar(require('../../../../package.json')));
130
130
  const version = packageJson['version'] || '0.0.0';
131
131
  settings = { namespace: key_1.HMNS, version };
132
- await this.setSettings(settings);
132
+ if (guid && role) {
133
+ await this.registerConnection(guid, role, version);
134
+ }
133
135
  return settings;
134
136
  }
135
137
  }
136
138
  throw new Error('settings not found');
137
139
  }
138
140
  async setSettings(manifest) {
139
- //HotMesh heartbeat. If a connection is made, the version will be set
140
- const params = {};
141
- const key = this.mintKey(key_1.KeyType.HOTMESH, params);
142
- return await this.kvsql().hset(key, manifest);
141
+ // No-op for Postgres settings are derived from package.json
142
+ // and connections are registered via registerConnection()
143
+ return;
144
+ }
145
+ async registerConnection(guid, role, version) {
146
+ const sql = `INSERT INTO public.hmsh_connections (guid, app_id, role, version)
147
+ VALUES ($1, $2, $3, $4)
148
+ ON CONFLICT (guid, app_id) DO UPDATE SET
149
+ version = EXCLUDED.version, connected_at = NOW()`;
150
+ await this.pgClient.query(sql, [guid, this.appId, role, version]);
143
151
  }
144
152
  async reserveSymbolRange(target, size, type, tryCount = 1) {
145
153
  const rangeKey = this.mintKey(key_1.KeyType.SYMKEYS, { appId: this.appId });
@@ -294,73 +302,65 @@ class PostgresStoreService extends __1.StoreService {
294
302
  return symKeys;
295
303
  }
296
304
  async getApp(id, refresh = false) {
297
- let app = this.cache.getApp(id);
305
+ let app = this.cache?.getApp(id);
298
306
  if (refresh || !(app && Object.keys(app).length > 0)) {
299
- const params = { appId: id };
300
- const key = this.mintKey(key_1.KeyType.APP, params);
301
- const sApp = await this.kvsql().hgetall(key);
302
- if (!sApp)
307
+ // Fetch from relational tables
308
+ const appResult = await this.pgClient.query(`SELECT app_id, version, active, settings FROM public.hmsh_applications WHERE app_id = $1`, [id]);
309
+ if (!appResult.rows.length)
303
310
  return null;
304
- app = {};
305
- for (const field in sApp) {
306
- try {
307
- if (field === 'active') {
308
- app[field] = sApp[field] === 'true';
309
- }
310
- else {
311
- app[field] = sApp[field];
312
- }
313
- }
314
- catch (e) {
315
- app[field] = sApp[field];
316
- }
311
+ const row = appResult.rows[0];
312
+ app = {
313
+ id: row.app_id,
314
+ version: row.version,
315
+ active: row.active,
316
+ };
317
+ // Fetch version history
318
+ const versionsResult = await this.pgClient.query(`SELECT version, status, deployed_at FROM public.hmsh_application_versions WHERE app_id = $1`, [id]);
319
+ for (const vRow of versionsResult.rows) {
320
+ app[`versions/${vRow.version}`] = `${vRow.status}:${(0, utils_1.formatISODate)(new Date(vRow.deployed_at))}`;
317
321
  }
318
- this.cache.setApp(id, app);
322
+ this.cache?.setApp(id, app);
319
323
  }
320
324
  return app;
321
325
  }
322
326
  async setApp(id, version) {
323
- const params = { appId: id };
324
- const key = this.mintKey(key_1.KeyType.APP, params);
325
- const versionId = `versions/${version}`;
327
+ const now = new Date();
328
+ // Upsert into applications
329
+ await this.pgClient.query(`INSERT INTO public.hmsh_applications (app_id, version, active, updated_at)
330
+ VALUES ($1, $2, TRUE, $3)
331
+ ON CONFLICT (app_id) DO UPDATE SET version = $2, updated_at = $3`, [id, version, now]);
332
+ // Insert version record
333
+ await this.pgClient.query(`INSERT INTO public.hmsh_application_versions (app_id, version, status, deployed_at)
334
+ VALUES ($1, $2, 'deployed', $3)
335
+ ON CONFLICT (app_id, version) DO UPDATE SET status = 'deployed', deployed_at = $3`, [id, version, now]);
326
336
  const payload = {
327
337
  id,
328
338
  version,
329
- [versionId]: `deployed:${(0, utils_1.formatISODate)(new Date())}`,
339
+ [`versions/${version}`]: `deployed:${(0, utils_1.formatISODate)(now)}`,
330
340
  };
331
- await this.kvsql().hset(key, payload);
332
- this.cache.setApp(id, payload);
341
+ this.cache?.setApp(id, payload);
333
342
  return payload;
334
343
  }
335
344
  async activateAppVersion(id, version) {
336
- const params = { appId: id };
337
- const key = this.mintKey(key_1.KeyType.APP, params);
338
- const versionId = `versions/${version}`;
339
345
  const app = await this.getApp(id, true);
346
+ const versionId = `versions/${version}`;
340
347
  if (app && app[versionId]) {
341
- const payload = {
342
- id,
343
- version: version.toString(),
344
- [versionId]: `activated:${(0, utils_1.formatISODate)(new Date())}`,
345
- active: true,
346
- };
347
- Object.entries(payload).forEach(([key, value]) => {
348
- payload[key] = value.toString();
349
- });
350
- await this.kvsql().hset(key, payload);
348
+ const now = new Date();
349
+ await this.pgClient.query(`UPDATE public.hmsh_applications SET active = TRUE, version = $2, updated_at = $3 WHERE app_id = $1`, [id, version, now]);
350
+ await this.pgClient.query(`UPDATE public.hmsh_application_versions SET status = 'activated', deployed_at = $3 WHERE app_id = $1 AND version = $2`, [id, version, now]);
351
351
  return true;
352
352
  }
353
353
  throw new Error(`Version ${version} does not exist for app ${id}`);
354
354
  }
355
355
  async registerAppVersion(appId, version) {
356
- const params = { appId };
357
- const key = this.mintKey(key_1.KeyType.APP, params);
358
- const payload = {
359
- id: appId,
360
- version: version.toString(),
361
- [`versions/${version}`]: (0, utils_1.formatISODate)(new Date()),
362
- };
363
- return await this.kvsql().hset(key, payload);
356
+ const now = new Date();
357
+ await this.pgClient.query(`INSERT INTO public.hmsh_applications (app_id, version, active, updated_at)
358
+ VALUES ($1, $2, TRUE, $3)
359
+ ON CONFLICT (app_id) DO UPDATE SET version = $2, updated_at = $3`, [appId, version, now]);
360
+ await this.pgClient.query(`INSERT INTO public.hmsh_application_versions (app_id, version, status, deployed_at)
361
+ VALUES ($1, $2, 'deployed', $3)
362
+ ON CONFLICT (app_id, version) DO UPDATE SET status = 'deployed', deployed_at = $3`, [appId, version, now]);
363
+ return 1;
364
364
  }
365
365
  async setStats(jobKey, jobId, dateTime, stats, appVersion, transaction) {
366
366
  const params = {
@@ -1252,7 +1252,7 @@ class PostgresStoreService extends __1.StoreService {
1252
1252
  */
1253
1253
  async getNextAwakeningTime() {
1254
1254
  const schemaName = this.kvsql().safeName(this.appId);
1255
- const appKey = `${this.appId}:time_range`;
1255
+ const appKey = '';
1256
1256
  try {
1257
1257
  const result = await this.pgClient.query(`SELECT ${schemaName}.get_next_awakening_time($1) as next_time`, [appKey]);
1258
1258
  if (result.rows[0]?.next_time) {
@@ -20,25 +20,26 @@ DECLARE
20
20
  next_time TIMESTAMP WITH TIME ZONE;
21
21
  BEGIN
22
22
  -- Get the earliest (lowest score) entry from the time range ZSET
23
+ -- After normalization, the key is empty string (prefix stripped)
23
24
  SELECT score INTO next_score
24
25
  FROM ${schema}.task_schedules
25
- WHERE key = app_key
26
+ WHERE key = ''
26
27
  AND (expiry IS NULL OR expiry > NOW())
27
28
  ORDER BY score ASC
28
29
  LIMIT 1;
29
-
30
+
30
31
  IF next_score IS NULL THEN
31
32
  RETURN NULL;
32
33
  END IF;
33
-
34
+
34
35
  -- Convert epoch milliseconds to timestamp
35
36
  next_time := to_timestamp(next_score / 1000.0);
36
-
37
+
37
38
  -- Only return if it's in the future
38
39
  IF next_time > NOW() THEN
39
40
  RETURN next_time;
40
41
  END IF;
41
-
42
+
42
43
  RETURN NULL;
43
44
  END;
44
45
  $$ LANGUAGE plpgsql;
@@ -54,8 +55,8 @@ DECLARE
54
55
  current_next_time TIMESTAMP WITH TIME ZONE;
55
56
  app_key TEXT;
56
57
  BEGIN
57
- -- Build the time range key for this app
58
- app_key := app_id || ':time_range';
58
+ -- After normalization, the key is empty string
59
+ app_key := '';
59
60
  channel_name := 'time_hooks_' || app_id;
60
61
 
61
62
  -- Get the current next awakening time
@@ -101,18 +102,14 @@ $$ LANGUAGE plpgsql;
101
102
  CREATE OR REPLACE FUNCTION ${schema}.on_time_hook_change()
102
103
  RETURNS TRIGGER AS $$
103
104
  DECLARE
104
- app_id_extracted TEXT;
105
105
  awakening_time TIMESTAMP WITH TIME ZONE;
106
106
  BEGIN
107
- -- Extract app_id from the key (assumes format: app_id:time_range)
108
- app_id_extracted := split_part(NEW.key, ':time_range', 1);
109
-
110
107
  -- Convert the score (epoch milliseconds) to timestamp
111
108
  awakening_time := to_timestamp(NEW.score / 1000.0);
112
-
113
- -- Schedule notification for this new awakening time
114
- PERFORM ${schema}.schedule_time_notification(app_id_extracted, awakening_time);
115
-
109
+
110
+ -- Schedule notification (app_id is the schema name)
111
+ PERFORM ${schema}.schedule_time_notification('${schema}', awakening_time);
112
+
116
113
  RETURN NEW;
117
114
  END;
118
115
  $$ LANGUAGE plpgsql;
@@ -120,15 +117,10 @@ $$ LANGUAGE plpgsql;
120
117
  -- Trigger function for when time hooks are removed
121
118
  CREATE OR REPLACE FUNCTION ${schema}.on_time_hook_remove()
122
119
  RETURNS TRIGGER AS $$
123
- DECLARE
124
- app_id_extracted TEXT;
125
120
  BEGIN
126
- -- Extract app_id from the key
127
- app_id_extracted := split_part(OLD.key, ':time_range', 1);
128
-
129
121
  -- Recalculate and notify about the schedule update
130
- PERFORM ${schema}.schedule_time_notification(app_id_extracted);
131
-
122
+ PERFORM ${schema}.schedule_time_notification('${schema}');
123
+
132
124
  RETURN OLD;
133
125
  END;
134
126
  $$ LANGUAGE plpgsql;
@@ -141,22 +133,23 @@ DROP TRIGGER IF EXISTS trg_time_hook_update ON ${schema}.task_schedules;
141
133
  DROP TRIGGER IF EXISTS trg_time_hook_delete ON ${schema}.task_schedules;
142
134
 
143
135
  -- Create new triggers
136
+ -- After normalization, the task_schedules key for time range is empty string
144
137
  CREATE TRIGGER trg_time_hook_insert
145
138
  AFTER INSERT ON ${schema}.task_schedules
146
139
  FOR EACH ROW
147
- WHEN (NEW.key LIKE '%:time_range')
140
+ WHEN (NEW.key = '')
148
141
  EXECUTE FUNCTION ${schema}.on_time_hook_change();
149
142
 
150
143
  CREATE TRIGGER trg_time_hook_update
151
144
  AFTER UPDATE ON ${schema}.task_schedules
152
145
  FOR EACH ROW
153
- WHEN (NEW.key LIKE '%:time_range')
146
+ WHEN (NEW.key = '')
154
147
  EXECUTE FUNCTION ${schema}.on_time_hook_change();
155
148
 
156
149
  CREATE TRIGGER trg_time_hook_delete
157
150
  AFTER DELETE ON ${schema}.task_schedules
158
151
  FOR EACH ROW
159
- WHEN (OLD.key LIKE '%:time_range')
152
+ WHEN (OLD.key = '')
160
153
  EXECUTE FUNCTION ${schema}.on_time_hook_remove();
161
154
  `;
162
155
  }
@@ -1,5 +1,5 @@
1
1
  import { ILogger } from '../../logger';
2
2
  import { HotMeshApps } from '../../../types/hotmesh';
3
3
  export interface StoreInitializable {
4
- init(namespace: string, appId: string, logger: ILogger): Promise<HotMeshApps>;
4
+ init(namespace: string, appId: string, logger: ILogger, guid?: string, role?: string): Promise<HotMeshApps>;
5
5
  }