@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.
- package/build/package.json +5 -3
- package/build/services/durable/worker.js +4 -0
- package/build/services/engine/init.js +1 -1
- package/build/services/engine/schema.js +5 -1
- package/build/services/mapper/index.d.ts +57 -2
- package/build/services/mapper/index.js +57 -2
- package/build/services/pipe/index.d.ts +444 -10
- package/build/services/pipe/index.js +444 -10
- package/build/services/quorum/index.js +1 -1
- package/build/services/router/consumption/index.js +20 -2
- package/build/services/router/error-handling/index.js +1 -1
- package/build/services/store/factory.d.ts +1 -1
- package/build/services/store/factory.js +2 -2
- package/build/services/store/index.d.ts +1 -1
- package/build/services/store/providers/postgres/kvsql.d.ts +11 -1
- package/build/services/store/providers/postgres/kvsql.js +22 -12
- package/build/services/store/providers/postgres/kvtables.js +39 -6
- package/build/services/store/providers/postgres/kvtypes/hash/basic.js +6 -6
- package/build/services/store/providers/postgres/kvtypes/hash/scan.js +2 -1
- package/build/services/store/providers/postgres/kvtypes/list.js +7 -6
- package/build/services/store/providers/postgres/kvtypes/string.js +3 -3
- package/build/services/store/providers/postgres/kvtypes/zset.js +7 -7
- package/build/services/store/providers/postgres/postgres.d.ts +3 -2
- package/build/services/store/providers/postgres/postgres.js +55 -55
- package/build/services/store/providers/postgres/time-notify.js +18 -25
- package/build/services/store/providers/store-initializable.d.ts +1 -1
- package/build/services/stream/registry.d.ts +1 -0
- package/build/services/stream/registry.js +12 -8
- package/build/services/worker/index.js +3 -1
- package/build/types/hotmesh.d.ts +8 -0
- package/package.json +5 -3
- 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
|
-
//
|
|
379
|
-
tableNames.push('
|
|
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: '
|
|
408
|
-
type: '
|
|
440
|
+
name: 'hmsh_applications',
|
|
441
|
+
type: 'relational_app',
|
|
409
442
|
},
|
|
410
443
|
{
|
|
411
444
|
schema: 'public',
|
|
412
|
-
name: '
|
|
413
|
-
type: '
|
|
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
|
|
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 = [
|
|
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
|
-
|
|
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
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
305
|
+
let app = this.cache?.getApp(id);
|
|
298
306
|
if (refresh || !(app && Object.keys(app).length > 0)) {
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
|
322
|
+
this.cache?.setApp(id, app);
|
|
319
323
|
}
|
|
320
324
|
return app;
|
|
321
325
|
}
|
|
322
326
|
async setApp(id, version) {
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
[
|
|
339
|
+
[`versions/${version}`]: `deployed:${(0, utils_1.formatISODate)(now)}`,
|
|
330
340
|
};
|
|
331
|
-
|
|
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
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
return
|
|
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 =
|
|
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 =
|
|
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
|
-
--
|
|
58
|
-
app_key :=
|
|
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
|
|
114
|
-
PERFORM ${schema}.schedule_time_notification(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|