velocious 1.0.670 → 1.0.672

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.
@@ -38,6 +38,7 @@ export default class ServerSequenceAllocator {
38
38
  this.tableName = tableName
39
39
  this._memorySequence = 0
40
40
  this._isReady = false
41
+ this._transactionalReadinessPending = false
41
42
  /** @type {Promise<void> | null} */
42
43
  this._readyPromise = null
43
44
  }
@@ -110,10 +111,28 @@ export default class ServerSequenceAllocator {
110
111
  // DDL joins any transaction already open on this connection (the mixin's
111
112
  // beforeCreate allocation always runs inside the record save transaction),
112
113
  // and on transactional-DDL databases (MSSQL, PostgreSQL, SQLite) a rollback
113
- // of that outer transaction removes the just-created table again. Only
114
- // cache readiness when the table was not created inside an active
115
- // transaction; otherwise the next allocation re-verifies the table.
116
- if (!created || !db.insideTransaction()) this._isReady = true
114
+ // of that outer transaction removes the just-created table again. Keep
115
+ // readiness pending for every later allocation in that same transaction;
116
+ // otherwise its table-exists check would incorrectly cache readiness before
117
+ // the outer transaction commits.
118
+ if (!created) {
119
+ if (!this._transactionalReadinessPending) this._isReady = true
120
+ return
121
+ }
122
+
123
+ if (db.insideTransaction()) {
124
+ this._transactionalReadinessPending = true
125
+
126
+ void db.transactionCompletion().then(() => {
127
+ // The transaction may have committed or rolled back. Re-check once
128
+ // on the next allocation, then cache the durable outcome normally.
129
+ this._isReady = false
130
+ this._transactionalReadinessPending = false
131
+ })
132
+ } else {
133
+ this._isReady = true
134
+ this._transactionalReadinessPending = false
135
+ }
117
136
  }, connection)
118
137
 
119
138
  try {
@@ -4,12 +4,11 @@ import Configuration from "../configuration.js"
4
4
  import Logger from "../logger.js"
5
5
  import {scalarModelPrimaryKeyValue} from "../utils/model-primary-key.js"
6
6
  import restArgsError from "../utils/rest-args-error.js"
7
- import sha256Hex from "../utils/sha256-hex.js"
8
- import stableJsonStringify from "../utils/stable-json.js"
9
7
 
10
8
  import {declaredSyncScopeAttributes} from "./sync-scope-attributes.js"
11
- import {deliverDeclaredBroadcasts, upsertSyncRow} from "./sync-change-fanout.js"
9
+ import {deliverDeclaredBroadcasts} from "./sync-change-fanout.js"
12
10
  import {isPublishingSuppressed} from "./sync-publish-suppression.js"
11
+ import {upsertServerOriginSyncRow} from "./upsert-server-origin-sync-row.js"
13
12
  import {VELOCIOUS_SYNC_CHANNEL} from "./sync-channel-name.js"
14
13
 
15
14
  /** @type {{create: "afterCreate", update: "afterUpdate", destroy: "afterDestroy"}} */
@@ -213,8 +212,13 @@ export default class SyncPublisher {
213
212
 
214
213
  await record.connection().afterCommit(async () => {
215
214
  try {
216
- const scopeColumnNames = resourceConfig.scopePlan.map(({columnName}) => columnName)
217
- const syncRow = await this.upsertPublishedSyncRow(attributes, operationScope, scopeColumnNames)
215
+ const syncRow = await upsertServerOriginSyncRow({
216
+ actorForeignKeyColumn: this.config.actorForeignKeyColumn,
217
+ attributes,
218
+ persistenceModel: operationScope,
219
+ scopeColumnNames: resourceConfig.scopePlan.map(({columnName}) => columnName),
220
+ syncModel: this.config.syncModel
221
+ })
218
222
 
219
223
  await this.broadcaster()({
220
224
  body: {
@@ -362,55 +366,6 @@ export default class SyncPublisher {
362
366
  return entry
363
367
  }
364
368
 
365
- /**
366
- * Upserts the published server-origin sync row for a resource identity:
367
- * server-origin rows carry a null actor column (no device to echo the
368
- * change back to), so repeated server changes to one complete resource and
369
- * scope identity reuse and re-sequence one feed row. A database advisory
370
- * lock serializes reconciliation plus the shared upsert because unique
371
- * constraints containing nullable actor/scope columns do not enforce this
372
- * identity portably across supported databases. Reconciliation retains the
373
- * row with the newest feed sequence (then lowest id for a deterministic tie)
374
- * and removes older matching server-origin rows before applying the current
375
- * mutation.
376
- * @param {Record<string, ReturnType<typeof JSON.parse>>} attributes - Snapshotted sync row attributes.
377
- * @param {ReturnType<typeof JSON.parse>} syncModel - Operation-bound or static Sync model interface.
378
- * @param {string[]} scopeColumnNames - Persisted scope columns participating in the complete identity.
379
- * @returns {Promise<ReturnType<typeof JSON.parse>>} Upserted sync row.
380
- */
381
- async upsertPublishedSyncRow(attributes, syncModel = this.config.syncModel, scopeColumnNames = []) {
382
- /** @type {Record<string, ReturnType<typeof JSON.parse>>} */
383
- const identity = {
384
- [this.config.actorForeignKeyColumn]: null,
385
- resource_id: attributes.resource_id,
386
- resource_type: attributes.resource_type
387
- }
388
-
389
- for (const columnName of scopeColumnNames) {
390
- if (!Object.hasOwn(attributes, columnName)) {
391
- throw new Error(`Published sync row identity is missing the scope column ${columnName}`)
392
- }
393
-
394
- identity[columnName] = attributes[columnName]
395
- }
396
-
397
- return await this.config.syncModel.withAdvisoryLock(syncPublisherIdentityLockName(identity), async () => {
398
- const matchingSyncs = await syncModel
399
- .where(identity)
400
- .toArray()
401
-
402
- matchingSyncs.sort(comparePublishedSyncRowsByRecency)
403
-
404
- const [existingSync, ...duplicateSyncs] = matchingSyncs
405
-
406
- for (const duplicateSync of duplicateSyncs) {
407
- await duplicateSync.destroy()
408
- }
409
-
410
- return await upsertSyncRow({attributes, existingSync, syncModel})
411
- }, {dedicatedConnection: true})
412
- }
413
-
414
369
  /**
415
370
  * Returns the broadcaster delivering declared broadcasts: the injected one,
416
371
  * or the configuration's channel broadcast awaited through the pending
@@ -465,40 +420,6 @@ export default class SyncPublisher {
465
420
  }
466
421
  }
467
422
 
468
- /**
469
- * Returns a deterministic, MySQL-safe advisory-lock name for one complete
470
- * server-origin publisher identity. Stable JSON preserves null actor/scope
471
- * components distinctly from strings, and the truncated SHA-256 digest keeps
472
- * the final name below MySQL/MariaDB's 64-character `GET_LOCK` limit.
473
- * @param {Record<string, ReturnType<typeof JSON.parse>>} identity - Complete sync-row identity in column form.
474
- * @returns {string} Advisory-lock name.
475
- */
476
- function syncPublisherIdentityLockName(identity) {
477
- const hash = sha256Hex(stableJsonStringify(identity)).slice(0, 32)
478
-
479
- return `vsp:${hash}`
480
- }
481
-
482
- /**
483
- * Orders matching published rows by the feed's public recency contract so
484
- * legacy duplicates have one deterministic survivor. Server sequences are
485
- * positive and monotonic; a legacy null sequence is older than any assigned
486
- * sequence, and the immutable row id breaks otherwise-equal ties.
487
- * @param {ReturnType<typeof JSON.parse>} left - First matching sync row.
488
- * @param {ReturnType<typeof JSON.parse>} right - Second matching sync row.
489
- * @returns {number} Sort comparison with the canonical survivor first.
490
- */
491
- function comparePublishedSyncRowsByRecency(left, right) {
492
- const leftSequence = left.serverSequence()
493
- const rightSequence = right.serverSequence()
494
-
495
- if (leftSequence === null && rightSequence !== null) return 1
496
- if (leftSequence !== null && rightSequence === null) return -1
497
- if (leftSequence !== rightSequence) return rightSequence - leftSequence
498
-
499
- return left.id().localeCompare(right.id())
500
- }
501
-
502
423
  /**
503
424
  * Resolves a model class's active publish declaration from `static sync`.
504
425
  * Opted-out (`publish: false`) and undeclared models resolve to null; every
@@ -0,0 +1,158 @@
1
+ // @ts-check
2
+
3
+ import restArgsError from "../utils/rest-args-error.js"
4
+ import sha256Hex from "../utils/sha256-hex.js"
5
+ import stableJsonStringify from "../utils/stable-json.js"
6
+
7
+ import {upsertSyncRow} from "./sync-change-fanout.js"
8
+ import {declaredSyncScopeAttributes} from "./sync-scope-attributes.js"
9
+
10
+ /**
11
+ * Atomically reconciles and upserts one server-origin sync row by its complete,
12
+ * null-safe logical identity. The identity contains the null actor, resource
13
+ * type/id, and either every scope column declared by the sync model or the
14
+ * caller's explicit effective scope columns for a supported legacy publisher.
15
+ * A stable portable advisory lock serializes runtime publication and maintenance/backfill calls;
16
+ * legacy duplicates converge to the newest server sequence, then the lowest
17
+ * immutable id, before the survivor is updated and re-sequenced.
18
+ *
19
+ * The lock is acquired through the static sync model on a dedicated connection.
20
+ * `persistenceModel` may be an operation-bound model facade so reads/writes keep
21
+ * their owning operation while sharing the same identity lock as ordinary
22
+ * publisher calls.
23
+ * @param {object} args - Persistence arguments.
24
+ * @param {string} [args.actorForeignKeyColumn] - Persisted actor foreign-key column.
25
+ * @param {Record<string, ReturnType<typeof JSON.parse>>} args.attributes - Complete sync-row mutation attributes.
26
+ * @param {ReturnType<typeof JSON.parse>} [args.persistenceModel] - Optional operation-bound model used for row reads/writes.
27
+ * @param {string[]} [args.scopeColumnNames] - Exact persisted scope columns overriding model declarations for a supported legacy publisher identity.
28
+ * @param {ReturnType<typeof JSON.parse>} args.syncModel - Static sync model owning scope metadata and the advisory lock.
29
+ * @returns {Promise<ReturnType<typeof JSON.parse>>} Created or reconciled sync row.
30
+ */
31
+ export async function upsertServerOriginSyncRow({
32
+ actorForeignKeyColumn = "authentication_token_id",
33
+ attributes,
34
+ persistenceModel,
35
+ scopeColumnNames,
36
+ syncModel,
37
+ ...restArgs
38
+ }) {
39
+ restArgsError(restArgs)
40
+
41
+ if (typeof actorForeignKeyColumn !== "string" || !actorForeignKeyColumn) {
42
+ throw new Error("Server-origin sync row actorForeignKeyColumn must be a non-empty string")
43
+ }
44
+ if (!attributes || typeof attributes !== "object" || Array.isArray(attributes)) {
45
+ throw new Error("Server-origin sync row attributes must be a plain object")
46
+ }
47
+ if (!Object.hasOwn(attributes, actorForeignKeyColumn) || attributes[actorForeignKeyColumn] === undefined) {
48
+ throw new Error(`Server-origin sync row identity must include the actor column ${actorForeignKeyColumn}`)
49
+ }
50
+ if (attributes[actorForeignKeyColumn] !== null) {
51
+ throw new Error(`Server-origin sync row actor column ${actorForeignKeyColumn} must be null`)
52
+ }
53
+
54
+ validateRequiredIdentityString({attributes, columnName: "resource_id"})
55
+ validateRequiredIdentityString({attributes, columnName: "resource_type"})
56
+
57
+ /** @type {Record<string, ReturnType<typeof JSON.parse>>} */
58
+ const identity = {
59
+ [actorForeignKeyColumn]: null,
60
+ resource_id: attributes.resource_id,
61
+ resource_type: attributes.resource_type
62
+ }
63
+ const attributeToColumnName = syncModel.getAttributeNameToColumnNameMap()
64
+ const scopeColumns = new Set(scopeColumnNames || [])
65
+
66
+ if (scopeColumnNames === undefined) {
67
+ for (const scopeAttribute of declaredSyncScopeAttributes(syncModel) || []) {
68
+ const columnName = attributeToColumnName[scopeAttribute]
69
+
70
+ if (!columnName) {
71
+ throw new Error(`${syncModel.name} declares the sync scope attribute ${scopeAttribute} but has no matching column for it`)
72
+ }
73
+
74
+ scopeColumns.add(columnName)
75
+ }
76
+ }
77
+
78
+ for (const columnName of scopeColumns) {
79
+ if (typeof columnName !== "string" || !columnName || !Object.values(attributeToColumnName).includes(columnName)) {
80
+ throw new Error(`Server-origin sync row received an unknown scope column: ${String(columnName)}`)
81
+ }
82
+
83
+ if (!Object.hasOwn(attributes, columnName) || attributes[columnName] === undefined) {
84
+ throw new Error(`Server-origin sync row identity must include the declared scope column ${columnName}`)
85
+ }
86
+
87
+ const value = attributes[columnName]
88
+
89
+ if (value !== null && typeof value !== "string" && (typeof value !== "number" || !Number.isFinite(value))) {
90
+ throw new Error(`Server-origin sync row scope column ${columnName} must be a string, finite number, or null`)
91
+ }
92
+
93
+ identity[columnName] = value
94
+ }
95
+
96
+ const rowModel = persistenceModel || syncModel
97
+
98
+ return await syncModel.withAdvisoryLock(serverOriginSyncRowLockName(identity), async () => {
99
+ const matchingSyncs = await rowModel
100
+ .where(identity)
101
+ .toArray()
102
+
103
+ matchingSyncs.sort(compareServerOriginSyncRowsByRecency)
104
+
105
+ const [existingSync, ...duplicateSyncs] = matchingSyncs
106
+
107
+ for (const duplicateSync of duplicateSyncs) {
108
+ await duplicateSync.destroy()
109
+ }
110
+
111
+ return await upsertSyncRow({attributes, existingSync, syncModel: rowModel})
112
+ }, {dedicatedConnection: true})
113
+ }
114
+
115
+ /**
116
+ * Validates one required string component of the server-origin identity.
117
+ * @param {{attributes: Record<string, ReturnType<typeof JSON.parse>>, columnName: "resource_id" | "resource_type"}} args - Attributes and required column.
118
+ * @returns {void}
119
+ */
120
+ function validateRequiredIdentityString({attributes, columnName}) {
121
+ const value = attributes[columnName]
122
+
123
+ if (typeof value !== "string" || !value) {
124
+ throw new Error(`Server-origin sync row ${columnName} must be a non-empty string`)
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Returns the stable MySQL-safe advisory-lock name shared by every caller of a
130
+ * complete server-origin identity. Stable JSON preserves null identity values,
131
+ * while the truncated SHA-256 digest keeps the name under GET_LOCK's limit.
132
+ * @param {Record<string, ReturnType<typeof JSON.parse>>} identity - Complete sync-row identity in column form.
133
+ * @returns {string} Advisory-lock name.
134
+ */
135
+ function serverOriginSyncRowLockName(identity) {
136
+ const hash = sha256Hex(stableJsonStringify(identity)).slice(0, 32)
137
+
138
+ return `vsp:${hash}`
139
+ }
140
+
141
+ /**
142
+ * Orders matching rows with the canonical deterministic survivor first:
143
+ * newest server sequence, then lowest immutable id. A null legacy sequence is
144
+ * older than every assigned sequence.
145
+ * @param {ReturnType<typeof JSON.parse>} left - First matching sync row.
146
+ * @param {ReturnType<typeof JSON.parse>} right - Second matching sync row.
147
+ * @returns {number} Sort comparison.
148
+ */
149
+ function compareServerOriginSyncRowsByRecency(left, right) {
150
+ const leftSequence = left.serverSequence()
151
+ const rightSequence = right.serverSequence()
152
+
153
+ if (leftSequence === null && rightSequence !== null) return 1
154
+ if (leftSequence !== null && rightSequence === null) return -1
155
+ if (leftSequence !== rightSequence) return rightSequence - leftSequence
156
+
157
+ return left.id().localeCompare(right.id())
158
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "velocious": "build/bin/velocious.js"
4
4
  },
5
5
  "name": "velocious",
6
- "version": "1.0.670",
6
+ "version": "1.0.672",
7
7
  "main": "build/index.js",
8
8
  "types": "build/index.d.ts",
9
9
  "files": [
@@ -1090,6 +1090,36 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
1090
1090
  })
1091
1091
  }
1092
1092
 
1093
+ /**
1094
+ * Resolves a test-shared connection from the per-tenant context providers only.
1095
+ * Unlike {@link testSharedConnection}, this never falls back to the pool default or
1096
+ * the per-configuration shared connection, so it is safe to consult from
1097
+ * `getCurrentConnection` without changing behavior when no per-tenant provider
1098
+ * matches (the production case, where the provider list is empty).
1099
+ *
1100
+ * The provider `matches()` callback may inspect the live tenant context, which is
1101
+ * established during route resolution — after the request-runner installs the async
1102
+ * connection context. This is what lets a test run enroll more than one tenant on a
1103
+ * single pool and route each tenant's in-request queries to its own enrolled
1104
+ * connection.
1105
+ * @returns {import("../drivers/base.js").default | undefined} - Per-tenant shared connection.
1106
+ */
1107
+ testSharedConnectionForCurrentTenant() {
1108
+ for (const {matches, provider} of this._testSharedConnectionProviders.values()) {
1109
+ if (!matches()) continue
1110
+
1111
+ const connection = provider()
1112
+
1113
+ if (connection && !this.connectionMatchesCurrentConfiguration(connection)) {
1114
+ throw new Error(`Test shared connection provider for ${this.identifier} returned a connection for a different database configuration`)
1115
+ }
1116
+
1117
+ return connection
1118
+ }
1119
+
1120
+ return undefined
1121
+ }
1122
+
1093
1123
  /**
1094
1124
  * Runs get current connection.
1095
1125
  * @returns {import("../drivers/base.js").default} - The current connection.
@@ -1109,6 +1139,12 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
1109
1139
  throw new Error(`Couldn't get current connection from that ID: ${id}`)
1110
1140
  }
1111
1141
 
1142
+ if (this._testSharedConnectionProviders.size > 0 && !this.connectionMatchesCurrentConfiguration(currentConnection)) {
1143
+ const perTenantConnection = this.testSharedConnectionForCurrentTenant()
1144
+
1145
+ if (perTenantConnection) return perTenantConnection
1146
+ }
1147
+
1112
1148
  return currentConnection
1113
1149
  }
1114
1150
 
@@ -38,6 +38,7 @@ export default class ServerSequenceAllocator {
38
38
  this.tableName = tableName
39
39
  this._memorySequence = 0
40
40
  this._isReady = false
41
+ this._transactionalReadinessPending = false
41
42
  /** @type {Promise<void> | null} */
42
43
  this._readyPromise = null
43
44
  }
@@ -110,10 +111,28 @@ export default class ServerSequenceAllocator {
110
111
  // DDL joins any transaction already open on this connection (the mixin's
111
112
  // beforeCreate allocation always runs inside the record save transaction),
112
113
  // and on transactional-DDL databases (MSSQL, PostgreSQL, SQLite) a rollback
113
- // of that outer transaction removes the just-created table again. Only
114
- // cache readiness when the table was not created inside an active
115
- // transaction; otherwise the next allocation re-verifies the table.
116
- if (!created || !db.insideTransaction()) this._isReady = true
114
+ // of that outer transaction removes the just-created table again. Keep
115
+ // readiness pending for every later allocation in that same transaction;
116
+ // otherwise its table-exists check would incorrectly cache readiness before
117
+ // the outer transaction commits.
118
+ if (!created) {
119
+ if (!this._transactionalReadinessPending) this._isReady = true
120
+ return
121
+ }
122
+
123
+ if (db.insideTransaction()) {
124
+ this._transactionalReadinessPending = true
125
+
126
+ void db.transactionCompletion().then(() => {
127
+ // The transaction may have committed or rolled back. Re-check once
128
+ // on the next allocation, then cache the durable outcome normally.
129
+ this._isReady = false
130
+ this._transactionalReadinessPending = false
131
+ })
132
+ } else {
133
+ this._isReady = true
134
+ this._transactionalReadinessPending = false
135
+ }
117
136
  }, connection)
118
137
 
119
138
  try {
@@ -4,12 +4,11 @@ import Configuration from "../configuration.js"
4
4
  import Logger from "../logger.js"
5
5
  import {scalarModelPrimaryKeyValue} from "../utils/model-primary-key.js"
6
6
  import restArgsError from "../utils/rest-args-error.js"
7
- import sha256Hex from "../utils/sha256-hex.js"
8
- import stableJsonStringify from "../utils/stable-json.js"
9
7
 
10
8
  import {declaredSyncScopeAttributes} from "./sync-scope-attributes.js"
11
- import {deliverDeclaredBroadcasts, upsertSyncRow} from "./sync-change-fanout.js"
9
+ import {deliverDeclaredBroadcasts} from "./sync-change-fanout.js"
12
10
  import {isPublishingSuppressed} from "./sync-publish-suppression.js"
11
+ import {upsertServerOriginSyncRow} from "./upsert-server-origin-sync-row.js"
13
12
  import {VELOCIOUS_SYNC_CHANNEL} from "./sync-channel-name.js"
14
13
 
15
14
  /** @type {{create: "afterCreate", update: "afterUpdate", destroy: "afterDestroy"}} */
@@ -213,8 +212,13 @@ export default class SyncPublisher {
213
212
 
214
213
  await record.connection().afterCommit(async () => {
215
214
  try {
216
- const scopeColumnNames = resourceConfig.scopePlan.map(({columnName}) => columnName)
217
- const syncRow = await this.upsertPublishedSyncRow(attributes, operationScope, scopeColumnNames)
215
+ const syncRow = await upsertServerOriginSyncRow({
216
+ actorForeignKeyColumn: this.config.actorForeignKeyColumn,
217
+ attributes,
218
+ persistenceModel: operationScope,
219
+ scopeColumnNames: resourceConfig.scopePlan.map(({columnName}) => columnName),
220
+ syncModel: this.config.syncModel
221
+ })
218
222
 
219
223
  await this.broadcaster()({
220
224
  body: {
@@ -362,55 +366,6 @@ export default class SyncPublisher {
362
366
  return entry
363
367
  }
364
368
 
365
- /**
366
- * Upserts the published server-origin sync row for a resource identity:
367
- * server-origin rows carry a null actor column (no device to echo the
368
- * change back to), so repeated server changes to one complete resource and
369
- * scope identity reuse and re-sequence one feed row. A database advisory
370
- * lock serializes reconciliation plus the shared upsert because unique
371
- * constraints containing nullable actor/scope columns do not enforce this
372
- * identity portably across supported databases. Reconciliation retains the
373
- * row with the newest feed sequence (then lowest id for a deterministic tie)
374
- * and removes older matching server-origin rows before applying the current
375
- * mutation.
376
- * @param {Record<string, ReturnType<typeof JSON.parse>>} attributes - Snapshotted sync row attributes.
377
- * @param {ReturnType<typeof JSON.parse>} syncModel - Operation-bound or static Sync model interface.
378
- * @param {string[]} scopeColumnNames - Persisted scope columns participating in the complete identity.
379
- * @returns {Promise<ReturnType<typeof JSON.parse>>} Upserted sync row.
380
- */
381
- async upsertPublishedSyncRow(attributes, syncModel = this.config.syncModel, scopeColumnNames = []) {
382
- /** @type {Record<string, ReturnType<typeof JSON.parse>>} */
383
- const identity = {
384
- [this.config.actorForeignKeyColumn]: null,
385
- resource_id: attributes.resource_id,
386
- resource_type: attributes.resource_type
387
- }
388
-
389
- for (const columnName of scopeColumnNames) {
390
- if (!Object.hasOwn(attributes, columnName)) {
391
- throw new Error(`Published sync row identity is missing the scope column ${columnName}`)
392
- }
393
-
394
- identity[columnName] = attributes[columnName]
395
- }
396
-
397
- return await this.config.syncModel.withAdvisoryLock(syncPublisherIdentityLockName(identity), async () => {
398
- const matchingSyncs = await syncModel
399
- .where(identity)
400
- .toArray()
401
-
402
- matchingSyncs.sort(comparePublishedSyncRowsByRecency)
403
-
404
- const [existingSync, ...duplicateSyncs] = matchingSyncs
405
-
406
- for (const duplicateSync of duplicateSyncs) {
407
- await duplicateSync.destroy()
408
- }
409
-
410
- return await upsertSyncRow({attributes, existingSync, syncModel})
411
- }, {dedicatedConnection: true})
412
- }
413
-
414
369
  /**
415
370
  * Returns the broadcaster delivering declared broadcasts: the injected one,
416
371
  * or the configuration's channel broadcast awaited through the pending
@@ -465,40 +420,6 @@ export default class SyncPublisher {
465
420
  }
466
421
  }
467
422
 
468
- /**
469
- * Returns a deterministic, MySQL-safe advisory-lock name for one complete
470
- * server-origin publisher identity. Stable JSON preserves null actor/scope
471
- * components distinctly from strings, and the truncated SHA-256 digest keeps
472
- * the final name below MySQL/MariaDB's 64-character `GET_LOCK` limit.
473
- * @param {Record<string, ReturnType<typeof JSON.parse>>} identity - Complete sync-row identity in column form.
474
- * @returns {string} Advisory-lock name.
475
- */
476
- function syncPublisherIdentityLockName(identity) {
477
- const hash = sha256Hex(stableJsonStringify(identity)).slice(0, 32)
478
-
479
- return `vsp:${hash}`
480
- }
481
-
482
- /**
483
- * Orders matching published rows by the feed's public recency contract so
484
- * legacy duplicates have one deterministic survivor. Server sequences are
485
- * positive and monotonic; a legacy null sequence is older than any assigned
486
- * sequence, and the immutable row id breaks otherwise-equal ties.
487
- * @param {ReturnType<typeof JSON.parse>} left - First matching sync row.
488
- * @param {ReturnType<typeof JSON.parse>} right - Second matching sync row.
489
- * @returns {number} Sort comparison with the canonical survivor first.
490
- */
491
- function comparePublishedSyncRowsByRecency(left, right) {
492
- const leftSequence = left.serverSequence()
493
- const rightSequence = right.serverSequence()
494
-
495
- if (leftSequence === null && rightSequence !== null) return 1
496
- if (leftSequence !== null && rightSequence === null) return -1
497
- if (leftSequence !== rightSequence) return rightSequence - leftSequence
498
-
499
- return left.id().localeCompare(right.id())
500
- }
501
-
502
423
  /**
503
424
  * Resolves a model class's active publish declaration from `static sync`.
504
425
  * Opted-out (`publish: false`) and undeclared models resolve to null; every