velocious 1.0.568 → 1.0.569

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.
Files changed (152) hide show
  1. package/README.md +20 -0
  2. package/build/configuration.js +36 -0
  3. package/build/database/drivers/base.js +167 -54
  4. package/build/database/drivers/mssql/index.js +10 -7
  5. package/build/database/drivers/mysql/index.js +10 -5
  6. package/build/database/drivers/pgsql/index.js +4 -4
  7. package/build/database/drivers/sqlite/base.js +2 -2
  8. package/build/database/drivers/sqlite/connection-sql-js.js +62 -2
  9. package/build/database/drivers/sqlite/index.web.js +74 -0
  10. package/build/database/operation-connection.js +110 -0
  11. package/build/database/operation-lease.js +47 -0
  12. package/build/database/operation.js +140 -0
  13. package/build/database/pool/base.js +26 -1
  14. package/build/database/pool/single-multi-use.js +49 -0
  15. package/build/database/query/model-class-query.js +38 -7
  16. package/build/database/query/preloader/belongs-to.js +3 -2
  17. package/build/database/query/preloader/has-many.js +4 -3
  18. package/build/database/query/preloader/has-one.js +2 -1
  19. package/build/database/query/preloader/query-for-model.js +17 -0
  20. package/build/database/query/query-data.js +12 -3
  21. package/build/database/query/with-count.js +5 -3
  22. package/build/database/record/acts-as-list.js +11 -8
  23. package/build/database/record/attachments/store.js +15 -11
  24. package/build/database/record/auditing.js +12 -9
  25. package/build/database/record/counter-cache-magnitude.js +5 -3
  26. package/build/database/record/index.js +95 -14
  27. package/build/database/record/instance-relationships/belongs-to.js +4 -2
  28. package/build/database/record/instance-relationships/has-many.js +7 -5
  29. package/build/database/record/instance-relationships/has-one.js +4 -2
  30. package/build/database/record/validators/uniqueness.js +3 -2
  31. package/build/frontend-models/websocket-publishers.js +2 -2
  32. package/build/src/configuration.d.ts +12 -0
  33. package/build/src/configuration.d.ts.map +1 -1
  34. package/build/src/configuration.js +35 -1
  35. package/build/src/database/drivers/base.d.ts +66 -20
  36. package/build/src/database/drivers/base.d.ts.map +1 -1
  37. package/build/src/database/drivers/base.js +158 -54
  38. package/build/src/database/drivers/mssql/index.d.ts +4 -1
  39. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  40. package/build/src/database/drivers/mssql/index.js +11 -8
  41. package/build/src/database/drivers/mysql/index.d.ts +2 -1
  42. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  43. package/build/src/database/drivers/mysql/index.js +10 -6
  44. package/build/src/database/drivers/pgsql/index.d.ts +2 -1
  45. package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
  46. package/build/src/database/drivers/pgsql/index.js +5 -5
  47. package/build/src/database/drivers/sqlite/base.d.ts +1 -1
  48. package/build/src/database/drivers/sqlite/base.d.ts.map +1 -1
  49. package/build/src/database/drivers/sqlite/base.js +3 -3
  50. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts +20 -0
  51. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts.map +1 -1
  52. package/build/src/database/drivers/sqlite/connection-sql-js.js +56 -3
  53. package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
  54. package/build/src/database/drivers/sqlite/index.web.js +64 -1
  55. package/build/src/database/operation-connection.d.ts +61 -0
  56. package/build/src/database/operation-connection.d.ts.map +1 -0
  57. package/build/src/database/operation-connection.js +93 -0
  58. package/build/src/database/operation-lease.d.ts +27 -0
  59. package/build/src/database/operation-lease.d.ts.map +1 -0
  60. package/build/src/database/operation-lease.js +44 -0
  61. package/build/src/database/operation.d.ts +77 -0
  62. package/build/src/database/operation.d.ts.map +1 -0
  63. package/build/src/database/operation.js +123 -0
  64. package/build/src/database/pool/base.d.ts +14 -0
  65. package/build/src/database/pool/base.d.ts.map +1 -1
  66. package/build/src/database/pool/base.js +24 -2
  67. package/build/src/database/pool/single-multi-use.d.ts +1 -0
  68. package/build/src/database/pool/single-multi-use.d.ts.map +1 -1
  69. package/build/src/database/pool/single-multi-use.js +49 -1
  70. package/build/src/database/query/model-class-query.d.ts +14 -0
  71. package/build/src/database/query/model-class-query.d.ts.map +1 -1
  72. package/build/src/database/query/model-class-query.js +32 -8
  73. package/build/src/database/query/preloader/belongs-to.d.ts.map +1 -1
  74. package/build/src/database/query/preloader/belongs-to.js +4 -3
  75. package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
  76. package/build/src/database/query/preloader/has-many.js +5 -4
  77. package/build/src/database/query/preloader/has-one.d.ts.map +1 -1
  78. package/build/src/database/query/preloader/has-one.js +3 -2
  79. package/build/src/database/query/preloader/query-for-model.d.ts +10 -0
  80. package/build/src/database/query/preloader/query-for-model.d.ts.map +1 -0
  81. package/build/src/database/query/preloader/query-for-model.js +16 -0
  82. package/build/src/database/query/query-data.d.ts.map +1 -1
  83. package/build/src/database/query/query-data.js +13 -4
  84. package/build/src/database/query/with-count.d.ts.map +1 -1
  85. package/build/src/database/query/with-count.js +6 -4
  86. package/build/src/database/record/acts-as-list.js +12 -9
  87. package/build/src/database/record/attachments/attachment-record.d.ts +9 -0
  88. package/build/src/database/record/attachments/attachment-record.d.ts.map +1 -1
  89. package/build/src/database/record/attachments/store.d.ts +4 -2
  90. package/build/src/database/record/attachments/store.d.ts.map +1 -1
  91. package/build/src/database/record/attachments/store.js +16 -12
  92. package/build/src/database/record/auditing.d.ts.map +1 -1
  93. package/build/src/database/record/auditing.js +13 -10
  94. package/build/src/database/record/counter-cache-magnitude.js +6 -4
  95. package/build/src/database/record/index.d.ts +42 -2
  96. package/build/src/database/record/index.d.ts.map +1 -1
  97. package/build/src/database/record/index.js +87 -15
  98. package/build/src/database/record/instance-relationships/belongs-to.d.ts.map +1 -1
  99. package/build/src/database/record/instance-relationships/belongs-to.js +4 -3
  100. package/build/src/database/record/instance-relationships/has-many.d.ts.map +1 -1
  101. package/build/src/database/record/instance-relationships/has-many.js +7 -5
  102. package/build/src/database/record/instance-relationships/has-one.d.ts.map +1 -1
  103. package/build/src/database/record/instance-relationships/has-one.js +4 -3
  104. package/build/src/database/record/validators/uniqueness.d.ts.map +1 -1
  105. package/build/src/database/record/validators/uniqueness.js +4 -3
  106. package/build/src/frontend-models/websocket-publishers.js +3 -3
  107. package/build/src/sync/server-sequence-allocator.d.ts +16 -4
  108. package/build/src/sync/server-sequence-allocator.d.ts.map +1 -1
  109. package/build/src/sync/server-sequence-allocator.js +37 -11
  110. package/build/src/sync/sync-client.d.ts.map +1 -1
  111. package/build/src/sync/sync-client.js +7 -3
  112. package/build/src/sync/sync-publisher.d.ts +2 -1
  113. package/build/src/sync/sync-publisher.d.ts.map +1 -1
  114. package/build/src/sync/sync-publisher.js +11 -6
  115. package/build/sync/server-sequence-allocator.js +43 -10
  116. package/build/sync/sync-client.js +6 -2
  117. package/build/sync/sync-publisher.js +10 -5
  118. package/build/tsconfig.tsbuildinfo +1 -1
  119. package/package.json +1 -1
  120. package/src/configuration.js +36 -0
  121. package/src/database/drivers/base.js +167 -54
  122. package/src/database/drivers/mssql/index.js +10 -7
  123. package/src/database/drivers/mysql/index.js +10 -5
  124. package/src/database/drivers/pgsql/index.js +4 -4
  125. package/src/database/drivers/sqlite/base.js +2 -2
  126. package/src/database/drivers/sqlite/connection-sql-js.js +62 -2
  127. package/src/database/drivers/sqlite/index.web.js +74 -0
  128. package/src/database/operation-connection.js +110 -0
  129. package/src/database/operation-lease.js +47 -0
  130. package/src/database/operation.js +140 -0
  131. package/src/database/pool/base.js +26 -1
  132. package/src/database/pool/single-multi-use.js +49 -0
  133. package/src/database/query/model-class-query.js +38 -7
  134. package/src/database/query/preloader/belongs-to.js +3 -2
  135. package/src/database/query/preloader/has-many.js +4 -3
  136. package/src/database/query/preloader/has-one.js +2 -1
  137. package/src/database/query/preloader/query-for-model.js +17 -0
  138. package/src/database/query/query-data.js +12 -3
  139. package/src/database/query/with-count.js +5 -3
  140. package/src/database/record/acts-as-list.js +11 -8
  141. package/src/database/record/attachments/store.js +15 -11
  142. package/src/database/record/auditing.js +12 -9
  143. package/src/database/record/counter-cache-magnitude.js +5 -3
  144. package/src/database/record/index.js +95 -14
  145. package/src/database/record/instance-relationships/belongs-to.js +4 -2
  146. package/src/database/record/instance-relationships/has-many.js +7 -5
  147. package/src/database/record/instance-relationships/has-one.js +4 -2
  148. package/src/database/record/validators/uniqueness.js +3 -2
  149. package/src/frontend-models/websocket-publishers.js +2 -2
  150. package/src/sync/server-sequence-allocator.js +43 -10
  151. package/src/sync/sync-client.js +6 -2
  152. package/src/sync/sync-publisher.js +10 -5
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import debounce from "debounce"
4
+ import Mutex from "epic-locks/build/mutex.js"
4
5
  import queryWeb from "./query.web.js"
5
6
 
6
7
  export default class VelociousDatabaseDriversSqliteConnectionSqlJs {
@@ -12,6 +13,9 @@ export default class VelociousDatabaseDriversSqliteConnectionSqlJs {
12
13
  */
13
14
  constructor(driver, connection, persistence) {
14
15
  this.connection = connection
16
+ this.databaseSaveDeferred = false
17
+ this.databaseSaveMutex = new Mutex()
18
+ this.databaseTransactionStarting = false
15
19
  this.driver = driver
16
20
  this.persistence = persistence
17
21
  }
@@ -30,6 +34,54 @@ export default class VelociousDatabaseDriversSqliteConnectionSqlJs {
30
34
  await this.saveDatabase()
31
35
  }
32
36
 
37
+ /**
38
+ * Flushes only when a mutation save is pending or was deferred by a transaction.
39
+ * @returns {Promise<void>} - Resolves when pending database bytes are stored.
40
+ */
41
+ async flushPendingDatabaseSave() {
42
+ if (!this.saveDatabaseDebounce.isPending && !this.databaseSaveDeferred) return
43
+
44
+ await this.flushDatabaseSave()
45
+ }
46
+
47
+ /**
48
+ * Drains active and queued persistence before atomically starting an outer transaction.
49
+ * @param {() => Promise<void>} callback - Starts the SQL transaction.
50
+ * @returns {Promise<void>} - Resolves after BEGIN succeeds.
51
+ */
52
+ async withTransactionStart(callback) {
53
+ if (this.saveDatabaseDebounce.isPending) {
54
+ this.saveDatabaseDebounce.clear()
55
+ this.databaseSaveDeferred = true
56
+ }
57
+
58
+ await this.databaseSaveMutex.sync(async () => {
59
+ if (this.databaseSaveDeferred) {
60
+ this.databaseSaveDeferred = false
61
+ const databaseContent = this.connection.export()
62
+
63
+ await this.persistence.save(databaseContent)
64
+ }
65
+
66
+ this.databaseTransactionStarting = true
67
+
68
+ try {
69
+ await callback()
70
+ } catch (error) {
71
+ this.databaseTransactionStarting = false
72
+ throw error
73
+ }
74
+ })
75
+ }
76
+
77
+ /**
78
+ * Marks successful outer transaction admission complete after driver state is updated.
79
+ * @returns {void}
80
+ */
81
+ completeTransactionStart() {
82
+ this.databaseTransactionStarting = false
83
+ }
84
+
33
85
  /**
34
86
  * Runs query.
35
87
  * @param {string} sql - SQL string.
@@ -59,9 +111,17 @@ export default class VelociousDatabaseDriversSqliteConnectionSqlJs {
59
111
  }
60
112
 
61
113
  saveDatabase = async () => {
62
- const databaseContent = this.connection.export()
114
+ await this.databaseSaveMutex.sync(async () => {
115
+ if (this.driver.insideTransaction() || this.databaseTransactionStarting) {
116
+ this.databaseSaveDeferred = true
117
+ return
118
+ }
119
+
120
+ this.databaseSaveDeferred = false
121
+ const databaseContent = this.connection.export()
63
122
 
64
- await this.persistence.save(databaseContent)
123
+ await this.persistence.save(databaseContent)
124
+ })
65
125
  }
66
126
 
67
127
  saveDatabaseDebounce = debounce(this.saveDatabase, 500)
@@ -64,6 +64,80 @@ export default class VelociousDatabaseDriversSqliteWeb extends Base {
64
64
  }
65
65
  }
66
66
 
67
+ /**
68
+ * Starts an outer transaction after draining SQL.js persistence admission.
69
+ * @param {Pick<import("../base.js").QueryOptions, "operationOwner">} [options] - Transaction ownership.
70
+ * @returns {Promise<void>} - Resolves when the transaction starts.
71
+ */
72
+ async startTransaction(options = {}) {
73
+ if (!this.args?.getConnection) {
74
+ if (!this._connection) throw new Error("SQLite web connection has not been initialized")
75
+
76
+ try {
77
+ await super.startTransaction(options)
78
+ } finally {
79
+ this._connection.completeTransactionStart()
80
+ }
81
+
82
+ return
83
+ }
84
+
85
+ await super.startTransaction(options)
86
+ }
87
+
88
+ /**
89
+ * Coordinates SQL BEGIN with active and queued persistence exports.
90
+ * @param {Pick<import("../base.js").QueryOptions, "operationOwner">} [options] - Transaction ownership.
91
+ * @returns {Promise<void>} - Resolves when the transaction starts.
92
+ */
93
+ async _startTransactionAction(options = {}) {
94
+ if (!this.args?.getConnection) {
95
+ if (!this._connection) throw new Error("SQLite web connection has not been initialized")
96
+
97
+ await this._connection.withTransactionStart(async () => {
98
+ await super._startTransactionAction(options)
99
+ })
100
+
101
+ return
102
+ }
103
+
104
+ await super._startTransactionAction(options)
105
+ }
106
+
107
+ /**
108
+ * Commits and persists bytes after the outermost SQL.js transaction closes.
109
+ * @param {Pick<import("../base.js").QueryOptions, "operationOwner">} [options] - Transaction ownership.
110
+ * @returns {Promise<void>} - Resolves when committed bytes are persisted.
111
+ */
112
+ async commitTransaction(options = {}) {
113
+ const outermostTransaction = this._transactionsCount === 1
114
+
115
+ await super.commitTransaction(options)
116
+
117
+ if (outermostTransaction && !this.args?.getConnection) {
118
+ if (!this._connection) throw new Error("SQLite web connection has not been initialized")
119
+
120
+ await this._connection.flushPendingDatabaseSave()
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Rolls back and persists bytes after the outermost SQL.js transaction closes.
126
+ * @param {Pick<import("../base.js").QueryOptions, "operationOwner">} [options] - Transaction ownership.
127
+ * @returns {Promise<void>} - Resolves when rolled-back bytes are persisted.
128
+ */
129
+ async rollbackTransaction(options = {}) {
130
+ const outermostTransaction = this._transactionsCount === 1
131
+
132
+ await super.rollbackTransaction(options)
133
+
134
+ if (outermostTransaction && !this.args?.getConnection) {
135
+ if (!this._connection) throw new Error("SQLite web connection has not been initialized")
136
+
137
+ await this._connection.flushPendingDatabaseSave()
138
+ }
139
+ }
140
+
67
141
  /**
68
142
  * Runs get connection.
69
143
  * @returns {ConnectionSqlJs | SqliteWebConnection} - The connection.
@@ -0,0 +1,110 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Explicit connection facade that tags operation-owned asynchronous work while
5
+ * forwarding the driver's synchronous SQL-builder and capability APIs.
6
+ */
7
+ export default class VelociousDatabaseOperationConnection {
8
+ /**
9
+ * Runs constructor.
10
+ * @param {object} args - Connection ownership.
11
+ * @param {import("./drivers/base.js").default} args.connection - Pinned physical connection.
12
+ * @param {import("./operation.js").default} args.operation - Owning operation.
13
+ * @param {symbol} args.owner - Opaque lease owner token.
14
+ */
15
+ constructor({connection, operation, owner}) {
16
+ this._physicalConnection = connection
17
+ this._operation = operation
18
+ this._owner = owner
19
+
20
+ return new Proxy(this, {
21
+ get: (target, property, receiver) => {
22
+ if (Reflect.has(target, property)) {
23
+ const value = Reflect.get(target, property, receiver)
24
+
25
+ return typeof value === "function" ? value.bind(target) : value
26
+ }
27
+
28
+ const value = Reflect.get(connection, property, receiver)
29
+
30
+ return typeof value === "function" ? value.bind(receiver) : value
31
+ },
32
+ set: (target, property, value, receiver) => {
33
+ if (Reflect.has(target, property)) {
34
+ return Reflect.set(target, property, value, receiver)
35
+ }
36
+
37
+ return Reflect.set(connection, property, value, connection)
38
+ }
39
+ })
40
+ }
41
+
42
+ /**
43
+ * Runs a tagged SQL query on the pinned connection.
44
+ * @param {string} sql - SQL string.
45
+ * @param {import("./drivers/base.js").QueryOptions} [options] - Query options.
46
+ * @returns {Promise<import("./drivers/base.js").QueryResultType>} - Query result.
47
+ */
48
+ async query(sql, options = {}) {
49
+ this._operation.assertActive()
50
+
51
+ return await this._physicalConnection.query(sql, {...options, operationOwner: this._owner})
52
+ }
53
+
54
+ /**
55
+ * Streams an operation-owned SQL query.
56
+ * @param {string} sql - SQL string.
57
+ * @param {import("./drivers/base.js").QueryOptions} [options] - Query options.
58
+ * @yields {Record<string, unknown>} - Query rows.
59
+ */
60
+ async *queryStream(sql, options = {}) {
61
+ this._operation.assertActive()
62
+
63
+ yield* this._physicalConnection.queryStream(sql, {...options, operationOwner: this._owner})
64
+ }
65
+
66
+ /**
67
+ * Executes an operation-owned mutation and returns its affected row count.
68
+ * @param {string} sql - Mutation SQL string.
69
+ * @param {import("./drivers/base.js").QueryOptions} [options] - Query options.
70
+ * @returns {Promise<number>} - Affected row count.
71
+ */
72
+ async affectedRows(sql, options = {}) {
73
+ this._operation.assertActive()
74
+
75
+ return await this._physicalConnection.affectedRows(sql, {...options, operationOwner: this._owner})
76
+ }
77
+
78
+ /**
79
+ * Runs an operation-owned transaction or nested savepoint.
80
+ * @template T
81
+ * @param {() => Promise<T>} callback - Transaction callback.
82
+ * @returns {Promise<T>} - Callback result.
83
+ */
84
+ async transaction(callback) {
85
+ this._operation.assertActive()
86
+
87
+ return /** @type {Promise<T>} */ (this._physicalConnection.transaction(callback, {operationOwner: this._owner}))
88
+ }
89
+
90
+ /**
91
+ * Registers an operation-owned after-commit callback.
92
+ * @param {() => void | Promise<void>} callback - Callback.
93
+ * @returns {Promise<void>} - Resolves after registration or execution.
94
+ */
95
+ async afterCommit(callback) {
96
+ this._operation.assertActive()
97
+
98
+ await this._physicalConnection.afterCommit(callback, {operationOwner: this._owner})
99
+ }
100
+
101
+ /**
102
+ * Returns the last inserted identifier through the operation lease.
103
+ * @returns {Promise<number>} - Last inserted identifier.
104
+ */
105
+ async lastInsertID() {
106
+ this._operation.assertActive()
107
+
108
+ return await this._physicalConnection.lastInsertID({operationOwner: this._owner})
109
+ }
110
+ }
@@ -0,0 +1,47 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Exclusive lease installed on a shared physical connection while one
5
+ * operation-scoped transaction owns it.
6
+ */
7
+ export default class VelociousDatabaseOperationLease {
8
+ /**
9
+ * Runs constructor.
10
+ * @param {symbol} owner - Opaque operation owner token.
11
+ */
12
+ constructor(owner) {
13
+ this.owner = owner
14
+ this.released = false
15
+ /**
16
+ * Resolves the lease waiters.
17
+ * @type {() => void} */
18
+ let release = () => {}
19
+
20
+ this.releasedPromise = new Promise((resolve) => {
21
+ release = () => resolve(undefined)
22
+ })
23
+ this.releasePromise = release
24
+ }
25
+
26
+ /**
27
+ * Waits until the lease is released unless `owner` owns it.
28
+ * @param {symbol | undefined} owner - Candidate operation owner.
29
+ * @returns {Promise<void>} - Resolves when access is allowed.
30
+ */
31
+ async wait(owner) {
32
+ if (owner === this.owner) return
33
+
34
+ await this.releasedPromise
35
+ }
36
+
37
+ /**
38
+ * Releases all waiters exactly once.
39
+ * @returns {void}
40
+ */
41
+ release() {
42
+ if (this.released) return
43
+
44
+ this.released = true
45
+ this.releasePromise()
46
+ }
47
+ }
@@ -0,0 +1,140 @@
1
+ // @ts-check
2
+
3
+ import OperationConnection from "./operation-connection.js"
4
+
5
+ /**
6
+ * Explicit owner for model work performed on one pinned transactional
7
+ * connection.
8
+ */
9
+ export default class VelociousDatabaseOperation {
10
+ /**
11
+ * Runs constructor.
12
+ * @param {object} args - Operation ownership.
13
+ * @param {import("../configuration.js").default} args.configuration - Owning configuration.
14
+ * @param {string} args.configurationReuseKey - Physical database configuration key captured at checkout.
15
+ * @param {import("./drivers/base.js").default} args.connection - Pinned physical connection.
16
+ * @param {string} args.databaseIdentifier - Singular database identifier.
17
+ * @param {symbol} args.owner - Opaque pool lease owner.
18
+ */
19
+ constructor({configuration, configurationReuseKey, connection, databaseIdentifier, owner}) {
20
+ this._active = true
21
+ this._configuration = configuration
22
+ this._configurationReuseKey = configurationReuseKey
23
+ this._databaseIdentifier = databaseIdentifier
24
+ this._connection = new OperationConnection({
25
+ connection,
26
+ operation: this,
27
+ owner
28
+ })
29
+ }
30
+
31
+ /**
32
+ * Returns an operation-bound model query/create scope.
33
+ * @template {typeof import("./record/index.js").default} MC
34
+ * @param {MC} ModelClass - Model class to bind.
35
+ * @returns {import("./query/model-class-query.js").default<MC>} - Operation-bound scope.
36
+ */
37
+ forModel(ModelClass) {
38
+ this.assertActive()
39
+ this.assertModel(ModelClass)
40
+
41
+ return ModelClass._newQuery({
42
+ driver: this.connection(),
43
+ operation: this
44
+ })
45
+ }
46
+
47
+ /**
48
+ * Verifies that a model belongs to this operation's configuration and database.
49
+ * @param {typeof import("./record/index.js").default} ModelClass - Model class to verify.
50
+ * @returns {void}
51
+ */
52
+ assertModel(ModelClass) {
53
+ if (ModelClass._getConfiguration() !== this._configuration) {
54
+ throw new Error(`${ModelClass.getModelName()} belongs to another Velocious configuration`)
55
+ }
56
+
57
+ const modelDatabaseIdentifier = ModelClass.getDatabaseIdentifier()
58
+
59
+ if (modelDatabaseIdentifier !== this._databaseIdentifier) {
60
+ throw new Error(`${ModelClass.getModelName()} uses database ${JSON.stringify(modelDatabaseIdentifier)}, not operation database ${JSON.stringify(this._databaseIdentifier)}`)
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Binds a loaded or built record to this operation.
66
+ * @template {import("./record/index.js").default} Model
67
+ * @param {Model} record - Record to bind.
68
+ * @returns {Model} - Bound record.
69
+ */
70
+ bindRecord(record) {
71
+ this.assertActive()
72
+ this.assertModel(record.getModelClass())
73
+ record.bindDatabaseOperation(this)
74
+
75
+ return record
76
+ }
77
+
78
+ /**
79
+ * Registers a callback owned by the current operation transaction frame.
80
+ * @param {() => void | Promise<void>} callback - Callback.
81
+ * @returns {Promise<void>} - Resolves after registration or execution.
82
+ */
83
+ async afterCommit(callback) {
84
+ this.assertActive()
85
+ await this.connection().afterCommit(callback)
86
+ }
87
+
88
+ /**
89
+ * Runs a nested operation transaction/savepoint.
90
+ * @template T
91
+ * @param {() => Promise<T>} callback - Callback.
92
+ * @returns {Promise<T>} - Callback result.
93
+ */
94
+ async transaction(callback) {
95
+ this.assertActive()
96
+
97
+ return await this.connection().transaction(callback)
98
+ }
99
+
100
+ /**
101
+ * Returns the deliberately exposed operation connection facade for raw SQL.
102
+ * @returns {import("./drivers/base.js").default} - Operation-bound connection.
103
+ */
104
+ connection() {
105
+ this.assertActive()
106
+
107
+ /**
108
+ * Narrows the Proxy facade to the complete driver surface that it forwards.
109
+ * @type {import("./drivers/base.js").default}
110
+ */
111
+ const connectionFacade = /** @type {?} */ (this._connection)
112
+
113
+ return connectionFacade
114
+ }
115
+
116
+ /**
117
+ * Raises when an operation handle has left its callback.
118
+ * @returns {void}
119
+ */
120
+ assertActive() {
121
+ if (!this._active) throw new Error("Database operation has completed")
122
+
123
+ const currentReuseKey = this
124
+ ._configuration
125
+ .getDatabasePool(this._databaseIdentifier)
126
+ .getConfigurationReuseKey()
127
+
128
+ if (currentReuseKey !== this._configurationReuseKey) {
129
+ throw new Error(`Database operation for ${JSON.stringify(this._databaseIdentifier)} belongs to a different physical database than the current tenant context`)
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Expires the operation and all scopes/records bound to it.
135
+ * @returns {void}
136
+ */
137
+ complete() {
138
+ this._active = false
139
+ }
140
+ }
@@ -241,9 +241,21 @@ class VelociousDatabasePoolBase {
241
241
  * @returns {boolean} - Whether connection matches current resolved configuration.
242
242
  */
243
243
  connectionMatchesCurrentConfiguration(connection) {
244
+ return this.getConnectionConfigurationReuseKey(connection) === this.getConfigurationReuseKey()
245
+ }
246
+
247
+ /**
248
+ * Returns the resolved database configuration key stamped on a connection at checkout.
249
+ * @param {import("../drivers/base.js").default} connection - Checked-out connection.
250
+ * @returns {string} - Connection configuration reuse key.
251
+ */
252
+ getConnectionConfigurationReuseKey(connection) {
244
253
  const connectionWithPoolKey = /** @type {import("../drivers/base.js").default & {[POOL_CONFIGURATION_KEY]?: string}} */ (connection)
254
+ const reuseKey = connectionWithPoolKey[POOL_CONFIGURATION_KEY]
255
+
256
+ if (!reuseKey) throw new Error("Database connection is missing its configuration reuse key")
245
257
 
246
- return connectionWithPoolKey[POOL_CONFIGURATION_KEY] === this.getConfigurationReuseKey()
258
+ return reuseKey
247
259
  }
248
260
 
249
261
  /**
@@ -357,6 +369,19 @@ class VelociousDatabasePoolBase {
357
369
  throw new Error("'withConnection' not implemented")
358
370
  }
359
371
 
372
+ /**
373
+ * Runs an operation on a freshly checked-out connection.
374
+ * @template T
375
+ * @param {ConnectionCheckoutOptions} options - Checkout options.
376
+ * @param {(connection: import("../drivers/base.js").default, owner: symbol) => Promise<T>} callback - Operation callback.
377
+ * @returns {Promise<T>} - Resolves with the callback result.
378
+ */
379
+ async withOperationConnection(options, callback) {
380
+ const owner = Symbol("database-operation-owner")
381
+
382
+ return await this.withConnection(options, async (connection) => await callback(connection, owner))
383
+ }
384
+
360
385
  /**
361
386
  * Ensures a reusable connection exists for contexts where AsyncLocalStorage isn't set.
362
387
  * Default implementation just checks out a connection.
@@ -1,10 +1,12 @@
1
1
  // @ts-check
2
2
 
3
3
  import BasePool from "./base.js"
4
+ import OperationLease from "../operation-lease.js"
4
5
 
5
6
  export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
6
7
  activeCheckoutCount = 0
7
8
  suppressedConnectionContextCount = 0
9
+ operationLeaseQueue = Promise.resolve()
8
10
 
9
11
  /**
10
12
  * Checkout names of the currently-active nested checkouts, innermost last. The
@@ -107,6 +109,53 @@ export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
107
109
  }
108
110
  }
109
111
 
112
+ /**
113
+ * Runs an operation under an exclusive FIFO lease on the reusable physical connection.
114
+ * @template T
115
+ * @param {import("./base.js").ConnectionCheckoutOptions} options - Checkout options.
116
+ * @param {(connection: import("../drivers/base.js").default, owner: symbol) => Promise<T>} callback - Operation callback.
117
+ * @returns {Promise<T>} - Resolves with the callback result.
118
+ */
119
+ async withOperationConnection(options, callback) {
120
+ const previousLease = this.operationLeaseQueue
121
+ /**
122
+ * Resolves this FIFO queue turn.
123
+ * @type {() => void} */
124
+ let releaseQueue = () => {}
125
+ const queueTurn = new Promise((resolve) => {
126
+ releaseQueue = () => resolve(undefined)
127
+ })
128
+
129
+ this.operationLeaseQueue = previousLease.then(async () => await queueTurn)
130
+ await previousLease
131
+
132
+ const owner = Symbol("single-pool-operation-owner")
133
+ const operationLease = new OperationLease(owner)
134
+ /** @type {import("../drivers/base.js").default | undefined} */
135
+ let connection
136
+ let operationLeaseInstalled = false
137
+
138
+ try {
139
+ connection = await this.checkout(options)
140
+ await connection.setOperationLease(operationLease)
141
+ operationLeaseInstalled = true
142
+
143
+ return await callback(connection, owner)
144
+ } finally {
145
+ operationLease.release()
146
+
147
+ try {
148
+ if (connection && operationLeaseInstalled) connection.clearOperationLease(operationLease)
149
+ } finally {
150
+ try {
151
+ if (connection) await this.checkin(connection)
152
+ } finally {
153
+ releaseQueue()
154
+ }
155
+ }
156
+ }
157
+ }
158
+
110
159
  /**
111
160
  * Runs without current connection context.
112
161
  * @template T
@@ -24,7 +24,7 @@ import WhereParser from "../query-parser/where-parser.js"
24
24
  /**
25
25
  * Defines this typedef.
26
26
  * @template {typeof import("../record/index.js").default} [MC=typeof import("../record/index.js").default]
27
- * @typedef {import("./index.js").QueryArgsType & {modelClass: MC, joinBasePath?: string[], joinTracker?: import("./join-tracker.js").default, forceQualifyBaseTable?: boolean, withCount?: import("./with-count.js").WithCountEntry[], queryData?: import("./query-data.js").QueryDataEntry[]}} ModelClassQueryArgsType
27
+ * @typedef {import("./index.js").QueryArgsType & {modelClass: MC, joinBasePath?: string[], joinTracker?: import("./join-tracker.js").default, forceQualifyBaseTable?: boolean, withCount?: import("./with-count.js").WithCountEntry[], queryData?: import("./query-data.js").QueryDataEntry[], operation?: import("../operation.js").default}} ModelClassQueryArgsType
28
28
  */
29
29
  /**
30
30
  * Runs unquote sql identifier.
@@ -200,6 +200,7 @@ export default class VelociousDatabaseQueryModelClassQuery extends DatabaseQuery
200
200
  this._joinBasePath = args.joinBasePath || []
201
201
  this._joinTracker = args.joinTracker || new JoinTracker({modelClass: this.modelClass})
202
202
  this._forceQualifyBaseTable = Boolean(args.forceQualifyBaseTable)
203
+ this._operation = args.operation
203
204
 
204
205
  /**
205
206
  * Narrows the runtime value to the documented type.
@@ -240,7 +241,8 @@ export default class VelociousDatabaseQueryModelClassQuery extends DatabaseQuery
240
241
  joinTracker: this._joinTracker.clone(),
241
242
  forceQualifyBaseTable: this._forceQualifyBaseTable,
242
243
  withCount: [...this._withCount],
243
- queryData: [...this._queryData]
244
+ queryData: [...this._queryData],
245
+ operation: this._operation
244
246
  }))
245
247
 
246
248
  // @ts-expect-error
@@ -723,7 +725,11 @@ export default class VelociousDatabaseQueryModelClassQuery extends DatabaseQuery
723
725
  * @returns {VelociousDatabaseQueryModelClassQuery<MC>} - The scoped join query.
724
726
  */
725
727
  buildJoinScopeQuery(targetModelClass, joinPath) {
726
- const scopedQuery = /** @type {VelociousDatabaseQueryModelClassQuery<MC>} */ (targetModelClass._newQuery())
728
+ const scopedQuery = /** @type {VelociousDatabaseQueryModelClassQuery<MC>} */ (
729
+ this._operation
730
+ ? this._operation.forModel(targetModelClass)
731
+ : targetModelClass._newQuery()
732
+ )
727
733
 
728
734
  scopedQuery._joinTracker = this._joinTracker
729
735
  scopedQuery._joinBasePath = joinPath
@@ -864,8 +870,7 @@ export default class VelociousDatabaseQueryModelClassQuery extends DatabaseQuery
864
870
 
865
871
  if (record) return record
866
872
 
867
- const ModelClass = this.getModelClass()
868
- const newRecord = /** @type {InstanceType<MC>} */ (new ModelClass(conditions))
873
+ const newRecord = this.build(conditions)
869
874
 
870
875
  if (callback) {
871
876
  callback(newRecord)
@@ -874,6 +879,33 @@ export default class VelociousDatabaseQueryModelClassQuery extends DatabaseQuery
874
879
  return newRecord
875
880
  }
876
881
 
882
+ /**
883
+ * Builds a record owned by the query's operation, when present.
884
+ * @param {Record<string, ?>} [attributes] - Initial attributes.
885
+ * @returns {InstanceType<MC>} - Built record.
886
+ */
887
+ build(attributes = {}) {
888
+ const ModelClass = this.getModelClass()
889
+ const record = /** @type {InstanceType<MC>} */ (new ModelClass(attributes))
890
+
891
+ if (this._operation) this._operation.bindRecord(record)
892
+
893
+ return record
894
+ }
895
+
896
+ /**
897
+ * Creates a record owned by the query's operation, when present.
898
+ * @param {Record<string, ?>} [attributes] - Initial attributes.
899
+ * @returns {Promise<InstanceType<MC>>} - Created record.
900
+ */
901
+ async create(attributes = {}) {
902
+ const record = this.build(attributes)
903
+
904
+ await record.save()
905
+
906
+ return record
907
+ }
908
+
877
909
  /**
878
910
  * Runs first.
879
911
  * @returns {Promise<InstanceType<MC> | null>} - Resolves with the first.
@@ -917,8 +949,7 @@ export default class VelociousDatabaseQueryModelClassQuery extends DatabaseQuery
917
949
  const results = await this.results()
918
950
 
919
951
  for (const result of results) {
920
- const ModelClass = this.getModelClass()
921
- const model = /** @type {InstanceType<MC>} */ (new ModelClass())
952
+ const model = this.build()
922
953
 
923
954
  model.loadExistingRecord(result)
924
955
  models.push(model)