@mantajs/adapter-locking-neon 0.2.0-beta.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.
@@ -0,0 +1,28 @@
1
+ import type { ILockingPort } from '@mantajs/core';
2
+ type RawSqlFn = (strings: TemplateStringsArray, ...values: unknown[]) => Promise<Array<Record<string, unknown>>>;
3
+ export interface NeonLockingOptions {
4
+ rawSql?: RawSqlFn;
5
+ }
6
+ export declare class NeonLockingAdapter implements ILockingPort {
7
+ private _rawSql;
8
+ private _owners;
9
+ private _expireTimers;
10
+ constructor(rawSql: RawSqlFn);
11
+ execute<T>(keys: string[], job: () => Promise<T>, options?: {
12
+ timeout?: number;
13
+ }): Promise<T>;
14
+ acquire(keys: string | string[], options?: {
15
+ ownerId?: string;
16
+ expire?: number;
17
+ }): Promise<boolean>;
18
+ release(keys: string | string[], options?: {
19
+ ownerId?: string;
20
+ }): Promise<void>;
21
+ releaseAll(options?: {
22
+ ownerId?: string;
23
+ }): Promise<void>;
24
+ private _unlockKey;
25
+ _reset(): void;
26
+ }
27
+ export {};
28
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAIjD,KAAK,QAAQ,GAAG,CAAC,OAAO,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;AAEhH,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,QAAQ,CAAA;CAClB;AAED,qBAAa,kBAAmB,YAAW,YAAY;IACrD,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,OAAO,CAAiC;IAChD,OAAO,CAAC,aAAa,CAAmD;gBAE5D,MAAM,EAAE,QAAQ;IAUtB,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,CAAC,CAAC;IAe7F,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAiDnG,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAwB/E,UAAU,CAAC,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;YAwBjD,UAAU;IAKxB,MAAM;CAOP"}
@@ -0,0 +1,128 @@
1
+ // SPEC-066 — NeonLockingAdapter implements ILockingPort
2
+ // Uses PostgreSQL advisory locks via raw SQL. Advisory locks are session-scoped —
3
+ // in serverless, the connection closes and locks auto-release.
4
+ import { MantaError } from '@mantajs/core';
5
+ import { stringToAdvisoryLockKey } from './hash';
6
+ export class NeonLockingAdapter {
7
+ _rawSql;
8
+ _owners = new Map();
9
+ _expireTimers = new Map();
10
+ constructor(rawSql) {
11
+ if (!rawSql) {
12
+ throw new MantaError('INVALID_STATE', 'NeonLockingAdapter requires a rawSql function from IDatabasePort.getPool()');
13
+ }
14
+ this._rawSql = rawSql;
15
+ }
16
+ async execute(keys, job, options) {
17
+ const ownerId = crypto.randomUUID();
18
+ const acquired = await this.acquire(keys, { ownerId, expire: options?.timeout });
19
+ if (!acquired) {
20
+ throw new MantaError('CONFLICT', `Failed to acquire locks for keys: ${keys.join(', ')}`);
21
+ }
22
+ try {
23
+ return await job();
24
+ }
25
+ finally {
26
+ await this.release(keys, { ownerId });
27
+ }
28
+ }
29
+ async acquire(keys, options) {
30
+ const keyArray = Array.isArray(keys) ? keys : [keys];
31
+ const ownerId = options?.ownerId ?? crypto.randomUUID();
32
+ const acquiredKeys = [];
33
+ // Try to acquire all locks atomically (L-06)
34
+ for (const key of keyArray) {
35
+ const lockKey = stringToAdvisoryLockKey(key);
36
+ const result = await this._rawSql `SELECT pg_try_advisory_lock(${lockKey}::bigint) AS acquired`;
37
+ const acquired = result[0]?.acquired === true;
38
+ if (!acquired) {
39
+ // Rollback: release all previously acquired locks
40
+ for (const acquiredKey of acquiredKeys) {
41
+ const releaseKey = stringToAdvisoryLockKey(acquiredKey);
42
+ await this._rawSql `SELECT pg_advisory_unlock(${releaseKey}::bigint)`;
43
+ }
44
+ return false;
45
+ }
46
+ acquiredKeys.push(key);
47
+ }
48
+ // Track ownership
49
+ if (!this._owners.has(ownerId)) {
50
+ this._owners.set(ownerId, new Set());
51
+ }
52
+ for (const key of keyArray) {
53
+ this._owners.get(ownerId).add(key);
54
+ }
55
+ // Set expire timer if requested
56
+ if (options?.expire) {
57
+ for (const key of keyArray) {
58
+ const timer = setTimeout(async () => {
59
+ await this._unlockKey(key);
60
+ this._expireTimers.delete(key);
61
+ // Remove from owner tracking
62
+ for (const [_oid, keySet] of this._owners) {
63
+ keySet.delete(key);
64
+ }
65
+ }, options.expire);
66
+ this._expireTimers.set(key, timer);
67
+ }
68
+ }
69
+ return true;
70
+ }
71
+ async release(keys, options) {
72
+ const keyArray = Array.isArray(keys) ? keys : [keys];
73
+ for (const key of keyArray) {
74
+ await this._unlockKey(key);
75
+ // Clear expire timer
76
+ const timer = this._expireTimers.get(key);
77
+ if (timer) {
78
+ clearTimeout(timer);
79
+ this._expireTimers.delete(key);
80
+ }
81
+ // Remove from owner tracking
82
+ if (options?.ownerId) {
83
+ this._owners.get(options.ownerId)?.delete(key);
84
+ }
85
+ else {
86
+ for (const [_oid, keySet] of this._owners) {
87
+ keySet.delete(key);
88
+ }
89
+ }
90
+ }
91
+ }
92
+ async releaseAll(options) {
93
+ if (options?.ownerId) {
94
+ const keys = this._owners.get(options.ownerId);
95
+ if (keys) {
96
+ for (const key of keys) {
97
+ await this._unlockKey(key);
98
+ const timer = this._expireTimers.get(key);
99
+ if (timer) {
100
+ clearTimeout(timer);
101
+ this._expireTimers.delete(key);
102
+ }
103
+ }
104
+ this._owners.delete(options.ownerId);
105
+ }
106
+ }
107
+ else {
108
+ await this._rawSql `SELECT pg_advisory_unlock_all()`;
109
+ for (const timer of this._expireTimers.values()) {
110
+ clearTimeout(timer);
111
+ }
112
+ this._expireTimers.clear();
113
+ this._owners.clear();
114
+ }
115
+ }
116
+ async _unlockKey(key) {
117
+ const lockKey = stringToAdvisoryLockKey(key);
118
+ await this._rawSql `SELECT pg_advisory_unlock(${lockKey}::bigint)`;
119
+ }
120
+ _reset() {
121
+ for (const timer of this._expireTimers.values()) {
122
+ clearTimeout(timer);
123
+ }
124
+ this._expireTimers.clear();
125
+ this._owners.clear();
126
+ }
127
+ }
128
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
package/dist/hash.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function stringToAdvisoryLockKey(key: string): bigint;
2
+ //# sourceMappingURL=hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAQA,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAW3D"}
package/dist/hash.js ADDED
@@ -0,0 +1,19 @@
1
+ // FNV-1a 64-bit hash — converts string key to bigint for pg_advisory_lock()
2
+ // PG advisory locks require a bigint key. We hash the string key to a signed 64-bit integer.
3
+ const FNV_OFFSET_BASIS = 14695981039346656037n;
4
+ const FNV_PRIME = 1099511628211n;
5
+ const MAX_SIGNED_64 = (1n << 63n) - 1n;
6
+ const MOD = 1n << 64n;
7
+ export function stringToAdvisoryLockKey(key) {
8
+ let hash = FNV_OFFSET_BASIS;
9
+ for (let i = 0; i < key.length; i++) {
10
+ hash ^= BigInt(key.charCodeAt(i));
11
+ hash = (hash * FNV_PRIME) % MOD;
12
+ }
13
+ // Convert to signed 64-bit range for PG
14
+ if (hash > MAX_SIGNED_64) {
15
+ hash = hash - MOD;
16
+ }
17
+ return hash;
18
+ }
19
+ //# sourceMappingURL=hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.js","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,6FAA6F;AAE7F,MAAM,gBAAgB,GAAG,qBAAqB,CAAA;AAC9C,MAAM,SAAS,GAAG,cAAc,CAAA;AAChC,MAAM,aAAa,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,GAAG,EAAE,CAAA;AACtC,MAAM,GAAG,GAAG,EAAE,IAAI,GAAG,CAAA;AAErB,MAAM,UAAU,uBAAuB,CAAC,GAAW;IACjD,IAAI,IAAI,GAAG,gBAAgB,CAAA;IAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,IAAI,IAAI,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;QACjC,IAAI,GAAG,CAAC,IAAI,GAAG,SAAS,CAAC,GAAG,GAAG,CAAA;IACjC,CAAC;IACD,wCAAwC;IACxC,IAAI,IAAI,GAAG,aAAa,EAAE,CAAC;QACzB,IAAI,GAAG,IAAI,GAAG,GAAG,CAAA;IACnB,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { NeonLockingAdapter } from './adapter';
2
+ export { stringToAdvisoryLockKey } from './hash';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAC9C,OAAO,EAAE,uBAAuB,EAAE,MAAM,QAAQ,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // SPEC-066 — @mantajs/adapter-locking-neon barrel export
2
+ export { NeonLockingAdapter } from './adapter';
3
+ export { stringToAdvisoryLockKey } from './hash';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,yDAAyD;AAEzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAC9C,OAAO,EAAE,uBAAuB,EAAE,MAAM,QAAQ,CAAA"}
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@mantajs/adapter-locking-neon",
3
+ "version": "0.2.0-beta.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "devDependencies": {
15
+ "postgres": "^3.4.0"
16
+ },
17
+ "peerDependencies": {
18
+ "@mantajs/core": "0.2.0-beta.0"
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ]
23
+ }