arcway 0.4.3 → 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.3",
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",
@@ -111,6 +111,7 @@ async function boot(options) {
111
111
  callbacks,
112
112
  workerPool,
113
113
  plugins,
114
+ appConfig: config,
114
115
  });
115
116
  await jobRunner.init();
116
117
 
@@ -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 () {
@@ -38,6 +38,7 @@ async function discoverJobs(jobsDir) {
38
38
  class JobRunner {
39
39
  _dispatcher;
40
40
  _config;
41
+ _appConfig;
41
42
  _log;
42
43
  _appContext;
43
44
  _jobs = [];
@@ -64,9 +65,11 @@ class JobRunner {
64
65
  callbacks,
65
66
  workerPool,
66
67
  plugins,
68
+ appConfig,
67
69
  } = {},
68
70
  ) {
69
71
  this._config = config;
72
+ this._appConfig = appConfig ?? config;
70
73
  this._log = log;
71
74
  this._dispatcher = new JobDispatcher({
72
75
  backoffMs: config?.backoffMs,
@@ -132,7 +135,7 @@ class JobRunner {
132
135
  meta: this._appContext.meta,
133
136
  log: this._log.extend({ logger: SYSTEM_JOB_DOMAIN }),
134
137
  };
135
- const systemJobs = registerSystemJobs(this._config, this._dispatcher, systemContext);
138
+ const systemJobs = registerSystemJobs(this._appConfig, this._dispatcher, systemContext);
136
139
  for (const sj of systemJobs) {
137
140
  this._log?.info(` ${sj.jobName} (system${sj.schedule ? `, ${sj.schedule}` : ''})`);
138
141
  }
@@ -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) {