@kronos-ts/drizzle 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/drizzle-token-store.d.ts +62 -0
- package/dist/drizzle-token-store.d.ts.map +1 -0
- package/dist/drizzle-token-store.js +125 -0
- package/dist/drizzle-token-store.js.map +1 -0
- package/dist/drizzle-transaction-manager.d.ts +36 -0
- package/dist/drizzle-transaction-manager.d.ts.map +1 -0
- package/dist/drizzle-transaction-manager.js +63 -0
- package/dist/drizzle-transaction-manager.js.map +1 -0
- 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/package.json +58 -0
- package/src/drizzle-token-store.ts +194 -0
- package/src/drizzle-transaction-manager.ts +83 -0
- package/src/index.ts +10 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { TokenStore } from "@kronos-ts/messaging";
|
|
2
|
+
import type { DrizzleDatabaseLike } from "./drizzle-transaction-manager.js";
|
|
3
|
+
/**
|
|
4
|
+
* Token table configuration. Users pass their Drizzle table reference
|
|
5
|
+
* and the `eq`, `and`, `or`, `lt` operators from drizzle-orm.
|
|
6
|
+
*
|
|
7
|
+
* The table must match the Kronos `TokenEntry` schema:
|
|
8
|
+
*
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { pgTable, varchar, integer, primaryKey } from "drizzle-orm/pg-core"
|
|
11
|
+
*
|
|
12
|
+
* export const kronosTokenEntries = pgTable("kronos_token_entries", {
|
|
13
|
+
* processorName: varchar("processor_name", { length: 255 }).notNull(),
|
|
14
|
+
* segment: integer("segment").notNull(),
|
|
15
|
+
* mask: integer("mask").notNull().default(0),
|
|
16
|
+
* tokenType: varchar("token_type", { length: 255 }),
|
|
17
|
+
* token: varchar("token", { length: 10000 }),
|
|
18
|
+
* timestamp: varchar("timestamp", { length: 255 }),
|
|
19
|
+
* owner: varchar("owner", { length: 255 }),
|
|
20
|
+
* }, (table) => ({
|
|
21
|
+
* pk: primaryKey({ columns: [table.processorName, table.segment] }),
|
|
22
|
+
* }))
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export interface DrizzleTokenStoreConfig {
|
|
26
|
+
/** The Drizzle database instance. */
|
|
27
|
+
db: DrizzleDatabaseLike;
|
|
28
|
+
/** The Drizzle table reference for `kronos_token_entries`. */
|
|
29
|
+
table: any;
|
|
30
|
+
/** Drizzle `eq` operator. */
|
|
31
|
+
eq: (column: any, value: any) => any;
|
|
32
|
+
/** Drizzle `and` operator. */
|
|
33
|
+
and: (...conditions: any[]) => any;
|
|
34
|
+
/** Drizzle `or` operator. */
|
|
35
|
+
or: (...conditions: any[]) => any;
|
|
36
|
+
/** Drizzle `lt` operator. */
|
|
37
|
+
lt: (column: any, value: any) => any;
|
|
38
|
+
/** Drizzle `isNull` operator. */
|
|
39
|
+
isNull: (column: any) => any;
|
|
40
|
+
/** Claim timeout in ms. Default: 10000. */
|
|
41
|
+
claimTimeoutMs?: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Creates a TokenStore backed by Drizzle ORM.
|
|
45
|
+
*
|
|
46
|
+
* Participates in the active transaction via `getActiveTransaction()`.
|
|
47
|
+
*
|
|
48
|
+
* ```typescript
|
|
49
|
+
* import { eq, and, or, lt, isNull } from "drizzle-orm"
|
|
50
|
+
* import { drizzleTokenStore } from "@kronos-ts/drizzle"
|
|
51
|
+
* import { kronosTokenEntries } from "./schema"
|
|
52
|
+
*
|
|
53
|
+
* // tokenStore wiring to a kronos() App is pending a typed `tokenStore` slot
|
|
54
|
+
* // (Phase 9). For now, construct the store and pass it directly to the
|
|
55
|
+
* // tracking processor that owns it:
|
|
56
|
+
* const tokenStore = drizzleTokenStore({
|
|
57
|
+
* db, table: kronosTokenEntries, eq, and, or, lt, isNull,
|
|
58
|
+
* })
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare function drizzleTokenStore(config: DrizzleTokenStoreConfig): TokenStore;
|
|
62
|
+
//# sourceMappingURL=drizzle-token-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drizzle-token-store.d.ts","sourceRoot":"","sources":["../src/drizzle-token-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAiB,MAAM,sBAAsB,CAAA;AAErE,OAAO,KAAK,EAAE,mBAAmB,EAAsB,MAAM,kCAAkC,CAAA;AAE/F;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,uBAAuB;IACtC,qCAAqC;IACrC,EAAE,EAAE,mBAAmB,CAAA;IACvB,8DAA8D;IAC9D,KAAK,EAAE,GAAG,CAAA;IACV,6BAA6B;IAC7B,EAAE,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,KAAK,GAAG,CAAA;IACpC,8BAA8B;IAC9B,GAAG,EAAE,CAAC,GAAG,UAAU,EAAE,GAAG,EAAE,KAAK,GAAG,CAAA;IAClC,6BAA6B;IAC7B,EAAE,EAAE,CAAC,GAAG,UAAU,EAAE,GAAG,EAAE,KAAK,GAAG,CAAA;IACjC,6BAA6B;IAC7B,EAAE,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,KAAK,GAAG,CAAA;IACpC,iCAAiC;IACjC,MAAM,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,GAAG,CAAA;IAC5B,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAmBD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,uBAAuB,GAAG,UAAU,CAiH7E"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { getActiveTransaction, UnableToClaimTokenError, globalSequenceToken } from "@kronos-ts/messaging";
|
|
2
|
+
function serializeToken(token) {
|
|
3
|
+
return {
|
|
4
|
+
tokenType: "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 Drizzle ORM.
|
|
19
|
+
*
|
|
20
|
+
* Participates in the active transaction via `getActiveTransaction()`.
|
|
21
|
+
*
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import { eq, and, or, lt, isNull } from "drizzle-orm"
|
|
24
|
+
* import { drizzleTokenStore } from "@kronos-ts/drizzle"
|
|
25
|
+
* import { kronosTokenEntries } from "./schema"
|
|
26
|
+
*
|
|
27
|
+
* // tokenStore wiring to a kronos() App is pending a typed `tokenStore` slot
|
|
28
|
+
* // (Phase 9). For now, construct the store and pass it directly to the
|
|
29
|
+
* // tracking processor that owns it:
|
|
30
|
+
* const tokenStore = drizzleTokenStore({
|
|
31
|
+
* db, table: kronosTokenEntries, eq, and, or, lt, isNull,
|
|
32
|
+
* })
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function drizzleTokenStore(config) {
|
|
36
|
+
const { table, eq, and, or, lt, isNull } = config;
|
|
37
|
+
const claimTimeoutMs = config.claimTimeoutMs ?? 10000;
|
|
38
|
+
function getDb() {
|
|
39
|
+
return getActiveTransaction() ?? config.db;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
async store(processorName, segment, token) {
|
|
43
|
+
const db = getDb();
|
|
44
|
+
const { tokenType, token: tokenData } = serializeToken(token);
|
|
45
|
+
await db.insert(table)
|
|
46
|
+
.values({ processorName, segment, mask: 0, tokenType, token: tokenData, timestamp: nowIso(), owner: null })
|
|
47
|
+
.onConflictDoUpdate({
|
|
48
|
+
target: [table.processorName, table.segment],
|
|
49
|
+
set: { tokenType, token: tokenData, timestamp: nowIso() },
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
async get(processorName, segment) {
|
|
53
|
+
const db = getDb();
|
|
54
|
+
const rows = await db.select().from(table)
|
|
55
|
+
.where(and(eq(table.processorName, processorName), eq(table.segment, segment)))
|
|
56
|
+
.limit(1);
|
|
57
|
+
if (rows.length === 0)
|
|
58
|
+
return undefined;
|
|
59
|
+
return deserializeToken(rows[0].tokenType, rows[0].token);
|
|
60
|
+
},
|
|
61
|
+
async initializeSegments(processorName, segmentCount) {
|
|
62
|
+
const db = getDb();
|
|
63
|
+
for (let i = 0; i < segmentCount; i++) {
|
|
64
|
+
await db.insert(table)
|
|
65
|
+
.values({ processorName, segment: i, mask: 0, tokenType: null, token: null, timestamp: null, owner: null })
|
|
66
|
+
.onConflictDoNothing();
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
async claimToken(processorName, segment, ownerId) {
|
|
70
|
+
const db = getDb();
|
|
71
|
+
const rows = await db.select().from(table)
|
|
72
|
+
.where(and(eq(table.processorName, processorName), eq(table.segment, segment)))
|
|
73
|
+
.limit(1);
|
|
74
|
+
if (rows.length === 0) {
|
|
75
|
+
await db.insert(table).values({
|
|
76
|
+
processorName, segment, mask: 0, tokenType: null, token: null, timestamp: nowIso(), owner: ownerId,
|
|
77
|
+
});
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
const row = rows[0];
|
|
81
|
+
const isExpired = !row.owner || !row.timestamp ||
|
|
82
|
+
(Date.now() - new Date(row.timestamp).getTime() > claimTimeoutMs);
|
|
83
|
+
if (row.owner === ownerId || isExpired) {
|
|
84
|
+
await db.update(table)
|
|
85
|
+
.set({ owner: ownerId, timestamp: nowIso() })
|
|
86
|
+
.where(and(eq(table.processorName, processorName), eq(table.segment, segment)));
|
|
87
|
+
return deserializeToken(row.tokenType, row.token);
|
|
88
|
+
}
|
|
89
|
+
throw new UnableToClaimTokenError(processorName, segment);
|
|
90
|
+
},
|
|
91
|
+
async extendClaim(processorName, segment, ownerId) {
|
|
92
|
+
const db = getDb();
|
|
93
|
+
await db.update(table)
|
|
94
|
+
.set({ timestamp: nowIso() })
|
|
95
|
+
.where(and(eq(table.processorName, processorName), eq(table.segment, segment), eq(table.owner, ownerId)));
|
|
96
|
+
},
|
|
97
|
+
async releaseClaim(processorName, segment, ownerId) {
|
|
98
|
+
const db = getDb();
|
|
99
|
+
await db.update(table)
|
|
100
|
+
.set({ owner: null, timestamp: null })
|
|
101
|
+
.where(and(eq(table.processorName, processorName), eq(table.segment, segment), eq(table.owner, ownerId)));
|
|
102
|
+
},
|
|
103
|
+
async fetchSegments(processorName) {
|
|
104
|
+
const db = getDb();
|
|
105
|
+
const rows = await db.select({ segment: table.segment }).from(table)
|
|
106
|
+
.where(eq(table.processorName, processorName))
|
|
107
|
+
.orderBy(table.segment);
|
|
108
|
+
return rows.map((r) => r.segment);
|
|
109
|
+
},
|
|
110
|
+
async fetchAvailableSegments(processorName) {
|
|
111
|
+
const db = getDb();
|
|
112
|
+
const cutoff = new Date(Date.now() - claimTimeoutMs).toISOString();
|
|
113
|
+
const rows = await db.select({ segment: table.segment }).from(table)
|
|
114
|
+
.where(and(eq(table.processorName, processorName), or(isNull(table.owner), lt(table.timestamp, cutoff))))
|
|
115
|
+
.orderBy(table.segment);
|
|
116
|
+
return rows.map((r) => r.segment);
|
|
117
|
+
},
|
|
118
|
+
async deleteToken(processorName, segment) {
|
|
119
|
+
const db = getDb();
|
|
120
|
+
await db.delete(table)
|
|
121
|
+
.where(and(eq(table.processorName, processorName), eq(table.segment, segment)));
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=drizzle-token-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drizzle-token-store.js","sourceRoot":"","sources":["../src/drizzle-token-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { TransactionManager } from "@kronos-ts/messaging";
|
|
2
|
+
/**
|
|
3
|
+
* A Drizzle database instance that supports transactions.
|
|
4
|
+
* Works with any Drizzle driver (postgres-js, node-postgres, better-sqlite3, etc.).
|
|
5
|
+
*/
|
|
6
|
+
export interface DrizzleDatabaseLike {
|
|
7
|
+
transaction<T>(fn: (tx: any) => Promise<T>): Promise<T>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The Drizzle transaction object — has the same query API as the database.
|
|
11
|
+
*/
|
|
12
|
+
export type DrizzleTransaction = any;
|
|
13
|
+
/**
|
|
14
|
+
* Creates a TransactionManager for Drizzle ORM.
|
|
15
|
+
*
|
|
16
|
+
* Drizzle's `db.transaction()` uses a callback pattern. This manager
|
|
17
|
+
* bridges it to the framework's `begin/commit/rollback` lifecycle
|
|
18
|
+
* using deferred promises — the same pattern as the Prisma extension.
|
|
19
|
+
*
|
|
20
|
+
* The `tx` is made available via `getActiveTransaction()` so projections
|
|
21
|
+
* and token stores participate in the same transaction.
|
|
22
|
+
*
|
|
23
|
+
* ```typescript
|
|
24
|
+
* import { drizzle } from "drizzle-orm/postgres-js"
|
|
25
|
+
* import { drizzleTransactionManager } from "@kronos-ts/drizzle"
|
|
26
|
+
*
|
|
27
|
+
* const db = drizzle(sql)
|
|
28
|
+
*
|
|
29
|
+
* // transactionManager wiring to a kronos() App is pending a typed
|
|
30
|
+
* // `transactionManager` slot (Phase 9). For now, construct the manager
|
|
31
|
+
* // and pass it directly into the unitOfWorkFactory composition:
|
|
32
|
+
* const txManager = drizzleTransactionManager(db)
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function drizzleTransactionManager(db: DrizzleDatabaseLike): TransactionManager<DrizzleTransaction>;
|
|
36
|
+
//# sourceMappingURL=drizzle-transaction-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drizzle-transaction-manager.d.ts","sourceRoot":"","sources":["../src/drizzle-transaction-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAE9D;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;CACxD;AAED;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,GAAG,CAAA;AAEpC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,mBAAmB,GACtB,kBAAkB,CAAC,kBAAkB,CAAC,CA2CxC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a TransactionManager for Drizzle ORM.
|
|
3
|
+
*
|
|
4
|
+
* Drizzle's `db.transaction()` uses a callback pattern. This manager
|
|
5
|
+
* bridges it to the framework's `begin/commit/rollback` lifecycle
|
|
6
|
+
* using deferred promises — the same pattern as the Prisma extension.
|
|
7
|
+
*
|
|
8
|
+
* The `tx` is made available via `getActiveTransaction()` so projections
|
|
9
|
+
* and token stores participate in the same transaction.
|
|
10
|
+
*
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { drizzle } from "drizzle-orm/postgres-js"
|
|
13
|
+
* import { drizzleTransactionManager } from "@kronos-ts/drizzle"
|
|
14
|
+
*
|
|
15
|
+
* const db = drizzle(sql)
|
|
16
|
+
*
|
|
17
|
+
* // transactionManager wiring to a kronos() App is pending a typed
|
|
18
|
+
* // `transactionManager` slot (Phase 9). For now, construct the manager
|
|
19
|
+
* // and pass it directly into the unitOfWorkFactory composition:
|
|
20
|
+
* const txManager = drizzleTransactionManager(db)
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function drizzleTransactionManager(db) {
|
|
24
|
+
return {
|
|
25
|
+
async begin() {
|
|
26
|
+
let resolveTx;
|
|
27
|
+
let resolveCompletion;
|
|
28
|
+
let rejectCompletion;
|
|
29
|
+
const txReady = new Promise((resolve) => {
|
|
30
|
+
resolveTx = resolve;
|
|
31
|
+
});
|
|
32
|
+
const completionSignal = new Promise((resolve, reject) => {
|
|
33
|
+
resolveCompletion = resolve;
|
|
34
|
+
rejectCompletion = reject;
|
|
35
|
+
});
|
|
36
|
+
const txPromise = db.transaction(async (tx) => {
|
|
37
|
+
resolveTx(tx);
|
|
38
|
+
await completionSignal;
|
|
39
|
+
});
|
|
40
|
+
const tx = await txReady;
|
|
41
|
+
tx.__kronos_commit = resolveCompletion;
|
|
42
|
+
tx.__kronos_rollback = rejectCompletion;
|
|
43
|
+
tx.__kronos_txPromise = txPromise;
|
|
44
|
+
return tx;
|
|
45
|
+
},
|
|
46
|
+
async commit(tx) {
|
|
47
|
+
const commit = tx.__kronos_commit;
|
|
48
|
+
const txPromise = tx.__kronos_txPromise;
|
|
49
|
+
commit();
|
|
50
|
+
await txPromise;
|
|
51
|
+
},
|
|
52
|
+
async rollback(tx) {
|
|
53
|
+
const rollback = tx.__kronos_rollback;
|
|
54
|
+
const txPromise = tx.__kronos_txPromise;
|
|
55
|
+
rollback(new Error("Transaction rolled back"));
|
|
56
|
+
try {
|
|
57
|
+
await txPromise;
|
|
58
|
+
}
|
|
59
|
+
catch { /* expected */ }
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=drizzle-transaction-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drizzle-transaction-manager.js","sourceRoot":"","sources":["../src/drizzle-transaction-manager.ts"],"names":[],"mappings":"AAeA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,yBAAyB,CACvC,EAAuB;IAEvB,OAAO;QACL,KAAK,CAAC,KAAK;YACT,IAAI,SAA4C,CAAA;YAChD,IAAI,iBAA8B,CAAA;YAClC,IAAI,gBAA2C,CAAA;YAE/C,MAAM,OAAO,GAAG,IAAI,OAAO,CAAqB,CAAC,OAAO,EAAE,EAAE;gBAC1D,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,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;gBAC5C,SAAS,CAAC,EAAE,CAAC,CAAA;gBACb,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,EAAsB;YACjC,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,EAAsB;YACnC,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/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,yBAAyB,EACzB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,GACxB,MAAM,kCAAkC,CAAA;AAEzC,OAAO,EACL,iBAAiB,EACjB,KAAK,uBAAuB,GAC7B,MAAM,0BAA0B,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,yBAAyB,GAG1B,MAAM,kCAAkC,CAAA;AAEzC,OAAO,EACL,iBAAiB,GAElB,MAAM,0BAA0B,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kronos-ts/drizzle",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Drizzle ORM 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/drizzle#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/KronosDB/kronos-ts.git",
|
|
12
|
+
"directory": "packages/extensions/drizzle"
|
|
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
|
+
"drizzle",
|
|
24
|
+
"orm"
|
|
25
|
+
],
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"main": "src/index.ts",
|
|
28
|
+
"types": "src/index.ts",
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"src",
|
|
32
|
+
"!src/**/__tests__",
|
|
33
|
+
"!src/**/*.test.ts",
|
|
34
|
+
"!src/**/*.bench.ts"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -p tsconfig.json",
|
|
38
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"main": "./dist/index.js",
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
44
|
+
"exports": {
|
|
45
|
+
".": {
|
|
46
|
+
"types": "./dist/index.d.ts",
|
|
47
|
+
"default": "./dist/index.js"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@kronos-ts/common": "workspace:*",
|
|
53
|
+
"@kronos-ts/messaging": "workspace:*"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"drizzle-orm": ">=0.30.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { TokenStore, TrackingToken } from "@kronos-ts/messaging"
|
|
2
|
+
import { getActiveTransaction, UnableToClaimTokenError, globalSequenceToken } from "@kronos-ts/messaging"
|
|
3
|
+
import type { DrizzleDatabaseLike, DrizzleTransaction } from "./drizzle-transaction-manager.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Token table configuration. Users pass their Drizzle table reference
|
|
7
|
+
* and the `eq`, `and`, `or`, `lt` operators from drizzle-orm.
|
|
8
|
+
*
|
|
9
|
+
* The table must match the Kronos `TokenEntry` schema:
|
|
10
|
+
*
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { pgTable, varchar, integer, primaryKey } from "drizzle-orm/pg-core"
|
|
13
|
+
*
|
|
14
|
+
* export const kronosTokenEntries = pgTable("kronos_token_entries", {
|
|
15
|
+
* processorName: varchar("processor_name", { length: 255 }).notNull(),
|
|
16
|
+
* segment: integer("segment").notNull(),
|
|
17
|
+
* mask: integer("mask").notNull().default(0),
|
|
18
|
+
* tokenType: varchar("token_type", { length: 255 }),
|
|
19
|
+
* token: varchar("token", { length: 10000 }),
|
|
20
|
+
* timestamp: varchar("timestamp", { length: 255 }),
|
|
21
|
+
* owner: varchar("owner", { length: 255 }),
|
|
22
|
+
* }, (table) => ({
|
|
23
|
+
* pk: primaryKey({ columns: [table.processorName, table.segment] }),
|
|
24
|
+
* }))
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export interface DrizzleTokenStoreConfig {
|
|
28
|
+
/** The Drizzle database instance. */
|
|
29
|
+
db: DrizzleDatabaseLike
|
|
30
|
+
/** The Drizzle table reference for `kronos_token_entries`. */
|
|
31
|
+
table: any
|
|
32
|
+
/** Drizzle `eq` operator. */
|
|
33
|
+
eq: (column: any, value: any) => any
|
|
34
|
+
/** Drizzle `and` operator. */
|
|
35
|
+
and: (...conditions: any[]) => any
|
|
36
|
+
/** Drizzle `or` operator. */
|
|
37
|
+
or: (...conditions: any[]) => any
|
|
38
|
+
/** Drizzle `lt` operator. */
|
|
39
|
+
lt: (column: any, value: any) => any
|
|
40
|
+
/** Drizzle `isNull` operator. */
|
|
41
|
+
isNull: (column: any) => any
|
|
42
|
+
/** Claim timeout in ms. Default: 10000. */
|
|
43
|
+
claimTimeoutMs?: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function serializeToken(token: TrackingToken): { tokenType: string; token: string } {
|
|
47
|
+
return {
|
|
48
|
+
tokenType: "GlobalSequenceToken",
|
|
49
|
+
token: JSON.stringify({ position: token.position().toString() }),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function deserializeToken(tokenType: string | null, token: string | null): TrackingToken | undefined {
|
|
54
|
+
if (!token || !tokenType) return undefined
|
|
55
|
+
const data = JSON.parse(token)
|
|
56
|
+
return globalSequenceToken(BigInt(data.position))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function nowIso(): string {
|
|
60
|
+
return new Date().toISOString()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Creates a TokenStore backed by Drizzle ORM.
|
|
65
|
+
*
|
|
66
|
+
* Participates in the active transaction via `getActiveTransaction()`.
|
|
67
|
+
*
|
|
68
|
+
* ```typescript
|
|
69
|
+
* import { eq, and, or, lt, isNull } from "drizzle-orm"
|
|
70
|
+
* import { drizzleTokenStore } from "@kronos-ts/drizzle"
|
|
71
|
+
* import { kronosTokenEntries } from "./schema"
|
|
72
|
+
*
|
|
73
|
+
* // tokenStore wiring to a kronos() App is pending a typed `tokenStore` slot
|
|
74
|
+
* // (Phase 9). For now, construct the store and pass it directly to the
|
|
75
|
+
* // tracking processor that owns it:
|
|
76
|
+
* const tokenStore = drizzleTokenStore({
|
|
77
|
+
* db, table: kronosTokenEntries, eq, and, or, lt, isNull,
|
|
78
|
+
* })
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export function drizzleTokenStore(config: DrizzleTokenStoreConfig): TokenStore {
|
|
82
|
+
const { table, eq, and, or, lt, isNull } = config
|
|
83
|
+
const claimTimeoutMs = config.claimTimeoutMs ?? 10000
|
|
84
|
+
|
|
85
|
+
function getDb(): any {
|
|
86
|
+
return getActiveTransaction<DrizzleTransaction>() ?? config.db
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
async store(processorName, segment, token) {
|
|
91
|
+
const db = getDb()
|
|
92
|
+
const { tokenType, token: tokenData } = serializeToken(token)
|
|
93
|
+
await db.insert(table)
|
|
94
|
+
.values({ processorName, segment, mask: 0, tokenType, token: tokenData, timestamp: nowIso(), owner: null })
|
|
95
|
+
.onConflictDoUpdate({
|
|
96
|
+
target: [table.processorName, table.segment],
|
|
97
|
+
set: { tokenType, token: tokenData, timestamp: nowIso() },
|
|
98
|
+
})
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
async get(processorName, segment) {
|
|
102
|
+
const db = getDb()
|
|
103
|
+
const rows = await db.select().from(table)
|
|
104
|
+
.where(and(eq(table.processorName, processorName), eq(table.segment, segment)))
|
|
105
|
+
.limit(1)
|
|
106
|
+
if (rows.length === 0) return undefined
|
|
107
|
+
return deserializeToken(rows[0].tokenType, rows[0].token)
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
async initializeSegments(processorName, segmentCount) {
|
|
111
|
+
const db = getDb()
|
|
112
|
+
for (let i = 0; i < segmentCount; i++) {
|
|
113
|
+
await db.insert(table)
|
|
114
|
+
.values({ processorName, segment: i, mask: 0, tokenType: null, token: null, timestamp: null, owner: null })
|
|
115
|
+
.onConflictDoNothing()
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
async claimToken(processorName, segment, ownerId) {
|
|
120
|
+
const db = getDb()
|
|
121
|
+
const rows = await db.select().from(table)
|
|
122
|
+
.where(and(eq(table.processorName, processorName), eq(table.segment, segment)))
|
|
123
|
+
.limit(1)
|
|
124
|
+
|
|
125
|
+
if (rows.length === 0) {
|
|
126
|
+
await db.insert(table).values({
|
|
127
|
+
processorName, segment, mask: 0, tokenType: null, token: null, timestamp: nowIso(), owner: ownerId,
|
|
128
|
+
})
|
|
129
|
+
return undefined
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const row = rows[0]
|
|
133
|
+
const isExpired = !row.owner || !row.timestamp ||
|
|
134
|
+
(Date.now() - new Date(row.timestamp).getTime() > claimTimeoutMs)
|
|
135
|
+
|
|
136
|
+
if (row.owner === ownerId || isExpired) {
|
|
137
|
+
await db.update(table)
|
|
138
|
+
.set({ owner: ownerId, timestamp: nowIso() })
|
|
139
|
+
.where(and(eq(table.processorName, processorName), eq(table.segment, segment)))
|
|
140
|
+
return deserializeToken(row.tokenType, row.token)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
throw new UnableToClaimTokenError(processorName, segment)
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
async extendClaim(processorName, segment, ownerId) {
|
|
147
|
+
const db = getDb()
|
|
148
|
+
await db.update(table)
|
|
149
|
+
.set({ timestamp: nowIso() })
|
|
150
|
+
.where(and(
|
|
151
|
+
eq(table.processorName, processorName),
|
|
152
|
+
eq(table.segment, segment),
|
|
153
|
+
eq(table.owner, ownerId),
|
|
154
|
+
))
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
async releaseClaim(processorName, segment, ownerId) {
|
|
158
|
+
const db = getDb()
|
|
159
|
+
await db.update(table)
|
|
160
|
+
.set({ owner: null, timestamp: null })
|
|
161
|
+
.where(and(
|
|
162
|
+
eq(table.processorName, processorName),
|
|
163
|
+
eq(table.segment, segment),
|
|
164
|
+
eq(table.owner, ownerId),
|
|
165
|
+
))
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
async fetchSegments(processorName) {
|
|
169
|
+
const db = getDb()
|
|
170
|
+
const rows = await db.select({ segment: table.segment }).from(table)
|
|
171
|
+
.where(eq(table.processorName, processorName))
|
|
172
|
+
.orderBy(table.segment)
|
|
173
|
+
return rows.map((r: any) => r.segment)
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
async fetchAvailableSegments(processorName) {
|
|
177
|
+
const db = getDb()
|
|
178
|
+
const cutoff = new Date(Date.now() - claimTimeoutMs).toISOString()
|
|
179
|
+
const rows = await db.select({ segment: table.segment }).from(table)
|
|
180
|
+
.where(and(
|
|
181
|
+
eq(table.processorName, processorName),
|
|
182
|
+
or(isNull(table.owner), lt(table.timestamp, cutoff)),
|
|
183
|
+
))
|
|
184
|
+
.orderBy(table.segment)
|
|
185
|
+
return rows.map((r: any) => r.segment)
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
async deleteToken(processorName, segment) {
|
|
189
|
+
const db = getDb()
|
|
190
|
+
await db.delete(table)
|
|
191
|
+
.where(and(eq(table.processorName, processorName), eq(table.segment, segment)))
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { TransactionManager } from "@kronos-ts/messaging"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A Drizzle database instance that supports transactions.
|
|
5
|
+
* Works with any Drizzle driver (postgres-js, node-postgres, better-sqlite3, etc.).
|
|
6
|
+
*/
|
|
7
|
+
export interface DrizzleDatabaseLike {
|
|
8
|
+
transaction<T>(fn: (tx: any) => Promise<T>): Promise<T>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The Drizzle transaction object — has the same query API as the database.
|
|
13
|
+
*/
|
|
14
|
+
export type DrizzleTransaction = any
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a TransactionManager for Drizzle ORM.
|
|
18
|
+
*
|
|
19
|
+
* Drizzle's `db.transaction()` uses a callback pattern. This manager
|
|
20
|
+
* bridges it to the framework's `begin/commit/rollback` lifecycle
|
|
21
|
+
* using deferred promises — the same pattern as the Prisma extension.
|
|
22
|
+
*
|
|
23
|
+
* The `tx` is made available via `getActiveTransaction()` so projections
|
|
24
|
+
* and token stores participate in the same transaction.
|
|
25
|
+
*
|
|
26
|
+
* ```typescript
|
|
27
|
+
* import { drizzle } from "drizzle-orm/postgres-js"
|
|
28
|
+
* import { drizzleTransactionManager } from "@kronos-ts/drizzle"
|
|
29
|
+
*
|
|
30
|
+
* const db = drizzle(sql)
|
|
31
|
+
*
|
|
32
|
+
* // transactionManager wiring to a kronos() App is pending a typed
|
|
33
|
+
* // `transactionManager` slot (Phase 9). For now, construct the manager
|
|
34
|
+
* // and pass it directly into the unitOfWorkFactory composition:
|
|
35
|
+
* const txManager = drizzleTransactionManager(db)
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function drizzleTransactionManager(
|
|
39
|
+
db: DrizzleDatabaseLike,
|
|
40
|
+
): TransactionManager<DrizzleTransaction> {
|
|
41
|
+
return {
|
|
42
|
+
async begin(): Promise<DrizzleTransaction> {
|
|
43
|
+
let resolveTx!: (tx: DrizzleTransaction) => void
|
|
44
|
+
let resolveCompletion!: () => void
|
|
45
|
+
let rejectCompletion!: (error: unknown) => void
|
|
46
|
+
|
|
47
|
+
const txReady = new Promise<DrizzleTransaction>((resolve) => {
|
|
48
|
+
resolveTx = resolve
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const completionSignal = new Promise<void>((resolve, reject) => {
|
|
52
|
+
resolveCompletion = resolve
|
|
53
|
+
rejectCompletion = reject
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const txPromise = db.transaction(async (tx) => {
|
|
57
|
+
resolveTx(tx)
|
|
58
|
+
await completionSignal
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const tx = await txReady
|
|
62
|
+
;(tx as any).__kronos_commit = resolveCompletion
|
|
63
|
+
;(tx as any).__kronos_rollback = rejectCompletion
|
|
64
|
+
;(tx as any).__kronos_txPromise = txPromise
|
|
65
|
+
|
|
66
|
+
return tx
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async commit(tx: DrizzleTransaction): Promise<void> {
|
|
70
|
+
const commit = (tx as any).__kronos_commit as () => void
|
|
71
|
+
const txPromise = (tx as any).__kronos_txPromise as Promise<void>
|
|
72
|
+
commit()
|
|
73
|
+
await txPromise
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
async rollback(tx: DrizzleTransaction): Promise<void> {
|
|
77
|
+
const rollback = (tx as any).__kronos_rollback as (error: unknown) => void
|
|
78
|
+
const txPromise = (tx as any).__kronos_txPromise as Promise<void>
|
|
79
|
+
rollback(new Error("Transaction rolled back"))
|
|
80
|
+
try { await txPromise } catch { /* expected */ }
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/index.ts
ADDED