@kronos-ts/kysely 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/kysely-token-store.d.ts +30 -0
- package/dist/kysely-token-store.d.ts.map +1 -0
- package/dist/kysely-token-store.js +161 -0
- package/dist/kysely-token-store.js.map +1 -0
- package/dist/kysely-transaction-manager.d.ts +31 -0
- package/dist/kysely-transaction-manager.d.ts.map +1 -0
- package/dist/kysely-transaction-manager.js +57 -0
- package/dist/kysely-transaction-manager.js.map +1 -0
- package/package.json +57 -0
- package/src/index.ts +10 -0
- package/src/kysely-token-store.ts +192 -0
- package/src/kysely-transaction-manager.ts +76 -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,wBAAwB,EACxB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,GACvB,MAAM,iCAAiC,CAAA;AAExC,OAAO,EACL,gBAAgB,EAChB,KAAK,YAAY,GAClB,MAAM,yBAAyB,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,wBAAwB,GAGzB,MAAM,iCAAiC,CAAA;AAExC,OAAO,EACL,gBAAgB,GAEjB,MAAM,yBAAyB,CAAA"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TokenStore } from "@kronos-ts/messaging";
|
|
2
|
+
/**
|
|
3
|
+
* A Kysely database instance (or transaction) with query methods.
|
|
4
|
+
*/
|
|
5
|
+
export interface KyselyDbLike {
|
|
6
|
+
selectFrom(table: string): any;
|
|
7
|
+
insertInto(table: string): any;
|
|
8
|
+
updateTable(table: string): any;
|
|
9
|
+
deleteFrom(table: string): any;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Creates a TokenStore backed by Kysely.
|
|
13
|
+
*
|
|
14
|
+
* Participates in the active transaction via `getActiveTransaction()`.
|
|
15
|
+
* Uses the `kronos_token_entries` table with snake_case column names.
|
|
16
|
+
*
|
|
17
|
+
* ```typescript
|
|
18
|
+
* import { kyselyTokenStore } from "@kronos-ts/kysely"
|
|
19
|
+
*
|
|
20
|
+
* // tokenStore wiring to a kronos() App is pending a typed `tokenStore` slot
|
|
21
|
+
* // (Phase 9). For now, construct the store and pass it directly to the
|
|
22
|
+
* // tracking processor that owns it:
|
|
23
|
+
* const tokenStore = kyselyTokenStore(db)
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function kyselyTokenStore(db: KyselyDbLike, options?: {
|
|
27
|
+
claimTimeoutMs?: number;
|
|
28
|
+
tableName?: string;
|
|
29
|
+
}): TokenStore;
|
|
30
|
+
//# sourceMappingURL=kysely-token-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kysely-token-store.d.ts","sourceRoot":"","sources":["../src/kysely-token-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAiB,MAAM,sBAAsB,CAAA;AAwCrE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAA;IAC9B,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAA;IAC9B,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAA;IAC/B,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAA;CAC/B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,YAAY,EAChB,OAAO,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACxD,UAAU,CA2HZ"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { getActiveTransaction, UnableToClaimTokenError, globalSequenceToken } from "@kronos-ts/messaging";
|
|
2
|
+
/**
|
|
3
|
+
* Token table interface for Kysely. Users must define this table in their
|
|
4
|
+
* Kysely database interface:
|
|
5
|
+
*
|
|
6
|
+
* ```typescript
|
|
7
|
+
* interface Database {
|
|
8
|
+
* kronos_token_entries: {
|
|
9
|
+
* processor_name: string
|
|
10
|
+
* segment: number
|
|
11
|
+
* mask: number
|
|
12
|
+
* token_type: string | null
|
|
13
|
+
* token: string | null
|
|
14
|
+
* timestamp: string | null
|
|
15
|
+
* owner: string | null
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
function serializeToken(token) {
|
|
21
|
+
return {
|
|
22
|
+
token_type: "GlobalSequenceToken",
|
|
23
|
+
token: JSON.stringify({ position: token.position().toString() }),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function deserializeToken(tokenType, token) {
|
|
27
|
+
if (!token || !tokenType)
|
|
28
|
+
return undefined;
|
|
29
|
+
const data = JSON.parse(token);
|
|
30
|
+
return globalSequenceToken(BigInt(data.position));
|
|
31
|
+
}
|
|
32
|
+
function nowIso() {
|
|
33
|
+
return new Date().toISOString();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Creates a TokenStore backed by Kysely.
|
|
37
|
+
*
|
|
38
|
+
* Participates in the active transaction via `getActiveTransaction()`.
|
|
39
|
+
* Uses the `kronos_token_entries` table with snake_case column names.
|
|
40
|
+
*
|
|
41
|
+
* ```typescript
|
|
42
|
+
* import { kyselyTokenStore } from "@kronos-ts/kysely"
|
|
43
|
+
*
|
|
44
|
+
* // tokenStore wiring to a kronos() App is pending a typed `tokenStore` slot
|
|
45
|
+
* // (Phase 9). For now, construct the store and pass it directly to the
|
|
46
|
+
* // tracking processor that owns it:
|
|
47
|
+
* const tokenStore = kyselyTokenStore(db)
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function kyselyTokenStore(db, options) {
|
|
51
|
+
const claimTimeoutMs = options?.claimTimeoutMs ?? 10000;
|
|
52
|
+
const table = options?.tableName ?? "kronos_token_entries";
|
|
53
|
+
function getDb() {
|
|
54
|
+
return getActiveTransaction() ?? db;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
async store(processorName, segment, token) {
|
|
58
|
+
const d = getDb();
|
|
59
|
+
const { token_type, token: tokenData } = serializeToken(token);
|
|
60
|
+
// Upsert via onConflict
|
|
61
|
+
await d.insertInto(table)
|
|
62
|
+
.values({ processor_name: processorName, segment, mask: 0, token_type, token: tokenData, timestamp: nowIso(), owner: null })
|
|
63
|
+
.onConflict((oc) => oc.columns(["processor_name", "segment"]).doUpdateSet({ token_type, token: tokenData, timestamp: nowIso() }))
|
|
64
|
+
.execute();
|
|
65
|
+
},
|
|
66
|
+
async get(processorName, segment) {
|
|
67
|
+
const d = getDb();
|
|
68
|
+
const row = await d.selectFrom(table)
|
|
69
|
+
.selectAll()
|
|
70
|
+
.where("processor_name", "=", processorName)
|
|
71
|
+
.where("segment", "=", segment)
|
|
72
|
+
.executeTakeFirst();
|
|
73
|
+
if (!row)
|
|
74
|
+
return undefined;
|
|
75
|
+
return deserializeToken(row.token_type, row.token);
|
|
76
|
+
},
|
|
77
|
+
async initializeSegments(processorName, segmentCount) {
|
|
78
|
+
const d = getDb();
|
|
79
|
+
for (let i = 0; i < segmentCount; i++) {
|
|
80
|
+
await d.insertInto(table)
|
|
81
|
+
.values({ processor_name: processorName, segment: i, mask: 0, token_type: null, token: null, timestamp: null, owner: null })
|
|
82
|
+
.onConflict((oc) => oc.columns(["processor_name", "segment"]).doNothing())
|
|
83
|
+
.execute();
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
async claimToken(processorName, segment, ownerId) {
|
|
87
|
+
const d = getDb();
|
|
88
|
+
const row = await d.selectFrom(table)
|
|
89
|
+
.selectAll()
|
|
90
|
+
.where("processor_name", "=", processorName)
|
|
91
|
+
.where("segment", "=", segment)
|
|
92
|
+
.executeTakeFirst();
|
|
93
|
+
if (!row) {
|
|
94
|
+
await d.insertInto(table)
|
|
95
|
+
.values({ processor_name: processorName, segment, mask: 0, token_type: null, token: null, timestamp: nowIso(), owner: ownerId })
|
|
96
|
+
.execute();
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const isExpired = !row.owner || !row.timestamp ||
|
|
100
|
+
(Date.now() - new Date(row.timestamp).getTime() > claimTimeoutMs);
|
|
101
|
+
if (row.owner === ownerId || isExpired) {
|
|
102
|
+
await d.updateTable(table)
|
|
103
|
+
.set({ owner: ownerId, timestamp: nowIso() })
|
|
104
|
+
.where("processor_name", "=", processorName)
|
|
105
|
+
.where("segment", "=", segment)
|
|
106
|
+
.execute();
|
|
107
|
+
return deserializeToken(row.token_type, row.token);
|
|
108
|
+
}
|
|
109
|
+
throw new UnableToClaimTokenError(processorName, segment);
|
|
110
|
+
},
|
|
111
|
+
async extendClaim(processorName, segment, ownerId) {
|
|
112
|
+
const d = getDb();
|
|
113
|
+
await d.updateTable(table)
|
|
114
|
+
.set({ timestamp: nowIso() })
|
|
115
|
+
.where("processor_name", "=", processorName)
|
|
116
|
+
.where("segment", "=", segment)
|
|
117
|
+
.where("owner", "=", ownerId)
|
|
118
|
+
.execute();
|
|
119
|
+
},
|
|
120
|
+
async releaseClaim(processorName, segment, ownerId) {
|
|
121
|
+
const d = getDb();
|
|
122
|
+
await d.updateTable(table)
|
|
123
|
+
.set({ owner: null, timestamp: null })
|
|
124
|
+
.where("processor_name", "=", processorName)
|
|
125
|
+
.where("segment", "=", segment)
|
|
126
|
+
.where("owner", "=", ownerId)
|
|
127
|
+
.execute();
|
|
128
|
+
},
|
|
129
|
+
async fetchSegments(processorName) {
|
|
130
|
+
const d = getDb();
|
|
131
|
+
const rows = await d.selectFrom(table)
|
|
132
|
+
.select("segment")
|
|
133
|
+
.where("processor_name", "=", processorName)
|
|
134
|
+
.orderBy("segment", "asc")
|
|
135
|
+
.execute();
|
|
136
|
+
return rows.map((r) => r.segment);
|
|
137
|
+
},
|
|
138
|
+
async fetchAvailableSegments(processorName) {
|
|
139
|
+
const d = getDb();
|
|
140
|
+
const cutoff = new Date(Date.now() - claimTimeoutMs).toISOString();
|
|
141
|
+
const rows = await d.selectFrom(table)
|
|
142
|
+
.select("segment")
|
|
143
|
+
.where("processor_name", "=", processorName)
|
|
144
|
+
.where((eb) => eb.or([
|
|
145
|
+
eb("owner", "is", null),
|
|
146
|
+
eb("timestamp", "<", cutoff),
|
|
147
|
+
]))
|
|
148
|
+
.orderBy("segment", "asc")
|
|
149
|
+
.execute();
|
|
150
|
+
return rows.map((r) => r.segment);
|
|
151
|
+
},
|
|
152
|
+
async deleteToken(processorName, segment) {
|
|
153
|
+
const d = getDb();
|
|
154
|
+
await d.deleteFrom(table)
|
|
155
|
+
.where("processor_name", "=", processorName)
|
|
156
|
+
.where("segment", "=", segment)
|
|
157
|
+
.execute();
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=kysely-token-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kysely-token-store.js","sourceRoot":"","sources":["../src/kysely-token-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { TransactionManager } from "@kronos-ts/messaging";
|
|
2
|
+
/**
|
|
3
|
+
* A Kysely database instance that supports transactions.
|
|
4
|
+
*/
|
|
5
|
+
export interface KyselyDatabaseLike {
|
|
6
|
+
transaction(): {
|
|
7
|
+
execute<T>(fn: (trx: any) => Promise<T>): Promise<T>;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The Kysely transaction object.
|
|
12
|
+
*/
|
|
13
|
+
export type KyselyTransaction = any;
|
|
14
|
+
/**
|
|
15
|
+
* Creates a TransactionManager for Kysely.
|
|
16
|
+
*
|
|
17
|
+
* Bridges Kysely's `db.transaction().execute(fn)` callback pattern
|
|
18
|
+
* to the framework's `begin/commit/rollback` lifecycle.
|
|
19
|
+
*
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { Kysely } from "kysely"
|
|
22
|
+
* import { kyselyTransactionManager } from "@kronos-ts/kysely"
|
|
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 = kyselyTransactionManager(db)
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function kyselyTransactionManager(db: KyselyDatabaseLike): TransactionManager<KyselyTransaction>;
|
|
31
|
+
//# sourceMappingURL=kysely-transaction-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kysely-transaction-manager.d.ts","sourceRoot":"","sources":["../src/kysely-transaction-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAE9D;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,WAAW,IAAI;QAAE,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;KAAE,CAAA;CACxE;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,GAAG,CAAA;AAEnC;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,wBAAwB,CACtC,EAAE,EAAE,kBAAkB,GACrB,kBAAkB,CAAC,iBAAiB,CAAC,CA2CvC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a TransactionManager for Kysely.
|
|
3
|
+
*
|
|
4
|
+
* Bridges Kysely's `db.transaction().execute(fn)` callback pattern
|
|
5
|
+
* to the framework's `begin/commit/rollback` lifecycle.
|
|
6
|
+
*
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { Kysely } from "kysely"
|
|
9
|
+
* import { kyselyTransactionManager } from "@kronos-ts/kysely"
|
|
10
|
+
*
|
|
11
|
+
* // transactionManager wiring to a kronos() App is pending a typed
|
|
12
|
+
* // `transactionManager` slot (Phase 9). For now, construct the manager
|
|
13
|
+
* // and pass it directly into the unitOfWorkFactory composition:
|
|
14
|
+
* const txManager = kyselyTransactionManager(db)
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function kyselyTransactionManager(db) {
|
|
18
|
+
return {
|
|
19
|
+
async begin() {
|
|
20
|
+
let resolveTx;
|
|
21
|
+
let resolveCompletion;
|
|
22
|
+
let rejectCompletion;
|
|
23
|
+
const txReady = new Promise((resolve) => {
|
|
24
|
+
resolveTx = resolve;
|
|
25
|
+
});
|
|
26
|
+
const completionSignal = new Promise((resolve, reject) => {
|
|
27
|
+
resolveCompletion = resolve;
|
|
28
|
+
rejectCompletion = reject;
|
|
29
|
+
});
|
|
30
|
+
const txPromise = db.transaction().execute(async (trx) => {
|
|
31
|
+
resolveTx(trx);
|
|
32
|
+
await completionSignal;
|
|
33
|
+
});
|
|
34
|
+
const tx = await txReady;
|
|
35
|
+
tx.__kronos_commit = resolveCompletion;
|
|
36
|
+
tx.__kronos_rollback = rejectCompletion;
|
|
37
|
+
tx.__kronos_txPromise = txPromise;
|
|
38
|
+
return tx;
|
|
39
|
+
},
|
|
40
|
+
async commit(tx) {
|
|
41
|
+
const commit = tx.__kronos_commit;
|
|
42
|
+
const txPromise = tx.__kronos_txPromise;
|
|
43
|
+
commit();
|
|
44
|
+
await txPromise;
|
|
45
|
+
},
|
|
46
|
+
async rollback(tx) {
|
|
47
|
+
const rollback = tx.__kronos_rollback;
|
|
48
|
+
const txPromise = tx.__kronos_txPromise;
|
|
49
|
+
rollback(new Error("Transaction rolled back"));
|
|
50
|
+
try {
|
|
51
|
+
await txPromise;
|
|
52
|
+
}
|
|
53
|
+
catch { /* expected */ }
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=kysely-transaction-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kysely-transaction-manager.js","sourceRoot":"","sources":["../src/kysely-transaction-manager.ts"],"names":[],"mappings":"AAcA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,wBAAwB,CACtC,EAAsB;IAEtB,OAAO;QACL,KAAK,CAAC,KAAK;YACT,IAAI,SAA2C,CAAA;YAC/C,IAAI,iBAA8B,CAAA;YAClC,IAAI,gBAA2C,CAAA;YAE/C,MAAM,OAAO,GAAG,IAAI,OAAO,CAAoB,CAAC,OAAO,EAAE,EAAE;gBACzD,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,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;gBACvD,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,EAAqB;YAChC,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,EAAqB;YAClC,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/kysely",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Kysely 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/kysely#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/KronosDB/kronos-ts.git",
|
|
12
|
+
"directory": "packages/extensions/kysely"
|
|
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
|
+
"kysely"
|
|
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
|
+
"kysely": ">=0.27.0"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { TokenStore, TrackingToken } from "@kronos-ts/messaging"
|
|
2
|
+
import { getActiveTransaction, UnableToClaimTokenError, globalSequenceToken } from "@kronos-ts/messaging"
|
|
3
|
+
import type { KyselyTransaction } from "./kysely-transaction-manager.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Token table interface for Kysely. Users must define this table in their
|
|
7
|
+
* Kysely database interface:
|
|
8
|
+
*
|
|
9
|
+
* ```typescript
|
|
10
|
+
* interface Database {
|
|
11
|
+
* kronos_token_entries: {
|
|
12
|
+
* processor_name: string
|
|
13
|
+
* segment: number
|
|
14
|
+
* mask: number
|
|
15
|
+
* token_type: string | null
|
|
16
|
+
* token: string | null
|
|
17
|
+
* timestamp: string | null
|
|
18
|
+
* owner: string | null
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
function serializeToken(token: TrackingToken): { token_type: string; token: string } {
|
|
25
|
+
return {
|
|
26
|
+
token_type: "GlobalSequenceToken",
|
|
27
|
+
token: JSON.stringify({ position: token.position().toString() }),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function deserializeToken(tokenType: string | null, token: string | null): TrackingToken | undefined {
|
|
32
|
+
if (!token || !tokenType) return undefined
|
|
33
|
+
const data = JSON.parse(token)
|
|
34
|
+
return globalSequenceToken(BigInt(data.position))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function nowIso(): string {
|
|
38
|
+
return new Date().toISOString()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A Kysely database instance (or transaction) with query methods.
|
|
43
|
+
*/
|
|
44
|
+
export interface KyselyDbLike {
|
|
45
|
+
selectFrom(table: string): any
|
|
46
|
+
insertInto(table: string): any
|
|
47
|
+
updateTable(table: string): any
|
|
48
|
+
deleteFrom(table: string): any
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Creates a TokenStore backed by Kysely.
|
|
53
|
+
*
|
|
54
|
+
* Participates in the active transaction via `getActiveTransaction()`.
|
|
55
|
+
* Uses the `kronos_token_entries` table with snake_case column names.
|
|
56
|
+
*
|
|
57
|
+
* ```typescript
|
|
58
|
+
* import { kyselyTokenStore } from "@kronos-ts/kysely"
|
|
59
|
+
*
|
|
60
|
+
* // tokenStore wiring to a kronos() App is pending a typed `tokenStore` slot
|
|
61
|
+
* // (Phase 9). For now, construct the store and pass it directly to the
|
|
62
|
+
* // tracking processor that owns it:
|
|
63
|
+
* const tokenStore = kyselyTokenStore(db)
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export function kyselyTokenStore(
|
|
67
|
+
db: KyselyDbLike,
|
|
68
|
+
options?: { claimTimeoutMs?: number; tableName?: string },
|
|
69
|
+
): TokenStore {
|
|
70
|
+
const claimTimeoutMs = options?.claimTimeoutMs ?? 10000
|
|
71
|
+
const table = options?.tableName ?? "kronos_token_entries"
|
|
72
|
+
|
|
73
|
+
function getDb(): KyselyDbLike {
|
|
74
|
+
return getActiveTransaction<KyselyTransaction>() ?? db
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
async store(processorName, segment, token) {
|
|
79
|
+
const d = getDb()
|
|
80
|
+
const { token_type, token: tokenData } = serializeToken(token)
|
|
81
|
+
// Upsert via onConflict
|
|
82
|
+
await d.insertInto(table)
|
|
83
|
+
.values({ processor_name: processorName, segment, mask: 0, token_type, token: tokenData, timestamp: nowIso(), owner: null })
|
|
84
|
+
.onConflict((oc: any) => oc.columns(["processor_name", "segment"]).doUpdateSet({ token_type, token: tokenData, timestamp: nowIso() }))
|
|
85
|
+
.execute()
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
async get(processorName, segment) {
|
|
89
|
+
const d = getDb()
|
|
90
|
+
const row = await d.selectFrom(table)
|
|
91
|
+
.selectAll()
|
|
92
|
+
.where("processor_name", "=", processorName)
|
|
93
|
+
.where("segment", "=", segment)
|
|
94
|
+
.executeTakeFirst()
|
|
95
|
+
if (!row) return undefined
|
|
96
|
+
return deserializeToken(row.token_type, row.token)
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async initializeSegments(processorName, segmentCount) {
|
|
100
|
+
const d = getDb()
|
|
101
|
+
for (let i = 0; i < segmentCount; i++) {
|
|
102
|
+
await d.insertInto(table)
|
|
103
|
+
.values({ processor_name: processorName, segment: i, mask: 0, token_type: null, token: null, timestamp: null, owner: null })
|
|
104
|
+
.onConflict((oc: any) => oc.columns(["processor_name", "segment"]).doNothing())
|
|
105
|
+
.execute()
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
async claimToken(processorName, segment, ownerId) {
|
|
110
|
+
const d = getDb()
|
|
111
|
+
const row = await d.selectFrom(table)
|
|
112
|
+
.selectAll()
|
|
113
|
+
.where("processor_name", "=", processorName)
|
|
114
|
+
.where("segment", "=", segment)
|
|
115
|
+
.executeTakeFirst()
|
|
116
|
+
|
|
117
|
+
if (!row) {
|
|
118
|
+
await d.insertInto(table)
|
|
119
|
+
.values({ processor_name: processorName, segment, mask: 0, token_type: null, token: null, timestamp: nowIso(), owner: ownerId })
|
|
120
|
+
.execute()
|
|
121
|
+
return undefined
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const isExpired = !row.owner || !row.timestamp ||
|
|
125
|
+
(Date.now() - new Date(row.timestamp).getTime() > claimTimeoutMs)
|
|
126
|
+
|
|
127
|
+
if (row.owner === ownerId || isExpired) {
|
|
128
|
+
await d.updateTable(table)
|
|
129
|
+
.set({ owner: ownerId, timestamp: nowIso() })
|
|
130
|
+
.where("processor_name", "=", processorName)
|
|
131
|
+
.where("segment", "=", segment)
|
|
132
|
+
.execute()
|
|
133
|
+
return deserializeToken(row.token_type, row.token)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
throw new UnableToClaimTokenError(processorName, segment)
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
async extendClaim(processorName, segment, ownerId) {
|
|
140
|
+
const d = getDb()
|
|
141
|
+
await d.updateTable(table)
|
|
142
|
+
.set({ timestamp: nowIso() })
|
|
143
|
+
.where("processor_name", "=", processorName)
|
|
144
|
+
.where("segment", "=", segment)
|
|
145
|
+
.where("owner", "=", ownerId)
|
|
146
|
+
.execute()
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
async releaseClaim(processorName, segment, ownerId) {
|
|
150
|
+
const d = getDb()
|
|
151
|
+
await d.updateTable(table)
|
|
152
|
+
.set({ owner: null, timestamp: null })
|
|
153
|
+
.where("processor_name", "=", processorName)
|
|
154
|
+
.where("segment", "=", segment)
|
|
155
|
+
.where("owner", "=", ownerId)
|
|
156
|
+
.execute()
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
async fetchSegments(processorName) {
|
|
160
|
+
const d = getDb()
|
|
161
|
+
const rows = await d.selectFrom(table)
|
|
162
|
+
.select("segment")
|
|
163
|
+
.where("processor_name", "=", processorName)
|
|
164
|
+
.orderBy("segment", "asc")
|
|
165
|
+
.execute()
|
|
166
|
+
return rows.map((r: any) => r.segment)
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
async fetchAvailableSegments(processorName) {
|
|
170
|
+
const d = getDb()
|
|
171
|
+
const cutoff = new Date(Date.now() - claimTimeoutMs).toISOString()
|
|
172
|
+
const rows = await d.selectFrom(table)
|
|
173
|
+
.select("segment")
|
|
174
|
+
.where("processor_name", "=", processorName)
|
|
175
|
+
.where((eb: any) => eb.or([
|
|
176
|
+
eb("owner", "is", null),
|
|
177
|
+
eb("timestamp", "<", cutoff),
|
|
178
|
+
]))
|
|
179
|
+
.orderBy("segment", "asc")
|
|
180
|
+
.execute()
|
|
181
|
+
return rows.map((r: any) => r.segment)
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
async deleteToken(processorName, segment) {
|
|
185
|
+
const d = getDb()
|
|
186
|
+
await d.deleteFrom(table)
|
|
187
|
+
.where("processor_name", "=", processorName)
|
|
188
|
+
.where("segment", "=", segment)
|
|
189
|
+
.execute()
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { TransactionManager } from "@kronos-ts/messaging"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A Kysely database instance that supports transactions.
|
|
5
|
+
*/
|
|
6
|
+
export interface KyselyDatabaseLike {
|
|
7
|
+
transaction(): { execute<T>(fn: (trx: any) => Promise<T>): Promise<T> }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The Kysely transaction object.
|
|
12
|
+
*/
|
|
13
|
+
export type KyselyTransaction = any
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a TransactionManager for Kysely.
|
|
17
|
+
*
|
|
18
|
+
* Bridges Kysely's `db.transaction().execute(fn)` callback pattern
|
|
19
|
+
* to the framework's `begin/commit/rollback` lifecycle.
|
|
20
|
+
*
|
|
21
|
+
* ```typescript
|
|
22
|
+
* import { Kysely } from "kysely"
|
|
23
|
+
* import { kyselyTransactionManager } from "@kronos-ts/kysely"
|
|
24
|
+
*
|
|
25
|
+
* // transactionManager wiring to a kronos() App is pending a typed
|
|
26
|
+
* // `transactionManager` slot (Phase 9). For now, construct the manager
|
|
27
|
+
* // and pass it directly into the unitOfWorkFactory composition:
|
|
28
|
+
* const txManager = kyselyTransactionManager(db)
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function kyselyTransactionManager(
|
|
32
|
+
db: KyselyDatabaseLike,
|
|
33
|
+
): TransactionManager<KyselyTransaction> {
|
|
34
|
+
return {
|
|
35
|
+
async begin(): Promise<KyselyTransaction> {
|
|
36
|
+
let resolveTx!: (tx: KyselyTransaction) => void
|
|
37
|
+
let resolveCompletion!: () => void
|
|
38
|
+
let rejectCompletion!: (error: unknown) => void
|
|
39
|
+
|
|
40
|
+
const txReady = new Promise<KyselyTransaction>((resolve) => {
|
|
41
|
+
resolveTx = resolve
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const completionSignal = new Promise<void>((resolve, reject) => {
|
|
45
|
+
resolveCompletion = resolve
|
|
46
|
+
rejectCompletion = reject
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const txPromise = db.transaction().execute(async (trx) => {
|
|
50
|
+
resolveTx(trx)
|
|
51
|
+
await completionSignal
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const tx = await txReady
|
|
55
|
+
;(tx as any).__kronos_commit = resolveCompletion
|
|
56
|
+
;(tx as any).__kronos_rollback = rejectCompletion
|
|
57
|
+
;(tx as any).__kronos_txPromise = txPromise
|
|
58
|
+
|
|
59
|
+
return tx
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
async commit(tx: KyselyTransaction): Promise<void> {
|
|
63
|
+
const commit = (tx as any).__kronos_commit as () => void
|
|
64
|
+
const txPromise = (tx as any).__kronos_txPromise as Promise<void>
|
|
65
|
+
commit()
|
|
66
|
+
await txPromise
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async rollback(tx: KyselyTransaction): Promise<void> {
|
|
70
|
+
const rollback = (tx as any).__kronos_rollback as (error: unknown) => void
|
|
71
|
+
const txPromise = (tx as any).__kronos_txPromise as Promise<void>
|
|
72
|
+
rollback(new Error("Transaction rolled back"))
|
|
73
|
+
try { await txPromise } catch { /* expected */ }
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
}
|