arcway 0.4.4 → 0.4.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/package.json +2 -1
- package/server/callbacks/store.js +15 -10
- package/server/lib/vault/ids.js +4 -2
- package/server/queue/drivers/knex.js +24 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcway",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "A convention-based framework for building modular monoliths with strict domain boundaries.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -131,6 +131,7 @@
|
|
|
131
131
|
"eslint-plugin-storybook": "^10.2.8",
|
|
132
132
|
"happy-dom": "^20.6.1",
|
|
133
133
|
"jsdom": "^28.0.0",
|
|
134
|
+
"mysql2": "^3.23.3",
|
|
134
135
|
"playwright": "^1.58.2",
|
|
135
136
|
"prettier": "^3.8.1",
|
|
136
137
|
"react": "^19.2.4",
|
|
@@ -8,6 +8,11 @@ function toIso(value) {
|
|
|
8
8
|
return new Date(value).toISOString();
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function toDatabaseTimestamp(db, value) {
|
|
12
|
+
const client = db?.client?.config?.client;
|
|
13
|
+
return client === 'mysql' || client === 'mysql2' ? new Date(value) : toIso(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
function serializePayload(payload) {
|
|
12
17
|
return JSON.stringify(payload ?? null);
|
|
13
18
|
}
|
|
@@ -21,7 +26,7 @@ function parseRow(row) {
|
|
|
21
26
|
oneTime: Boolean(row.one_time),
|
|
22
27
|
used: Boolean(row.used),
|
|
23
28
|
boundToSession: row.bound_to_session ?? null,
|
|
24
|
-
expiresAt: row.expires_at,
|
|
29
|
+
expiresAt: toIso(row.expires_at),
|
|
25
30
|
};
|
|
26
31
|
}
|
|
27
32
|
|
|
@@ -66,23 +71,23 @@ class CallbackStore {
|
|
|
66
71
|
one_time: oneTime !== false,
|
|
67
72
|
used: false,
|
|
68
73
|
bound_to_session: boundToSession ?? null,
|
|
69
|
-
expires_at:
|
|
70
|
-
created_at:
|
|
71
|
-
updated_at:
|
|
74
|
+
expires_at: toDatabaseTimestamp(this.db, now + ttlSeconds * 1000),
|
|
75
|
+
created_at: toDatabaseTimestamp(this.db, now),
|
|
76
|
+
updated_at: toDatabaseTimestamp(this.db, now),
|
|
72
77
|
};
|
|
73
78
|
await this.db(this.tableName).insert(row);
|
|
74
79
|
return parseRow(row);
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
async claim(id, now = new Date()) {
|
|
78
|
-
const
|
|
83
|
+
const nowTimestamp = toDatabaseTimestamp(this.db, now);
|
|
79
84
|
return this.db.transaction(async (trx) => {
|
|
80
85
|
const updated = await trx(this.tableName)
|
|
81
86
|
.where('id', id)
|
|
82
87
|
.andWhere('one_time', true)
|
|
83
88
|
.andWhere('used', false)
|
|
84
|
-
.andWhere('expires_at', '>',
|
|
85
|
-
.update({ used: true, updated_at:
|
|
89
|
+
.andWhere('expires_at', '>', nowTimestamp)
|
|
90
|
+
.update({ used: true, updated_at: nowTimestamp });
|
|
86
91
|
if (updated === 0) return null;
|
|
87
92
|
const row = await trx(this.tableName).where('id', id).first();
|
|
88
93
|
return parseRow(row);
|
|
@@ -93,7 +98,7 @@ class CallbackStore {
|
|
|
93
98
|
const row = await this.db(this.tableName)
|
|
94
99
|
.where('id', id)
|
|
95
100
|
.andWhere('one_time', false)
|
|
96
|
-
.andWhere('expires_at', '>',
|
|
101
|
+
.andWhere('expires_at', '>', toDatabaseTimestamp(this.db, now))
|
|
97
102
|
.first();
|
|
98
103
|
if (!row || row.used) return null;
|
|
99
104
|
return parseRow(row);
|
|
@@ -107,8 +112,8 @@ class CallbackStore {
|
|
|
107
112
|
|
|
108
113
|
async cleanup({ graceSeconds = DEFAULT_CLEANUP_GRACE_SECONDS } = {}) {
|
|
109
114
|
const now = Date.now();
|
|
110
|
-
const expiredCutoff =
|
|
111
|
-
const usedCutoff =
|
|
115
|
+
const expiredCutoff = toDatabaseTimestamp(this.db, now);
|
|
116
|
+
const usedCutoff = toDatabaseTimestamp(this.db, now - graceSeconds * 1000);
|
|
112
117
|
return this.db(this.tableName)
|
|
113
118
|
.where('expires_at', '<=', expiredCutoff)
|
|
114
119
|
.orWhere(function () {
|
package/server/lib/vault/ids.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { customAlphabet
|
|
2
|
+
import { customAlphabet } from 'nanoid';
|
|
3
3
|
|
|
4
|
+
const ALPHANUMERIC_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
4
5
|
const LOWERCASE_BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567';
|
|
6
|
+
const alphanumericNanoId = customAlphabet(ALPHANUMERIC_ALPHABET);
|
|
5
7
|
const lowercaseBase32 = customAlphabet(LOWERCASE_BASE32_ALPHABET);
|
|
6
8
|
function generateUUID() {
|
|
7
9
|
return randomUUID();
|
|
8
10
|
}
|
|
9
11
|
function generateNanoId(size) {
|
|
10
|
-
return
|
|
12
|
+
return alphanumericNanoId(size);
|
|
11
13
|
}
|
|
12
14
|
function generateBase32Id(size = 16) {
|
|
13
15
|
return lowercaseBase32(size);
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
function toDatabaseTimestamp(db, value) {
|
|
2
|
+
const client = db?.client?.config?.client;
|
|
3
|
+
return client === 'mysql' || client === 'mysql2'
|
|
4
|
+
? new Date(value)
|
|
5
|
+
: new Date(value).toISOString();
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
class KnexQueueDriver {
|
|
2
9
|
tableName;
|
|
3
10
|
constructor(db, tableName = 'arcway_queue') {
|
|
@@ -37,10 +44,10 @@ class KnexQueueDriver {
|
|
|
37
44
|
await this.db(this.tableName).insert(rows);
|
|
38
45
|
}
|
|
39
46
|
async pop(namespace, topic, count, lockCooldownMs) {
|
|
40
|
-
const cutoff =
|
|
41
|
-
const now =
|
|
47
|
+
const cutoff = toDatabaseTimestamp(this.db, Date.now() - lockCooldownMs);
|
|
48
|
+
const now = toDatabaseTimestamp(this.db, Date.now());
|
|
42
49
|
return this.db.transaction(async (trx) => {
|
|
43
|
-
|
|
50
|
+
let readyQuery = trx(this.tableName)
|
|
44
51
|
.where('namespace', namespace)
|
|
45
52
|
.andWhere('topic', topic)
|
|
46
53
|
.andWhere(function () {
|
|
@@ -50,22 +57,21 @@ class KnexQueueDriver {
|
|
|
50
57
|
})
|
|
51
58
|
.orderBy('id', 'asc')
|
|
52
59
|
.limit(count)
|
|
53
|
-
.select('id');
|
|
54
|
-
const updatedCount = await trx(this.tableName)
|
|
55
|
-
.whereIn('id', subquery)
|
|
56
|
-
.update({ status: 'locked', locked_at: now });
|
|
57
|
-
if (updatedCount === 0) {
|
|
58
|
-
return [];
|
|
59
|
-
}
|
|
60
|
-
const rows = await trx(this.tableName)
|
|
61
|
-
.where('namespace', namespace)
|
|
62
|
-
.andWhere('topic', topic)
|
|
63
|
-
.andWhere('status', 'locked')
|
|
64
|
-
.andWhere('locked_at', now)
|
|
65
|
-
.orderBy('id', 'asc')
|
|
66
|
-
.limit(count)
|
|
67
60
|
.select('id', 'payload');
|
|
68
|
-
|
|
61
|
+
const client = trx.client.config.client;
|
|
62
|
+
if (client === 'mysql' || client === 'mysql2' || client === 'pg') {
|
|
63
|
+
readyQuery = readyQuery.forUpdate().skipLocked();
|
|
64
|
+
}
|
|
65
|
+
const rows = await readyQuery;
|
|
66
|
+
if (rows.length === 0) return [];
|
|
67
|
+
|
|
68
|
+
await trx(this.tableName)
|
|
69
|
+
.whereIn(
|
|
70
|
+
'id',
|
|
71
|
+
rows.map((row) => row.id),
|
|
72
|
+
)
|
|
73
|
+
.update({ status: 'locked', locked_at: now });
|
|
74
|
+
return rows.map((row) => ({ id: row.id, payload: row.payload }));
|
|
69
75
|
});
|
|
70
76
|
}
|
|
71
77
|
async remove(namespace, ids) {
|