@hasna/economy 0.3.4 → 0.3.6
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/cli/index.js +34 -9
- package/dist/db/sync-pg.d.ts +20 -0
- package/dist/db/sync-pg.d.ts.map +1 -1
- package/dist/server/index.js +34 -9
- package/package.json +3 -3
package/dist/cli/index.js
CHANGED
|
@@ -6394,11 +6394,16 @@ function resolveWorkerPath() {
|
|
|
6394
6394
|
return candidate;
|
|
6395
6395
|
return candidates[0];
|
|
6396
6396
|
}
|
|
6397
|
+
function isTransientConnError(message) {
|
|
6398
|
+
const m = message.toLowerCase();
|
|
6399
|
+
return m.includes("econnreset") || m.includes("econnrefused") || m.includes("etimedout") || m.includes("connection terminated") || m.includes("connection ended") || m.includes("timeout exceeded when trying to connect") || m.includes("terminating connection") || m.includes("server closed the connection");
|
|
6400
|
+
}
|
|
6397
6401
|
|
|
6398
6402
|
class SyncPgAdapter {
|
|
6399
6403
|
worker;
|
|
6400
6404
|
port;
|
|
6401
6405
|
counter = 0;
|
|
6406
|
+
sleeper = new Int32Array(new SharedArrayBuffer(4));
|
|
6402
6407
|
constructor(dsn) {
|
|
6403
6408
|
const workerPath = resolveWorkerPath();
|
|
6404
6409
|
this.worker = new Worker(workerPath, { workerData: { dsn } });
|
|
@@ -6407,25 +6412,45 @@ class SyncPgAdapter {
|
|
|
6407
6412
|
this.port = channel.port1;
|
|
6408
6413
|
this.worker.postMessage({ type: "init", port: channel.port2 }, [channel.port2]);
|
|
6409
6414
|
}
|
|
6410
|
-
|
|
6411
|
-
const
|
|
6412
|
-
|
|
6415
|
+
drainReply(expectedId) {
|
|
6416
|
+
const deadline = Date.now() + 30000;
|
|
6417
|
+
for (;; ) {
|
|
6418
|
+
const received = receiveMessageOnPort(this.port);
|
|
6419
|
+
if (received) {
|
|
6420
|
+
const reply = received.message;
|
|
6421
|
+
if (reply.id === expectedId)
|
|
6422
|
+
return reply;
|
|
6423
|
+
continue;
|
|
6424
|
+
}
|
|
6425
|
+
if (Date.now() > deadline)
|
|
6426
|
+
throw new Error("SyncPgAdapter: no reply from worker (timeout)");
|
|
6427
|
+
Atomics.wait(this.sleeper, 0, 0, 1);
|
|
6428
|
+
}
|
|
6429
|
+
}
|
|
6430
|
+
queryOnce(pgSql, pgParams) {
|
|
6413
6431
|
const id = ++this.counter;
|
|
6414
6432
|
const signal = new SharedArrayBuffer(4);
|
|
6415
6433
|
const sig = new Int32Array(signal);
|
|
6416
6434
|
Atomics.store(sig, 0, 0);
|
|
6417
6435
|
this.worker.postMessage({ id, sql: pgSql, params: pgParams, signal });
|
|
6418
6436
|
Atomics.wait(sig, 0, 0);
|
|
6419
|
-
|
|
6420
|
-
for (let attempt = 0;!received && attempt < 1e6; attempt++)
|
|
6421
|
-
received = receiveMessageOnPort(this.port);
|
|
6422
|
-
if (!received)
|
|
6423
|
-
throw new Error("SyncPgAdapter: no reply from worker");
|
|
6424
|
-
const reply = received.message;
|
|
6437
|
+
const reply = this.drainReply(id);
|
|
6425
6438
|
if (!reply.ok)
|
|
6426
6439
|
throw new Error(reply.error);
|
|
6427
6440
|
return { rows: reply.rows, rowCount: reply.rowCount };
|
|
6428
6441
|
}
|
|
6442
|
+
query(sql, params) {
|
|
6443
|
+
const pgSql = translateSqliteDates(translateSql(sql, "pg"));
|
|
6444
|
+
const pgParams = translateParams(params);
|
|
6445
|
+
try {
|
|
6446
|
+
return this.queryOnce(pgSql, pgParams);
|
|
6447
|
+
} catch (err) {
|
|
6448
|
+
if (err instanceof Error && isTransientConnError(err.message)) {
|
|
6449
|
+
return this.queryOnce(pgSql, pgParams);
|
|
6450
|
+
}
|
|
6451
|
+
throw err;
|
|
6452
|
+
}
|
|
6453
|
+
}
|
|
6429
6454
|
run(sql, ...params) {
|
|
6430
6455
|
const result = this.query(sql, params);
|
|
6431
6456
|
return { changes: result.rowCount, lastInsertRowid: result.rows?.[0]?.["id"] ?? 0 };
|
package/dist/db/sync-pg.d.ts
CHANGED
|
@@ -6,11 +6,31 @@ import { type DbAdapter, type PreparedStatement, type RunResult } from '@hasna/c
|
|
|
6
6
|
* `DATE(col)` casts through `timestamptz`. Ordered most-specific first.
|
|
7
7
|
*/
|
|
8
8
|
export declare function translateSqliteDates(sql: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Connection-level failures where the query provably never executed (the pool
|
|
11
|
+
* could not hand out or keep a live backend). These are safe to retry once —
|
|
12
|
+
* the pg pool discards the dead client and dials a fresh one on the next query,
|
|
13
|
+
* so a single retry transparently absorbs RDS failover / idle-drop / connect
|
|
14
|
+
* timeout blips instead of surfacing a 500 the caller has to retry by hand.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isTransientConnError(message: string): boolean;
|
|
9
17
|
export declare class SyncPgAdapter implements DbAdapter {
|
|
10
18
|
private readonly worker;
|
|
11
19
|
private readonly port;
|
|
12
20
|
private counter;
|
|
21
|
+
private readonly sleeper;
|
|
13
22
|
constructor(dsn: string);
|
|
23
|
+
/**
|
|
24
|
+
* Drain the reply matching `expectedId`. The reply is enqueued on the port
|
|
25
|
+
* before the worker notifies, but cross-thread delivery can lag under CPU
|
|
26
|
+
* pressure — so poll on a wall-clock deadline (yielding via a 1ms Atomics.wait
|
|
27
|
+
* between empty reads) rather than a fixed iteration count, which could give
|
|
28
|
+
* up prematurely on a busy box and throw a spurious 500. Replies whose id does
|
|
29
|
+
* not match are stale results of a previously abandoned (timed-out) query;
|
|
30
|
+
* discard them so the port never cross-talks between requests.
|
|
31
|
+
*/
|
|
32
|
+
private drainReply;
|
|
33
|
+
private queryOnce;
|
|
14
34
|
private query;
|
|
15
35
|
run(sql: string, ...params: unknown[]): RunResult;
|
|
16
36
|
get(sql: string, ...params: unknown[]): unknown;
|
package/dist/db/sync-pg.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-pg.d.ts","sourceRoot":"","sources":["../../src/db/sync-pg.ts"],"names":[],"mappings":"AAeA,OAAO,EAAiC,KAAK,SAAS,EAAE,KAAK,iBAAiB,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAIpH;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAiCxD;AAaD,qBAAa,aAAc,YAAW,SAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;IAClC,OAAO,CAAC,OAAO,CAAI;
|
|
1
|
+
{"version":3,"file":"sync-pg.d.ts","sourceRoot":"","sources":["../../src/db/sync-pg.ts"],"names":[],"mappings":"AAeA,OAAO,EAAiC,KAAK,SAAS,EAAE,KAAK,iBAAiB,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAIpH;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAiCxD;AAaD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAY7D;AAED,qBAAa,aAAc,YAAW,SAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;IAClC,OAAO,CAAC,OAAO,CAAI;IAInB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2C;gBAEvD,GAAG,EAAE,MAAM;IAYvB;;;;;;;;OAQG;IACH,OAAO,CAAC,UAAU;IAclB,OAAO,CAAC,SAAS;IAcjB,OAAO,CAAC,KAAK;IAcb,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS;IAIjD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO;IAG/C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE;IAGjD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAGvB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB;IASvC,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC;IAI9B,KAAK,IAAI,IAAI;CAGd"}
|
package/dist/server/index.js
CHANGED
|
@@ -1988,11 +1988,16 @@ function resolveWorkerPath() {
|
|
|
1988
1988
|
return candidate;
|
|
1989
1989
|
return candidates[0];
|
|
1990
1990
|
}
|
|
1991
|
+
function isTransientConnError(message) {
|
|
1992
|
+
const m = message.toLowerCase();
|
|
1993
|
+
return m.includes("econnreset") || m.includes("econnrefused") || m.includes("etimedout") || m.includes("connection terminated") || m.includes("connection ended") || m.includes("timeout exceeded when trying to connect") || m.includes("terminating connection") || m.includes("server closed the connection");
|
|
1994
|
+
}
|
|
1991
1995
|
|
|
1992
1996
|
class SyncPgAdapter {
|
|
1993
1997
|
worker;
|
|
1994
1998
|
port;
|
|
1995
1999
|
counter = 0;
|
|
2000
|
+
sleeper = new Int32Array(new SharedArrayBuffer(4));
|
|
1996
2001
|
constructor(dsn) {
|
|
1997
2002
|
const workerPath = resolveWorkerPath();
|
|
1998
2003
|
this.worker = new Worker(workerPath, { workerData: { dsn } });
|
|
@@ -2001,25 +2006,45 @@ class SyncPgAdapter {
|
|
|
2001
2006
|
this.port = channel.port1;
|
|
2002
2007
|
this.worker.postMessage({ type: "init", port: channel.port2 }, [channel.port2]);
|
|
2003
2008
|
}
|
|
2004
|
-
|
|
2005
|
-
const
|
|
2006
|
-
|
|
2009
|
+
drainReply(expectedId) {
|
|
2010
|
+
const deadline = Date.now() + 30000;
|
|
2011
|
+
for (;; ) {
|
|
2012
|
+
const received = receiveMessageOnPort(this.port);
|
|
2013
|
+
if (received) {
|
|
2014
|
+
const reply = received.message;
|
|
2015
|
+
if (reply.id === expectedId)
|
|
2016
|
+
return reply;
|
|
2017
|
+
continue;
|
|
2018
|
+
}
|
|
2019
|
+
if (Date.now() > deadline)
|
|
2020
|
+
throw new Error("SyncPgAdapter: no reply from worker (timeout)");
|
|
2021
|
+
Atomics.wait(this.sleeper, 0, 0, 1);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
queryOnce(pgSql, pgParams) {
|
|
2007
2025
|
const id = ++this.counter;
|
|
2008
2026
|
const signal = new SharedArrayBuffer(4);
|
|
2009
2027
|
const sig = new Int32Array(signal);
|
|
2010
2028
|
Atomics.store(sig, 0, 0);
|
|
2011
2029
|
this.worker.postMessage({ id, sql: pgSql, params: pgParams, signal });
|
|
2012
2030
|
Atomics.wait(sig, 0, 0);
|
|
2013
|
-
|
|
2014
|
-
for (let attempt = 0;!received && attempt < 1e6; attempt++)
|
|
2015
|
-
received = receiveMessageOnPort(this.port);
|
|
2016
|
-
if (!received)
|
|
2017
|
-
throw new Error("SyncPgAdapter: no reply from worker");
|
|
2018
|
-
const reply = received.message;
|
|
2031
|
+
const reply = this.drainReply(id);
|
|
2019
2032
|
if (!reply.ok)
|
|
2020
2033
|
throw new Error(reply.error);
|
|
2021
2034
|
return { rows: reply.rows, rowCount: reply.rowCount };
|
|
2022
2035
|
}
|
|
2036
|
+
query(sql, params) {
|
|
2037
|
+
const pgSql = translateSqliteDates(translateSql(sql, "pg"));
|
|
2038
|
+
const pgParams = translateParams(params);
|
|
2039
|
+
try {
|
|
2040
|
+
return this.queryOnce(pgSql, pgParams);
|
|
2041
|
+
} catch (err) {
|
|
2042
|
+
if (err instanceof Error && isTransientConnError(err.message)) {
|
|
2043
|
+
return this.queryOnce(pgSql, pgParams);
|
|
2044
|
+
}
|
|
2045
|
+
throw err;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2023
2048
|
run(sql, ...params) {
|
|
2024
2049
|
const result = this.query(sql, params);
|
|
2025
2050
|
return { changes: result.rowCount, lastInsertRowid: result.rows?.[0]?.["id"] ?? 0 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/economy",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "AI coding cost tracker — CLI + MCP server + REST API + web dashboard for Claude Code, Codex, Gemini, OpenCode, Cursor, Pi, and Hermes",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -70,11 +70,11 @@
|
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
72
|
"@hasna/accounts": "0.1.6",
|
|
73
|
-
"@hasna/agent-registry": "
|
|
73
|
+
"@hasna/agent-registry": "^0.1.0",
|
|
74
74
|
"@hasna/cloud": "0.1.24",
|
|
75
75
|
"@hasna/contracts": "^0.4.2",
|
|
76
76
|
"@hasna/events": "^0.1.7",
|
|
77
|
-
"@hasna/mcp-harness": "
|
|
77
|
+
"@hasna/mcp-harness": "^0.1.0",
|
|
78
78
|
"@hasna/projects": "0.1.51",
|
|
79
79
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
80
80
|
"chalk": "^5.4.1",
|