arcway 0.4.4 → 0.4.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcway",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
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: toIso(now + ttlSeconds * 1000),
70
- created_at: toIso(now),
71
- updated_at: toIso(now),
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 nowIso = toIso(now);
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', '>', nowIso)
85
- .update({ used: true, updated_at: nowIso });
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', '>', toIso(now))
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 = toIso(now);
111
- const usedCutoff = toIso(now - graceSeconds * 1000);
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 () {
@@ -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 = new Date(Date.now() - lockCooldownMs).toISOString();
41
- const now = new Date().toISOString();
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
- const subquery = trx(this.tableName)
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
- return rows.map((r) => ({ id: r.id, payload: r.payload }));
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) {