@stonyx/orm 0.3.2-beta.89 → 0.3.2-beta.90

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.
@@ -101,6 +101,16 @@ export default class DynamoDBDB {
101
101
  */
102
102
  startup(): Promise<void>;
103
103
  shutdown(): Promise<void>;
104
+ /**
105
+ * DynamoDB does NOT use write serialization (#156).
106
+ *
107
+ * Unlike MySQL/PostgreSQL, DynamoDB has no server-side foreign key
108
+ * constraints and no multi-row transactions in standard single-item
109
+ * operations (PutItem, UpdateItem, DeleteItem). Each operation is
110
+ * atomic at the item level and cannot deadlock against other items.
111
+ * Concurrent fire-and-forget writes therefore cannot produce the
112
+ * cross-row lock contention that causes InnoDB/PG deadlocks.
113
+ */
104
114
  persist(operation: string, modelName: string, context: PersistContext, response: PersistResponse): Promise<void>;
105
115
  findRecord(modelName: string, id: unknown): Promise<OrmRecord | undefined>;
106
116
  findAll(modelName: string, conditions?: Record<string, unknown>): Promise<OrmRecord[]>;
@@ -191,6 +191,16 @@ export default class DynamoDBDB {
191
191
  // -------------------------------------------------------------------------
192
192
  // SqlDb contract — persist
193
193
  // -------------------------------------------------------------------------
194
+ /**
195
+ * DynamoDB does NOT use write serialization (#156).
196
+ *
197
+ * Unlike MySQL/PostgreSQL, DynamoDB has no server-side foreign key
198
+ * constraints and no multi-row transactions in standard single-item
199
+ * operations (PutItem, UpdateItem, DeleteItem). Each operation is
200
+ * atomic at the item level and cannot deadlock against other items.
201
+ * Concurrent fire-and-forget writes therefore cannot produce the
202
+ * cross-row lock contention that causes InnoDB/PG deadlocks.
203
+ */
194
204
  async persist(operation, modelName, context, response) {
195
205
  const OrmModule = await this._getOrm();
196
206
  if (OrmModule.default?.instance?.isView?.(modelName))
@@ -61,6 +61,14 @@ export default class MysqlDB {
61
61
  deps: MysqlDBDeps;
62
62
  pool: Pool | null;
63
63
  mysqlConfig: MysqlConfig;
64
+ /**
65
+ * Promise-chain mutex for write serialization (#156).
66
+ * All persist() calls chain through this single queue so concurrent
67
+ * fire-and-forget writes never produce parallel InnoDB transactions
68
+ * on FK-linked rows (which cause deadlocks).
69
+ * Reads are NOT affected — only persist() serializes.
70
+ */
71
+ private _writeQueue;
64
72
  constructor(deps?: Partial<MysqlDBDeps>);
65
73
  private requirePool;
66
74
  init(): Promise<void>;
@@ -26,6 +26,14 @@ export default class MysqlDB {
26
26
  deps;
27
27
  pool;
28
28
  mysqlConfig;
29
+ /**
30
+ * Promise-chain mutex for write serialization (#156).
31
+ * All persist() calls chain through this single queue so concurrent
32
+ * fire-and-forget writes never produce parallel InnoDB transactions
33
+ * on FK-linked rows (which cause deadlocks).
34
+ * Reads are NOT affected — only persist() serializes.
35
+ */
36
+ _writeQueue = Promise.resolve();
29
37
  constructor(deps = {}) {
30
38
  if (MysqlDB.instance)
31
39
  return MysqlDB.instance;
@@ -302,14 +310,20 @@ export default class MysqlDB {
302
310
  const Orm = (await import('@stonyx/orm')).default;
303
311
  if (Orm.instance?.isView?.(modelName))
304
312
  return;
305
- switch (operation) {
306
- case 'create':
307
- return this._persistCreate(modelName, context, response);
308
- case 'update':
309
- return this._persistUpdate(modelName, context, response);
310
- case 'delete':
311
- return this._persistDelete(modelName, context);
312
- }
313
+ const work = async () => {
314
+ switch (operation) {
315
+ case 'create':
316
+ return this._persistCreate(modelName, context, response);
317
+ case 'update':
318
+ return this._persistUpdate(modelName, context, response);
319
+ case 'delete':
320
+ return this._persistDelete(modelName, context);
321
+ }
322
+ };
323
+ // Chain through the write queue — .then(work, work) ensures the queue
324
+ // advances even when a previous persist rejects (#156).
325
+ this._writeQueue = this._writeQueue.then(work, work);
326
+ return this._writeQueue;
313
327
  }
314
328
  async _persistCreate(modelName, context, response) {
315
329
  const schemas = this.deps.introspectModels();
@@ -72,6 +72,14 @@ export default class PostgresDB {
72
72
  deps: PostgresDeps;
73
73
  pool: Pool | null;
74
74
  pgConfig: Record<string, unknown>;
75
+ /**
76
+ * Promise-chain mutex for write serialization (#156).
77
+ * All persist() calls chain through this single queue so concurrent
78
+ * fire-and-forget writes never produce parallel transactions
79
+ * on FK-linked rows (which cause deadlocks).
80
+ * Reads are NOT affected — only persist() serializes.
81
+ */
82
+ private _writeQueue;
75
83
  constructor(deps?: Partial<PostgresDeps>);
76
84
  protected requirePool(): Pool;
77
85
  init(): Promise<void>;
@@ -30,6 +30,14 @@ export default class PostgresDB {
30
30
  deps;
31
31
  pool;
32
32
  pgConfig;
33
+ /**
34
+ * Promise-chain mutex for write serialization (#156).
35
+ * All persist() calls chain through this single queue so concurrent
36
+ * fire-and-forget writes never produce parallel transactions
37
+ * on FK-linked rows (which cause deadlocks).
38
+ * Reads are NOT affected — only persist() serializes.
39
+ */
40
+ _writeQueue = Promise.resolve();
33
41
  constructor(deps = {}) {
34
42
  const Ctor = this.constructor;
35
43
  if (Ctor.instance)
@@ -356,14 +364,20 @@ export default class PostgresDB {
356
364
  const Orm = (await import('@stonyx/orm')).default;
357
365
  if (Orm.instance?.isView?.(modelName))
358
366
  return;
359
- switch (operation) {
360
- case 'create':
361
- return this._persistCreate(modelName, context, response);
362
- case 'update':
363
- return this._persistUpdate(modelName, context, response);
364
- case 'delete':
365
- return this._persistDelete(modelName, context);
366
- }
367
+ const work = async () => {
368
+ switch (operation) {
369
+ case 'create':
370
+ return this._persistCreate(modelName, context, response);
371
+ case 'update':
372
+ return this._persistUpdate(modelName, context, response);
373
+ case 'delete':
374
+ return this._persistDelete(modelName, context);
375
+ }
376
+ };
377
+ // Chain through the write queue — .then(work, work) ensures the queue
378
+ // advances even when a previous persist rejects (#156).
379
+ this._writeQueue = this._writeQueue.then(work, work);
380
+ return this._writeQueue;
367
381
  }
368
382
  async _persistCreate(modelName, context, response) {
369
383
  const schemas = this.deps.introspectModels();
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.3.2-beta.89",
7
+ "version": "0.3.2-beta.90",
8
8
  "description": "",
9
9
  "main": "dist/index.js",
10
10
  "type": "module",
@@ -319,6 +319,16 @@ export default class DynamoDBDB {
319
319
  // SqlDb contract — persist
320
320
  // -------------------------------------------------------------------------
321
321
 
322
+ /**
323
+ * DynamoDB does NOT use write serialization (#156).
324
+ *
325
+ * Unlike MySQL/PostgreSQL, DynamoDB has no server-side foreign key
326
+ * constraints and no multi-row transactions in standard single-item
327
+ * operations (PutItem, UpdateItem, DeleteItem). Each operation is
328
+ * atomic at the item level and cannot deadlock against other items.
329
+ * Concurrent fire-and-forget writes therefore cannot produce the
330
+ * cross-row lock contention that causes InnoDB/PG deadlocks.
331
+ */
322
332
  async persist(operation: string, modelName: string, context: PersistContext, response: PersistResponse): Promise<void> {
323
333
  const OrmModule = await this._getOrm();
324
334
  if (OrmModule.default?.instance?.isView?.(modelName)) return;
@@ -84,6 +84,15 @@ export default class MysqlDB {
84
84
  pool!: Pool | null;
85
85
  mysqlConfig!: MysqlConfig;
86
86
 
87
+ /**
88
+ * Promise-chain mutex for write serialization (#156).
89
+ * All persist() calls chain through this single queue so concurrent
90
+ * fire-and-forget writes never produce parallel InnoDB transactions
91
+ * on FK-linked rows (which cause deadlocks).
92
+ * Reads are NOT affected — only persist() serializes.
93
+ */
94
+ private _writeQueue: Promise<void> = Promise.resolve();
95
+
87
96
  constructor(deps: Partial<MysqlDBDeps> = {}) {
88
97
  if (MysqlDB.instance) return MysqlDB.instance;
89
98
  MysqlDB.instance = this;
@@ -398,14 +407,21 @@ export default class MysqlDB {
398
407
  const Orm = (await import('@stonyx/orm')).default;
399
408
  if ((Orm as unknown as { instance?: { isView?: (name: string) => boolean } }).instance?.isView?.(modelName)) return;
400
409
 
401
- switch (operation) {
402
- case 'create':
403
- return this._persistCreate(modelName, context, response);
404
- case 'update':
405
- return this._persistUpdate(modelName, context, response);
406
- case 'delete':
407
- return this._persistDelete(modelName, context);
408
- }
410
+ const work = async () => {
411
+ switch (operation) {
412
+ case 'create':
413
+ return this._persistCreate(modelName, context, response);
414
+ case 'update':
415
+ return this._persistUpdate(modelName, context, response);
416
+ case 'delete':
417
+ return this._persistDelete(modelName, context);
418
+ }
419
+ };
420
+
421
+ // Chain through the write queue — .then(work, work) ensures the queue
422
+ // advances even when a previous persist rejects (#156).
423
+ this._writeQueue = this._writeQueue.then(work, work);
424
+ return this._writeQueue;
409
425
  }
410
426
 
411
427
  private async _persistCreate(modelName: string, context: PersistContext, response: PersistResponse): Promise<void> {
@@ -90,6 +90,15 @@ export default class PostgresDB {
90
90
  pool!: Pool | null;
91
91
  pgConfig!: Record<string, unknown>;
92
92
 
93
+ /**
94
+ * Promise-chain mutex for write serialization (#156).
95
+ * All persist() calls chain through this single queue so concurrent
96
+ * fire-and-forget writes never produce parallel transactions
97
+ * on FK-linked rows (which cause deadlocks).
98
+ * Reads are NOT affected — only persist() serializes.
99
+ */
100
+ private _writeQueue: Promise<void> = Promise.resolve();
101
+
93
102
  constructor(deps: Partial<PostgresDeps> = {}) {
94
103
  const Ctor = this.constructor as typeof PostgresDB;
95
104
  if (Ctor.instance) return Ctor.instance;
@@ -468,14 +477,21 @@ export default class PostgresDB {
468
477
  const Orm = (await import('@stonyx/orm')).default;
469
478
  if ((Orm.instance as { isView?: (name: string) => boolean })?.isView?.(modelName)) return;
470
479
 
471
- switch (operation) {
472
- case 'create':
473
- return this._persistCreate(modelName, context, response);
474
- case 'update':
475
- return this._persistUpdate(modelName, context, response);
476
- case 'delete':
477
- return this._persistDelete(modelName, context);
478
- }
480
+ const work = async () => {
481
+ switch (operation) {
482
+ case 'create':
483
+ return this._persistCreate(modelName, context, response);
484
+ case 'update':
485
+ return this._persistUpdate(modelName, context, response);
486
+ case 'delete':
487
+ return this._persistDelete(modelName, context);
488
+ }
489
+ };
490
+
491
+ // Chain through the write queue — .then(work, work) ensures the queue
492
+ // advances even when a previous persist rejects (#156).
493
+ this._writeQueue = this._writeQueue.then(work, work);
494
+ return this._writeQueue;
479
495
  }
480
496
 
481
497
  private async _persistCreate(modelName: string, context: PersistContext, response: PersistResponse): Promise<void> {