@kronos-ts/knex 0.1.0
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/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/knex-token-store.d.ts +27 -0
- package/dist/knex-token-store.d.ts.map +1 -0
- package/dist/knex-token-store.js +124 -0
- package/dist/knex-token-store.js.map +1 -0
- package/dist/knex-transaction-manager.d.ts +31 -0
- package/dist/knex-transaction-manager.d.ts.map +1 -0
- package/dist/knex-transaction-manager.js +59 -0
- package/dist/knex-transaction-manager.js.map +1 -0
- package/package.json +57 -0
- package/src/index.ts +10 -0
- package/src/knex-token-store.ts +158 -0
- package/src/knex-transaction-manager.ts +78 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,+BAA+B,CAAA;AAEtC,OAAO,EACL,cAAc,EACd,KAAK,aAAa,GACnB,MAAM,uBAAuB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,GAGvB,MAAM,+BAA+B,CAAA;AAEtC,OAAO,EACL,cAAc,GAEf,MAAM,uBAAuB,CAAA"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { TokenStore } from "@kronos-ts/messaging";
|
|
2
|
+
/**
|
|
3
|
+
* A Knex instance or transaction with query builder methods.
|
|
4
|
+
*/
|
|
5
|
+
export interface KnexQueryable {
|
|
6
|
+
(tableName: string): any;
|
|
7
|
+
raw(sql: string, ...bindings: any[]): any;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Creates a TokenStore backed by Knex.
|
|
11
|
+
*
|
|
12
|
+
* Participates in the active transaction via `getActiveTransaction()`.
|
|
13
|
+
*
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { knexTokenStore } from "@kronos-ts/knex"
|
|
16
|
+
*
|
|
17
|
+
* // tokenStore wiring to a kronos() App is pending a typed `tokenStore` slot
|
|
18
|
+
* // (Phase 9). For now, construct the store and pass it directly to the
|
|
19
|
+
* // tracking processor that owns it:
|
|
20
|
+
* const tokenStore = knexTokenStore(knex)
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function knexTokenStore(knex: KnexQueryable, options?: {
|
|
24
|
+
claimTimeoutMs?: number;
|
|
25
|
+
tableName?: string;
|
|
26
|
+
}): TokenStore;
|
|
27
|
+
//# sourceMappingURL=knex-token-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"knex-token-store.d.ts","sourceRoot":"","sources":["../src/knex-token-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAiB,MAAM,sBAAsB,CAAA;AAqBrE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,CAAC,SAAS,EAAE,MAAM,GAAG,GAAG,CAAA;IACxB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,GAAG,EAAE,GAAG,GAAG,CAAA;CAC1C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,aAAa,EACnB,OAAO,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACxD,UAAU,CA+GZ"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { getActiveTransaction, UnableToClaimTokenError, globalSequenceToken } from "@kronos-ts/messaging";
|
|
2
|
+
function serializeToken(token) {
|
|
3
|
+
return {
|
|
4
|
+
token_type: "GlobalSequenceToken",
|
|
5
|
+
token: JSON.stringify({ position: token.position().toString() }),
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function deserializeToken(tokenType, token) {
|
|
9
|
+
if (!token || !tokenType)
|
|
10
|
+
return undefined;
|
|
11
|
+
const data = JSON.parse(token);
|
|
12
|
+
return globalSequenceToken(BigInt(data.position));
|
|
13
|
+
}
|
|
14
|
+
function nowIso() {
|
|
15
|
+
return new Date().toISOString();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Creates a TokenStore backed by Knex.
|
|
19
|
+
*
|
|
20
|
+
* Participates in the active transaction via `getActiveTransaction()`.
|
|
21
|
+
*
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import { knexTokenStore } from "@kronos-ts/knex"
|
|
24
|
+
*
|
|
25
|
+
* // tokenStore wiring to a kronos() App is pending a typed `tokenStore` slot
|
|
26
|
+
* // (Phase 9). For now, construct the store and pass it directly to the
|
|
27
|
+
* // tracking processor that owns it:
|
|
28
|
+
* const tokenStore = knexTokenStore(knex)
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function knexTokenStore(knex, options) {
|
|
32
|
+
const claimTimeoutMs = options?.claimTimeoutMs ?? 10000;
|
|
33
|
+
const table = options?.tableName ?? "kronos_token_entries";
|
|
34
|
+
function getKnex() {
|
|
35
|
+
return getActiveTransaction() ?? knex;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
async store(processorName, segment, token) {
|
|
39
|
+
const k = getKnex();
|
|
40
|
+
const { token_type, token: tokenData } = serializeToken(token);
|
|
41
|
+
await k.raw(`INSERT INTO ?? (processor_name, segment, mask, token_type, token, timestamp, owner)
|
|
42
|
+
VALUES (?, ?, 0, ?, ?, ?, NULL)
|
|
43
|
+
ON CONFLICT (processor_name, segment) DO UPDATE SET token_type = ?, token = ?, timestamp = ?`, [table, processorName, segment, token_type, tokenData, nowIso(), token_type, tokenData, nowIso()]);
|
|
44
|
+
},
|
|
45
|
+
async get(processorName, segment) {
|
|
46
|
+
const k = getKnex();
|
|
47
|
+
const row = await k(table)
|
|
48
|
+
.where({ processor_name: processorName, segment })
|
|
49
|
+
.first();
|
|
50
|
+
if (!row)
|
|
51
|
+
return undefined;
|
|
52
|
+
return deserializeToken(row.token_type, row.token);
|
|
53
|
+
},
|
|
54
|
+
async initializeSegments(processorName, segmentCount) {
|
|
55
|
+
const k = getKnex();
|
|
56
|
+
for (let i = 0; i < segmentCount; i++) {
|
|
57
|
+
await k.raw(`INSERT INTO ?? (processor_name, segment, mask, token_type, token, timestamp, owner)
|
|
58
|
+
VALUES (?, ?, 0, NULL, NULL, NULL, NULL)
|
|
59
|
+
ON CONFLICT (processor_name, segment) DO NOTHING`, [table, processorName, i]);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
async claimToken(processorName, segment, ownerId) {
|
|
63
|
+
const k = getKnex();
|
|
64
|
+
const row = await k(table)
|
|
65
|
+
.where({ processor_name: processorName, segment })
|
|
66
|
+
.first();
|
|
67
|
+
if (!row) {
|
|
68
|
+
await k(table).insert({
|
|
69
|
+
processor_name: processorName, segment, mask: 0,
|
|
70
|
+
token_type: null, token: null, timestamp: nowIso(), owner: ownerId,
|
|
71
|
+
});
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const isExpired = !row.owner || !row.timestamp ||
|
|
75
|
+
(Date.now() - new Date(row.timestamp).getTime() > claimTimeoutMs);
|
|
76
|
+
if (row.owner === ownerId || isExpired) {
|
|
77
|
+
await k(table)
|
|
78
|
+
.where({ processor_name: processorName, segment })
|
|
79
|
+
.update({ owner: ownerId, timestamp: nowIso() });
|
|
80
|
+
return deserializeToken(row.token_type, row.token);
|
|
81
|
+
}
|
|
82
|
+
throw new UnableToClaimTokenError(processorName, segment);
|
|
83
|
+
},
|
|
84
|
+
async extendClaim(processorName, segment, ownerId) {
|
|
85
|
+
const k = getKnex();
|
|
86
|
+
await k(table)
|
|
87
|
+
.where({ processor_name: processorName, segment, owner: ownerId })
|
|
88
|
+
.update({ timestamp: nowIso() });
|
|
89
|
+
},
|
|
90
|
+
async releaseClaim(processorName, segment, ownerId) {
|
|
91
|
+
const k = getKnex();
|
|
92
|
+
await k(table)
|
|
93
|
+
.where({ processor_name: processorName, segment, owner: ownerId })
|
|
94
|
+
.update({ owner: null, timestamp: null });
|
|
95
|
+
},
|
|
96
|
+
async fetchSegments(processorName) {
|
|
97
|
+
const k = getKnex();
|
|
98
|
+
const rows = await k(table)
|
|
99
|
+
.where({ processor_name: processorName })
|
|
100
|
+
.select("segment")
|
|
101
|
+
.orderBy("segment", "asc");
|
|
102
|
+
return rows.map((r) => r.segment);
|
|
103
|
+
},
|
|
104
|
+
async fetchAvailableSegments(processorName) {
|
|
105
|
+
const k = getKnex();
|
|
106
|
+
const cutoff = new Date(Date.now() - claimTimeoutMs).toISOString();
|
|
107
|
+
const rows = await k(table)
|
|
108
|
+
.where({ processor_name: processorName })
|
|
109
|
+
.where(function () {
|
|
110
|
+
this.whereNull("owner").orWhere("timestamp", "<", cutoff);
|
|
111
|
+
})
|
|
112
|
+
.select("segment")
|
|
113
|
+
.orderBy("segment", "asc");
|
|
114
|
+
return rows.map((r) => r.segment);
|
|
115
|
+
},
|
|
116
|
+
async deleteToken(processorName, segment) {
|
|
117
|
+
const k = getKnex();
|
|
118
|
+
await k(table)
|
|
119
|
+
.where({ processor_name: processorName, segment })
|
|
120
|
+
.delete();
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=knex-token-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"knex-token-store.js","sourceRoot":"","sources":["../src/knex-token-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { TransactionManager } from "@kronos-ts/messaging";
|
|
2
|
+
/**
|
|
3
|
+
* A Knex instance that supports transactions.
|
|
4
|
+
*/
|
|
5
|
+
export interface KnexInstanceLike {
|
|
6
|
+
transaction<T>(fn: (trx: any) => Promise<T>): Promise<T>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The Knex transaction object.
|
|
10
|
+
*/
|
|
11
|
+
export type KnexTransaction = any;
|
|
12
|
+
/**
|
|
13
|
+
* Creates a TransactionManager for Knex.
|
|
14
|
+
*
|
|
15
|
+
* Bridges Knex's `knex.transaction(fn)` callback to the framework's
|
|
16
|
+
* `begin/commit/rollback` lifecycle.
|
|
17
|
+
*
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import Knex from "knex"
|
|
20
|
+
* import { knexTransactionManager } from "@kronos-ts/knex"
|
|
21
|
+
*
|
|
22
|
+
* const knex = Knex({ client: "pg", connection: "..." })
|
|
23
|
+
*
|
|
24
|
+
* // transactionManager wiring to a kronos() App is pending a typed
|
|
25
|
+
* // `transactionManager` slot (Phase 9). For now, construct the manager
|
|
26
|
+
* // and pass it directly into the unitOfWorkFactory composition:
|
|
27
|
+
* const txManager = knexTransactionManager(knex)
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function knexTransactionManager(knex: KnexInstanceLike): TransactionManager<KnexTransaction>;
|
|
31
|
+
//# sourceMappingURL=knex-transaction-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"knex-transaction-manager.d.ts","sourceRoot":"","sources":["../src/knex-transaction-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAE9D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;CACzD;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,GAAG,CAAA;AAEjC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,gBAAgB,GACrB,kBAAkB,CAAC,eAAe,CAAC,CA2CrC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a TransactionManager for Knex.
|
|
3
|
+
*
|
|
4
|
+
* Bridges Knex's `knex.transaction(fn)` callback to the framework's
|
|
5
|
+
* `begin/commit/rollback` lifecycle.
|
|
6
|
+
*
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import Knex from "knex"
|
|
9
|
+
* import { knexTransactionManager } from "@kronos-ts/knex"
|
|
10
|
+
*
|
|
11
|
+
* const knex = Knex({ client: "pg", connection: "..." })
|
|
12
|
+
*
|
|
13
|
+
* // transactionManager wiring to a kronos() App is pending a typed
|
|
14
|
+
* // `transactionManager` slot (Phase 9). For now, construct the manager
|
|
15
|
+
* // and pass it directly into the unitOfWorkFactory composition:
|
|
16
|
+
* const txManager = knexTransactionManager(knex)
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function knexTransactionManager(knex) {
|
|
20
|
+
return {
|
|
21
|
+
async begin() {
|
|
22
|
+
let resolveTx;
|
|
23
|
+
let resolveCompletion;
|
|
24
|
+
let rejectCompletion;
|
|
25
|
+
const txReady = new Promise((resolve) => {
|
|
26
|
+
resolveTx = resolve;
|
|
27
|
+
});
|
|
28
|
+
const completionSignal = new Promise((resolve, reject) => {
|
|
29
|
+
resolveCompletion = resolve;
|
|
30
|
+
rejectCompletion = reject;
|
|
31
|
+
});
|
|
32
|
+
const txPromise = knex.transaction(async (trx) => {
|
|
33
|
+
resolveTx(trx);
|
|
34
|
+
await completionSignal;
|
|
35
|
+
});
|
|
36
|
+
const tx = await txReady;
|
|
37
|
+
tx.__kronos_commit = resolveCompletion;
|
|
38
|
+
tx.__kronos_rollback = rejectCompletion;
|
|
39
|
+
tx.__kronos_txPromise = txPromise;
|
|
40
|
+
return tx;
|
|
41
|
+
},
|
|
42
|
+
async commit(tx) {
|
|
43
|
+
const commit = tx.__kronos_commit;
|
|
44
|
+
const txPromise = tx.__kronos_txPromise;
|
|
45
|
+
commit();
|
|
46
|
+
await txPromise;
|
|
47
|
+
},
|
|
48
|
+
async rollback(tx) {
|
|
49
|
+
const rollback = tx.__kronos_rollback;
|
|
50
|
+
const txPromise = tx.__kronos_txPromise;
|
|
51
|
+
rollback(new Error("Transaction rolled back"));
|
|
52
|
+
try {
|
|
53
|
+
await txPromise;
|
|
54
|
+
}
|
|
55
|
+
catch { /* expected */ }
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=knex-transaction-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"knex-transaction-manager.js","sourceRoot":"","sources":["../src/knex-transaction-manager.ts"],"names":[],"mappings":"AAcA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,sBAAsB,CACpC,IAAsB;IAEtB,OAAO;QACL,KAAK,CAAC,KAAK;YACT,IAAI,SAAyC,CAAA;YAC7C,IAAI,iBAA8B,CAAA;YAClC,IAAI,gBAA2C,CAAA;YAE/C,MAAM,OAAO,GAAG,IAAI,OAAO,CAAkB,CAAC,OAAO,EAAE,EAAE;gBACvD,SAAS,GAAG,OAAO,CAAA;YACrB,CAAC,CAAC,CAAA;YAEF,MAAM,gBAAgB,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBAC7D,iBAAiB,GAAG,OAAO,CAAA;gBAC3B,gBAAgB,GAAG,MAAM,CAAA;YAC3B,CAAC,CAAC,CAAA;YAEF,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;gBAC/C,SAAS,CAAC,GAAG,CAAC,CAAA;gBACd,MAAM,gBAAgB,CAAA;YACxB,CAAC,CAAC,CAAA;YAEF,MAAM,EAAE,GAAG,MAAM,OAAO,CACvB;YAAC,EAAU,CAAC,eAAe,GAAG,iBAAiB,CAC/C;YAAC,EAAU,CAAC,iBAAiB,GAAG,gBAAgB,CAChD;YAAC,EAAU,CAAC,kBAAkB,GAAG,SAAS,CAAA;YAE3C,OAAO,EAAE,CAAA;QACX,CAAC;QAED,KAAK,CAAC,MAAM,CAAC,EAAmB;YAC9B,MAAM,MAAM,GAAI,EAAU,CAAC,eAA6B,CAAA;YACxD,MAAM,SAAS,GAAI,EAAU,CAAC,kBAAmC,CAAA;YACjE,MAAM,EAAE,CAAA;YACR,MAAM,SAAS,CAAA;QACjB,CAAC;QAED,KAAK,CAAC,QAAQ,CAAC,EAAmB;YAChC,MAAM,QAAQ,GAAI,EAAU,CAAC,iBAA6C,CAAA;YAC1E,MAAM,SAAS,GAAI,EAAU,CAAC,kBAAmC,CAAA;YACjE,QAAQ,CAAC,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC,CAAA;YAC9C,IAAI,CAAC;gBAAC,MAAM,SAAS,CAAA;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,cAAc,CAAC,CAAC;QAClD,CAAC;KACF,CAAA;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kronos-ts/knex",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Knex extension for Kronos.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"author": "Theo Emanuelsson",
|
|
8
|
+
"homepage": "https://github.com/KronosDB/kronos-ts/tree/main/packages/extensions/knex#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/KronosDB/kronos-ts.git",
|
|
12
|
+
"directory": "packages/extensions/knex"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/KronosDB/kronos-ts/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"kronos",
|
|
19
|
+
"event-sourcing",
|
|
20
|
+
"cqrs",
|
|
21
|
+
"dcb",
|
|
22
|
+
"typescript",
|
|
23
|
+
"knex"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"main": "src/index.ts",
|
|
27
|
+
"types": "src/index.ts",
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"src",
|
|
31
|
+
"!src/**/__tests__",
|
|
32
|
+
"!src/**/*.test.ts",
|
|
33
|
+
"!src/**/*.bench.ts"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.json",
|
|
37
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public",
|
|
41
|
+
"main": "./dist/index.js",
|
|
42
|
+
"types": "./dist/index.d.ts",
|
|
43
|
+
"exports": {
|
|
44
|
+
".": {
|
|
45
|
+
"types": "./dist/index.d.ts",
|
|
46
|
+
"default": "./dist/index.js"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@kronos-ts/common": "workspace:*",
|
|
52
|
+
"@kronos-ts/messaging": "workspace:*"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"knex": ">=3.0.0"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { TokenStore, TrackingToken } from "@kronos-ts/messaging"
|
|
2
|
+
import { getActiveTransaction, UnableToClaimTokenError, globalSequenceToken } from "@kronos-ts/messaging"
|
|
3
|
+
import type { KnexTransaction } from "./knex-transaction-manager.js"
|
|
4
|
+
|
|
5
|
+
function serializeToken(token: TrackingToken): { token_type: string; token: string } {
|
|
6
|
+
return {
|
|
7
|
+
token_type: "GlobalSequenceToken",
|
|
8
|
+
token: JSON.stringify({ position: token.position().toString() }),
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function deserializeToken(tokenType: string | null, token: string | null): TrackingToken | undefined {
|
|
13
|
+
if (!token || !tokenType) return undefined
|
|
14
|
+
const data = JSON.parse(token)
|
|
15
|
+
return globalSequenceToken(BigInt(data.position))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function nowIso(): string {
|
|
19
|
+
return new Date().toISOString()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A Knex instance or transaction with query builder methods.
|
|
24
|
+
*/
|
|
25
|
+
export interface KnexQueryable {
|
|
26
|
+
(tableName: string): any
|
|
27
|
+
raw(sql: string, ...bindings: any[]): any
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates a TokenStore backed by Knex.
|
|
32
|
+
*
|
|
33
|
+
* Participates in the active transaction via `getActiveTransaction()`.
|
|
34
|
+
*
|
|
35
|
+
* ```typescript
|
|
36
|
+
* import { knexTokenStore } from "@kronos-ts/knex"
|
|
37
|
+
*
|
|
38
|
+
* // tokenStore wiring to a kronos() App is pending a typed `tokenStore` slot
|
|
39
|
+
* // (Phase 9). For now, construct the store and pass it directly to the
|
|
40
|
+
* // tracking processor that owns it:
|
|
41
|
+
* const tokenStore = knexTokenStore(knex)
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function knexTokenStore(
|
|
45
|
+
knex: KnexQueryable,
|
|
46
|
+
options?: { claimTimeoutMs?: number; tableName?: string },
|
|
47
|
+
): TokenStore {
|
|
48
|
+
const claimTimeoutMs = options?.claimTimeoutMs ?? 10000
|
|
49
|
+
const table = options?.tableName ?? "kronos_token_entries"
|
|
50
|
+
|
|
51
|
+
function getKnex(): KnexQueryable {
|
|
52
|
+
return getActiveTransaction<KnexTransaction>() ?? knex
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
async store(processorName, segment, token) {
|
|
57
|
+
const k = getKnex()
|
|
58
|
+
const { token_type, token: tokenData } = serializeToken(token)
|
|
59
|
+
await k.raw(
|
|
60
|
+
`INSERT INTO ?? (processor_name, segment, mask, token_type, token, timestamp, owner)
|
|
61
|
+
VALUES (?, ?, 0, ?, ?, ?, NULL)
|
|
62
|
+
ON CONFLICT (processor_name, segment) DO UPDATE SET token_type = ?, token = ?, timestamp = ?`,
|
|
63
|
+
[table, processorName, segment, token_type, tokenData, nowIso(), token_type, tokenData, nowIso()],
|
|
64
|
+
)
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
async get(processorName, segment) {
|
|
68
|
+
const k = getKnex()
|
|
69
|
+
const row = await k(table)
|
|
70
|
+
.where({ processor_name: processorName, segment })
|
|
71
|
+
.first()
|
|
72
|
+
if (!row) return undefined
|
|
73
|
+
return deserializeToken(row.token_type, row.token)
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
async initializeSegments(processorName, segmentCount) {
|
|
77
|
+
const k = getKnex()
|
|
78
|
+
for (let i = 0; i < segmentCount; i++) {
|
|
79
|
+
await k.raw(
|
|
80
|
+
`INSERT INTO ?? (processor_name, segment, mask, token_type, token, timestamp, owner)
|
|
81
|
+
VALUES (?, ?, 0, NULL, NULL, NULL, NULL)
|
|
82
|
+
ON CONFLICT (processor_name, segment) DO NOTHING`,
|
|
83
|
+
[table, processorName, i],
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
async claimToken(processorName, segment, ownerId) {
|
|
89
|
+
const k = getKnex()
|
|
90
|
+
const row = await k(table)
|
|
91
|
+
.where({ processor_name: processorName, segment })
|
|
92
|
+
.first()
|
|
93
|
+
|
|
94
|
+
if (!row) {
|
|
95
|
+
await k(table).insert({
|
|
96
|
+
processor_name: processorName, segment, mask: 0,
|
|
97
|
+
token_type: null, token: null, timestamp: nowIso(), owner: ownerId,
|
|
98
|
+
})
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const isExpired = !row.owner || !row.timestamp ||
|
|
103
|
+
(Date.now() - new Date(row.timestamp).getTime() > claimTimeoutMs)
|
|
104
|
+
|
|
105
|
+
if (row.owner === ownerId || isExpired) {
|
|
106
|
+
await k(table)
|
|
107
|
+
.where({ processor_name: processorName, segment })
|
|
108
|
+
.update({ owner: ownerId, timestamp: nowIso() })
|
|
109
|
+
return deserializeToken(row.token_type, row.token)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw new UnableToClaimTokenError(processorName, segment)
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
async extendClaim(processorName, segment, ownerId) {
|
|
116
|
+
const k = getKnex()
|
|
117
|
+
await k(table)
|
|
118
|
+
.where({ processor_name: processorName, segment, owner: ownerId })
|
|
119
|
+
.update({ timestamp: nowIso() })
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
async releaseClaim(processorName, segment, ownerId) {
|
|
123
|
+
const k = getKnex()
|
|
124
|
+
await k(table)
|
|
125
|
+
.where({ processor_name: processorName, segment, owner: ownerId })
|
|
126
|
+
.update({ owner: null, timestamp: null })
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
async fetchSegments(processorName) {
|
|
130
|
+
const k = getKnex()
|
|
131
|
+
const rows = await k(table)
|
|
132
|
+
.where({ processor_name: processorName })
|
|
133
|
+
.select("segment")
|
|
134
|
+
.orderBy("segment", "asc")
|
|
135
|
+
return rows.map((r: any) => r.segment)
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
async fetchAvailableSegments(processorName) {
|
|
139
|
+
const k = getKnex()
|
|
140
|
+
const cutoff = new Date(Date.now() - claimTimeoutMs).toISOString()
|
|
141
|
+
const rows = await k(table)
|
|
142
|
+
.where({ processor_name: processorName })
|
|
143
|
+
.where(function (this: any) {
|
|
144
|
+
this.whereNull("owner").orWhere("timestamp", "<", cutoff)
|
|
145
|
+
})
|
|
146
|
+
.select("segment")
|
|
147
|
+
.orderBy("segment", "asc")
|
|
148
|
+
return rows.map((r: any) => r.segment)
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
async deleteToken(processorName, segment) {
|
|
152
|
+
const k = getKnex()
|
|
153
|
+
await k(table)
|
|
154
|
+
.where({ processor_name: processorName, segment })
|
|
155
|
+
.delete()
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { TransactionManager } from "@kronos-ts/messaging"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A Knex instance that supports transactions.
|
|
5
|
+
*/
|
|
6
|
+
export interface KnexInstanceLike {
|
|
7
|
+
transaction<T>(fn: (trx: any) => Promise<T>): Promise<T>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The Knex transaction object.
|
|
12
|
+
*/
|
|
13
|
+
export type KnexTransaction = any
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a TransactionManager for Knex.
|
|
17
|
+
*
|
|
18
|
+
* Bridges Knex's `knex.transaction(fn)` callback to the framework's
|
|
19
|
+
* `begin/commit/rollback` lifecycle.
|
|
20
|
+
*
|
|
21
|
+
* ```typescript
|
|
22
|
+
* import Knex from "knex"
|
|
23
|
+
* import { knexTransactionManager } from "@kronos-ts/knex"
|
|
24
|
+
*
|
|
25
|
+
* const knex = Knex({ client: "pg", connection: "..." })
|
|
26
|
+
*
|
|
27
|
+
* // transactionManager wiring to a kronos() App is pending a typed
|
|
28
|
+
* // `transactionManager` slot (Phase 9). For now, construct the manager
|
|
29
|
+
* // and pass it directly into the unitOfWorkFactory composition:
|
|
30
|
+
* const txManager = knexTransactionManager(knex)
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function knexTransactionManager(
|
|
34
|
+
knex: KnexInstanceLike,
|
|
35
|
+
): TransactionManager<KnexTransaction> {
|
|
36
|
+
return {
|
|
37
|
+
async begin(): Promise<KnexTransaction> {
|
|
38
|
+
let resolveTx!: (tx: KnexTransaction) => void
|
|
39
|
+
let resolveCompletion!: () => void
|
|
40
|
+
let rejectCompletion!: (error: unknown) => void
|
|
41
|
+
|
|
42
|
+
const txReady = new Promise<KnexTransaction>((resolve) => {
|
|
43
|
+
resolveTx = resolve
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const completionSignal = new Promise<void>((resolve, reject) => {
|
|
47
|
+
resolveCompletion = resolve
|
|
48
|
+
rejectCompletion = reject
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const txPromise = knex.transaction(async (trx) => {
|
|
52
|
+
resolveTx(trx)
|
|
53
|
+
await completionSignal
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const tx = await txReady
|
|
57
|
+
;(tx as any).__kronos_commit = resolveCompletion
|
|
58
|
+
;(tx as any).__kronos_rollback = rejectCompletion
|
|
59
|
+
;(tx as any).__kronos_txPromise = txPromise
|
|
60
|
+
|
|
61
|
+
return tx
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
async commit(tx: KnexTransaction): Promise<void> {
|
|
65
|
+
const commit = (tx as any).__kronos_commit as () => void
|
|
66
|
+
const txPromise = (tx as any).__kronos_txPromise as Promise<void>
|
|
67
|
+
commit()
|
|
68
|
+
await txPromise
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
async rollback(tx: KnexTransaction): Promise<void> {
|
|
72
|
+
const rollback = (tx as any).__kronos_rollback as (error: unknown) => void
|
|
73
|
+
const txPromise = (tx as any).__kronos_txPromise as Promise<void>
|
|
74
|
+
rollback(new Error("Transaction rolled back"))
|
|
75
|
+
try { await txPromise } catch { /* expected */ }
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
}
|