@rvoh/dream 2.26.0 → 2.28.0

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 (331) hide show
  1. package/dist/cjs/src/Dream.js +32 -17
  2. package/dist/cjs/src/db/errors.js +4 -0
  3. package/dist/cjs/src/decorators/field/sortable/Sortable.js +43 -118
  4. package/dist/cjs/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.js +38 -0
  5. package/dist/cjs/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.js +183 -0
  6. package/dist/cjs/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js +40 -0
  7. package/dist/cjs/src/decorators/field/sortable/helpers/canonicalScopeValue.js +194 -0
  8. package/dist/cjs/src/decorators/field/sortable/helpers/clearCachedSortableValues.js +18 -0
  9. package/dist/cjs/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.js +9 -9
  10. package/dist/cjs/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.js +20 -0
  11. package/dist/cjs/src/decorators/field/sortable/helpers/filterQueryToScopeValues.js +32 -0
  12. package/dist/cjs/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.js +34 -0
  13. package/dist/cjs/src/decorators/field/sortable/helpers/performSortableDestroyWork.js +21 -0
  14. package/dist/cjs/src/decorators/field/sortable/helpers/performSortablePositionWork.js +36 -0
  15. package/dist/cjs/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js +56 -0
  16. package/dist/cjs/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.js +84 -0
  17. package/dist/cjs/src/decorators/field/sortable/helpers/resortAllRecords.js +186 -29
  18. package/dist/cjs/src/decorators/field/sortable/helpers/setPosition.js +90 -46
  19. package/dist/cjs/src/decorators/field/sortable/helpers/sortableRowCache.js +73 -0
  20. package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeColumns.js +42 -0
  21. package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeLockKey.js +99 -0
  22. package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeLockKeys.js +72 -0
  23. package/dist/cjs/src/decorators/field/sortable/helpers/sortableSnapshot.js +101 -0
  24. package/dist/cjs/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.js +86 -0
  25. package/dist/cjs/src/decorators/field/sortable/helpers/validPosition.js +21 -4
  26. package/dist/cjs/src/decorators/field/sortable/hooks/afterSortableDestroy.js +24 -7
  27. package/dist/cjs/src/decorators/field/sortable/hooks/afterSortableUpdate.js +10 -0
  28. package/dist/cjs/src/dream/DreamClassTransactionBuilder.js +0 -18
  29. package/dist/cjs/src/dream/Query.js +64 -32
  30. package/dist/cjs/src/dream/QueryDriver/Base.js +75 -2
  31. package/dist/cjs/src/dream/QueryDriver/Kysely.js +19 -12
  32. package/dist/cjs/src/dream/QueryDriver/Postgres.js +74 -0
  33. package/dist/cjs/src/dream/internal/destroyDream.js +60 -6
  34. package/dist/cjs/src/dream/internal/saveDream.js +109 -7
  35. package/dist/cjs/src/dream/internal/similarity/SimilarityBuilder.js +19 -4
  36. package/dist/cjs/src/dream/internal/softDeleteDream.js +13 -1
  37. package/dist/cjs/src/dream/internal/undestroyDream.js +56 -8
  38. package/dist/cjs/src/dream-app/index.js +21 -0
  39. package/dist/cjs/src/errors/SortableBatchRequiresTooManyScopeLocks.js +57 -0
  40. package/dist/cjs/src/errors/SortableRequiresAdvisoryTransactionLocks.js +26 -0
  41. package/dist/cjs/src/errors/SortableScopeDidNotStabilize.js +39 -0
  42. package/dist/cjs/src/errors/SortableScopeLockWaitTimedOut.js +50 -0
  43. package/dist/cjs/src/package-exports/errors.js +3 -0
  44. package/dist/esm/src/Dream.js +32 -17
  45. package/dist/esm/src/db/errors.js +4 -0
  46. package/dist/esm/src/decorators/field/sortable/Sortable.js +43 -118
  47. package/dist/esm/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.js +38 -0
  48. package/dist/esm/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.js +183 -0
  49. package/dist/esm/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js +40 -0
  50. package/dist/esm/src/decorators/field/sortable/helpers/canonicalScopeValue.js +194 -0
  51. package/dist/esm/src/decorators/field/sortable/helpers/clearCachedSortableValues.js +18 -0
  52. package/dist/esm/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.js +9 -9
  53. package/dist/esm/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.js +20 -0
  54. package/dist/esm/src/decorators/field/sortable/helpers/filterQueryToScopeValues.js +32 -0
  55. package/dist/esm/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.js +34 -0
  56. package/dist/esm/src/decorators/field/sortable/helpers/performSortableDestroyWork.js +21 -0
  57. package/dist/esm/src/decorators/field/sortable/helpers/performSortablePositionWork.js +36 -0
  58. package/dist/esm/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js +56 -0
  59. package/dist/esm/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.js +84 -0
  60. package/dist/esm/src/decorators/field/sortable/helpers/resortAllRecords.js +186 -29
  61. package/dist/esm/src/decorators/field/sortable/helpers/setPosition.js +90 -46
  62. package/dist/esm/src/decorators/field/sortable/helpers/sortableRowCache.js +73 -0
  63. package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeColumns.js +42 -0
  64. package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeLockKey.js +99 -0
  65. package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeLockKeys.js +72 -0
  66. package/dist/esm/src/decorators/field/sortable/helpers/sortableSnapshot.js +101 -0
  67. package/dist/esm/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.js +86 -0
  68. package/dist/esm/src/decorators/field/sortable/helpers/validPosition.js +21 -4
  69. package/dist/esm/src/decorators/field/sortable/hooks/afterSortableDestroy.js +24 -7
  70. package/dist/esm/src/decorators/field/sortable/hooks/afterSortableUpdate.js +10 -0
  71. package/dist/esm/src/dream/DreamClassTransactionBuilder.js +0 -18
  72. package/dist/esm/src/dream/Query.js +64 -32
  73. package/dist/esm/src/dream/QueryDriver/Base.js +75 -2
  74. package/dist/esm/src/dream/QueryDriver/Kysely.js +19 -12
  75. package/dist/esm/src/dream/QueryDriver/Postgres.js +74 -0
  76. package/dist/esm/src/dream/internal/destroyDream.js +60 -6
  77. package/dist/esm/src/dream/internal/saveDream.js +109 -7
  78. package/dist/esm/src/dream/internal/similarity/SimilarityBuilder.js +19 -4
  79. package/dist/esm/src/dream/internal/softDeleteDream.js +13 -1
  80. package/dist/esm/src/dream/internal/undestroyDream.js +56 -8
  81. package/dist/esm/src/dream-app/index.js +21 -0
  82. package/dist/esm/src/errors/SortableBatchRequiresTooManyScopeLocks.js +57 -0
  83. package/dist/esm/src/errors/SortableRequiresAdvisoryTransactionLocks.js +26 -0
  84. package/dist/esm/src/errors/SortableScopeDidNotStabilize.js +39 -0
  85. package/dist/esm/src/errors/SortableScopeLockWaitTimedOut.js +50 -0
  86. package/dist/esm/src/package-exports/errors.js +3 -0
  87. package/dist/types/src/Dream.d.ts +35 -2
  88. package/dist/types/src/db/errors.d.ts +2 -0
  89. package/dist/types/src/decorators/field/sortable/Sortable.d.ts +33 -0
  90. package/dist/types/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.d.ts +25 -0
  91. package/dist/types/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.d.ts +91 -0
  92. package/dist/types/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.d.ts +31 -0
  93. package/dist/types/src/decorators/field/sortable/helpers/canonicalScopeValue.d.ts +85 -0
  94. package/dist/types/src/decorators/field/sortable/helpers/clearCachedSortableValues.d.ts +13 -0
  95. package/dist/types/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.d.ts +8 -1
  96. package/dist/types/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.d.ts +24 -0
  97. package/dist/types/src/decorators/field/sortable/helpers/filterQueryToScopeValues.d.ts +22 -0
  98. package/dist/types/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.d.ts +27 -0
  99. package/dist/types/src/decorators/field/sortable/helpers/performSortableDestroyWork.d.ts +14 -0
  100. package/dist/types/src/decorators/field/sortable/helpers/performSortablePositionWork.d.ts +15 -0
  101. package/dist/types/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.d.ts +41 -0
  102. package/dist/types/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.d.ts +35 -0
  103. package/dist/types/src/decorators/field/sortable/helpers/resortAllRecords.d.ts +32 -0
  104. package/dist/types/src/decorators/field/sortable/helpers/setPosition.d.ts +27 -11
  105. package/dist/types/src/decorators/field/sortable/helpers/sortableRowCache.d.ts +48 -0
  106. package/dist/types/src/decorators/field/sortable/helpers/sortableScopeColumns.d.ts +30 -0
  107. package/dist/types/src/decorators/field/sortable/helpers/sortableScopeLockKey.d.ts +44 -0
  108. package/dist/types/src/decorators/field/sortable/helpers/sortableScopeLockKeys.d.ts +51 -0
  109. package/dist/types/src/decorators/field/sortable/helpers/sortableSnapshot.d.ts +92 -0
  110. package/dist/types/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.d.ts +59 -0
  111. package/dist/types/src/decorators/field/sortable/helpers/validPosition.d.ts +14 -2
  112. package/dist/types/src/decorators/field/sortable/hooks/afterSortableCreate.d.ts +1 -1
  113. package/dist/types/src/decorators/field/sortable/hooks/afterSortableDestroy.d.ts +3 -1
  114. package/dist/types/src/decorators/field/sortable/hooks/afterSortableUpdate.d.ts +1 -1
  115. package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +3 -2
  116. package/dist/types/src/dream/Query.d.ts +44 -9
  117. package/dist/types/src/dream/QueryDriver/Base.d.ts +61 -1
  118. package/dist/types/src/dream/QueryDriver/Kysely.d.ts +6 -2
  119. package/dist/types/src/dream/QueryDriver/Postgres.d.ts +40 -0
  120. package/dist/types/src/dream/internal/softDeleteDream.d.ts +7 -1
  121. package/dist/types/src/dream-app/index.d.ts +18 -2
  122. package/dist/types/src/errors/SortableBatchRequiresTooManyScopeLocks.d.ts +37 -0
  123. package/dist/types/src/errors/SortableRequiresAdvisoryTransactionLocks.d.ts +13 -0
  124. package/dist/types/src/errors/SortableScopeDidNotStabilize.d.ts +23 -0
  125. package/dist/types/src/errors/SortableScopeLockWaitTimedOut.d.ts +34 -0
  126. package/dist/types/src/package-exports/errors.d.ts +3 -0
  127. package/dist/types/src/types/associations/shared.d.ts +1 -1
  128. package/dist/types/src/types/dream.d.ts +3 -0
  129. package/dist/types/src/types/query.d.ts +1 -1
  130. package/dist/types/src/types/types/associations/shared.ts +8 -6
  131. package/dist/types/src/types/types/dream.ts +6 -0
  132. package/dist/types/src/types/types/query.ts +1 -1
  133. package/docs/assets/hierarchy.js +1 -1
  134. package/docs/assets/navigation.js +1 -1
  135. package/docs/assets/search.js +1 -1
  136. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  137. package/docs/classes/db.KyselyQueryDriver.html +88 -41
  138. package/docs/classes/db.PostgresQueryDriver.html +82 -42
  139. package/docs/classes/db.QueryDriverBase.html +86 -39
  140. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  141. package/docs/classes/errors.ColumnOverflow.html +3 -3
  142. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  143. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  144. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  145. package/docs/classes/errors.DecryptionError.html +2 -2
  146. package/docs/classes/errors.DecryptionParseError.html +2 -2
  147. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  148. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  149. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  150. package/docs/classes/errors.InvalidClockTime.html +2 -2
  151. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  152. package/docs/classes/errors.InvalidDateTime.html +2 -2
  153. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  154. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  155. package/docs/classes/errors.NotNullViolation.html +3 -3
  156. package/docs/classes/errors.RecordNotFound.html +3 -3
  157. package/docs/classes/errors.SortableBatchRequiresTooManyScopeLocks.html +61 -0
  158. package/docs/classes/errors.SortableScopeDidNotStabilize.html +48 -0
  159. package/docs/classes/errors.SortableScopeLockWaitTimedOut.html +58 -0
  160. package/docs/classes/errors.ValidationError.html +3 -3
  161. package/docs/classes/index.CalendarDate.html +33 -33
  162. package/docs/classes/index.ClockTime.html +32 -32
  163. package/docs/classes/index.ClockTimeTz.html +35 -35
  164. package/docs/classes/index.DateTime.html +86 -86
  165. package/docs/classes/index.Decorators.html +19 -19
  166. package/docs/classes/index.Dream.html +159 -124
  167. package/docs/classes/index.DreamApp.html +11 -10
  168. package/docs/classes/index.DreamTransaction.html +2 -2
  169. package/docs/classes/index.Env.html +2 -2
  170. package/docs/classes/index.Query.html +122 -99
  171. package/docs/classes/system.CliFileWriter.html +4 -4
  172. package/docs/classes/system.DreamBin.html +2 -2
  173. package/docs/classes/system.DreamCLI.html +7 -7
  174. package/docs/classes/system.DreamImporter.html +2 -2
  175. package/docs/classes/system.DreamLogos.html +2 -2
  176. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  177. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  178. package/docs/classes/system.PathHelpers.html +3 -3
  179. package/docs/classes/utils.Encrypt.html +3 -3
  180. package/docs/classes/utils.Range.html +2 -2
  181. package/docs/functions/db.closeAllDbConnections.html +1 -1
  182. package/docs/functions/db.dreamDbConnections.html +1 -1
  183. package/docs/functions/db.untypedDb.html +1 -1
  184. package/docs/functions/db.validateColumn.html +1 -1
  185. package/docs/functions/db.validateTable.html +1 -1
  186. package/docs/functions/errors.pgErrorType.html +1 -1
  187. package/docs/functions/index.DreamSerializer.html +1 -1
  188. package/docs/functions/index.ObjectSerializer.html +1 -1
  189. package/docs/functions/index.ReplicaSafe.html +1 -1
  190. package/docs/functions/index.STI.html +1 -1
  191. package/docs/functions/index.SoftDelete.html +1 -1
  192. package/docs/functions/utils.camelize.html +1 -1
  193. package/docs/functions/utils.capitalize.html +1 -1
  194. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  195. package/docs/functions/utils.compact.html +1 -1
  196. package/docs/functions/utils.groupBy.html +1 -1
  197. package/docs/functions/utils.hyphenize.html +1 -1
  198. package/docs/functions/utils.intersection.html +1 -1
  199. package/docs/functions/utils.isEmpty.html +1 -1
  200. package/docs/functions/utils.normalizeUnicode.html +1 -1
  201. package/docs/functions/utils.pascalize.html +1 -1
  202. package/docs/functions/utils.percent.html +1 -1
  203. package/docs/functions/utils.range.html +1 -1
  204. package/docs/functions/utils.round.html +1 -1
  205. package/docs/functions/utils.sanitizeString.html +1 -1
  206. package/docs/functions/utils.snakeify.html +1 -1
  207. package/docs/functions/utils.sort.html +2 -2
  208. package/docs/functions/utils.sortBy.html +1 -1
  209. package/docs/functions/utils.sortObjectByKey.html +1 -1
  210. package/docs/functions/utils.sortObjectByValue.html +1 -1
  211. package/docs/functions/utils.uncapitalize.html +1 -1
  212. package/docs/functions/utils.uniq.html +1 -1
  213. package/docs/hierarchy.html +1 -1
  214. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  215. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  216. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  217. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  218. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  219. package/docs/interfaces/types.DecoratorContext.html +2 -2
  220. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  221. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  222. package/docs/interfaces/types.DreamDbConfig.html +5 -5
  223. package/docs/interfaces/types.DurationObject.html +2 -2
  224. package/docs/interfaces/types.EncryptOptions.html +2 -2
  225. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  226. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  227. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  228. package/docs/modules/errors.html +1 -1
  229. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  230. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  231. package/docs/types/openapi.OpenapiFormats.html +1 -1
  232. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  233. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  234. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  235. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  236. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  237. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  238. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  239. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  240. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  241. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  242. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  243. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  244. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  245. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  246. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  247. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  248. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  249. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  250. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  251. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  252. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  253. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  254. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  255. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  256. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  257. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  258. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  259. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  260. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  261. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  262. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  263. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  264. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  265. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  266. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  267. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  268. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  269. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  270. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  271. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  272. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  273. package/docs/types/types.CalendarDateObject.html +1 -1
  274. package/docs/types/types.Camelized.html +1 -1
  275. package/docs/types/types.ClockTimeObject.html +1 -1
  276. package/docs/types/types.DbConnectionType.html +1 -1
  277. package/docs/types/types.DbTypes.html +1 -1
  278. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  279. package/docs/types/types.DreamAttributes.html +1 -1
  280. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  281. package/docs/types/types.DreamClassColumn.html +1 -1
  282. package/docs/types/types.DreamColumn.html +1 -1
  283. package/docs/types/types.DreamColumnNames.html +1 -1
  284. package/docs/types/types.DreamLogLevel.html +1 -1
  285. package/docs/types/types.DreamLogger.html +2 -2
  286. package/docs/types/types.DreamModelSerializerType.html +1 -1
  287. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  288. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  289. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  290. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  291. package/docs/types/types.DreamSerializable.html +1 -1
  292. package/docs/types/types.DreamSerializableArray.html +1 -1
  293. package/docs/types/types.DreamSerializerKey.html +1 -1
  294. package/docs/types/types.DreamSerializers.html +1 -1
  295. package/docs/types/types.DreamVirtualColumns.html +1 -1
  296. package/docs/types/types.DurationUnit.html +1 -1
  297. package/docs/types/types.EncryptAlgorithm.html +1 -1
  298. package/docs/types/types.HasManyStatement.html +1 -1
  299. package/docs/types/types.HasOneStatement.html +1 -1
  300. package/docs/types/types.Hyphenized.html +1 -1
  301. package/docs/types/types.Pascalized.html +1 -1
  302. package/docs/types/types.PrimaryKeyType.html +1 -1
  303. package/docs/types/types.RoundingPrecision.html +1 -1
  304. package/docs/types/types.SerializerCasing.html +1 -1
  305. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  306. package/docs/types/types.Snakeified.html +1 -1
  307. package/docs/types/types.StrictInterface.html +1 -1
  308. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  309. package/docs/types/types.UpdateableProperties.html +1 -1
  310. package/docs/types/types.ValidationType.html +1 -1
  311. package/docs/types/types.ViewModel.html +2 -2
  312. package/docs/types/types.ViewModelClass.html +1 -1
  313. package/docs/types/types.WeekdayName.html +1 -1
  314. package/docs/types/types.WhereStatementForDream.html +1 -1
  315. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  316. package/docs/variables/index.DreamConst.html +1 -1
  317. package/docs/variables/index.ops.html +1 -1
  318. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  319. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  320. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  321. package/docs/variables/system.primaryKeyTypes.html +1 -1
  322. package/package.json +1 -1
  323. package/dist/cjs/src/decorators/field/sortable/helpers/applySortableScopeToQuery.js +0 -13
  324. package/dist/cjs/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.js +0 -7
  325. package/dist/cjs/src/decorators/field/sortable/hooks/beforeSortableSave.js +0 -41
  326. package/dist/esm/src/decorators/field/sortable/helpers/applySortableScopeToQuery.js +0 -13
  327. package/dist/esm/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.js +0 -7
  328. package/dist/esm/src/decorators/field/sortable/hooks/beforeSortableSave.js +0 -41
  329. package/dist/types/src/decorators/field/sortable/helpers/applySortableScopeToQuery.d.ts +0 -12
  330. package/dist/types/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.d.ts +0 -12
  331. package/dist/types/src/decorators/field/sortable/hooks/beforeSortableSave.d.ts +0 -12
@@ -0,0 +1,183 @@
1
+ import Dream from '../../../../Dream.js';
2
+ import SortableBatchRequiresTooManyScopeLocks from '../../../../errors/SortableBatchRequiresTooManyScopeLocks.js';
3
+ import heldSortableScopeLockKeys from './heldSortableScopeLockKeys.js';
4
+ import sortableScopeLockKey from './sortableScopeLockKey.js';
5
+ import { cacheSortableRowsReadUnderLock } from './sortableRowCache.js';
6
+ import { sortableScopeColumnPairs } from './sortableScopeColumns.js';
7
+ import stabilizeSortableScopeLocks from './stabilizeSortableScopeLocks.js';
8
+ /**
9
+ * The most advisory sort-scope locks a batch preflight will let one transaction
10
+ * hold.
11
+ *
12
+ * Advisory locks are entries in a cluster-wide shared lock table sized at
13
+ * startup (`max_locks_per_transaction` × `max_connections`, 6400 entries under
14
+ * the stock settings), and a transaction holds every key it takes until it
15
+ * ends. `batchSize` is the caller's to choose and nothing bounds the number of
16
+ * distinct sort scopes a batch's records span, so without a limit a wide enough
17
+ * batch over a high-cardinality scope can starve every other connection in the
18
+ * cluster of a resource they all draw on — a failure that lands on unrelated
19
+ * transactions rather than on the one that caused it.
20
+ *
21
+ * The count is **cumulative over the transaction**: each preflight round counts
22
+ * the keys the transaction already holds together with the keys that round
23
+ * would add. So the bound holds across the rounds of one stabilization loop
24
+ * (each of which can discover a different set of scopes), and across the
25
+ * batches of a multi-batch run inside a caller-owned transaction, where
26
+ * advisory locks are released only when the caller commits.
27
+ *
28
+ * The headroom this leaves: the locked-batch defaults are ten records per batch
29
+ * (`Query.BATCH_SIZES.LOCKED_DESTROY` / `LOCKED_UPDATE`), so reaching the bound
30
+ * takes a caller who raises `batchSize` by two orders of magnitude and whose
31
+ * records each occupy a sort scope of their own — or a long run of such batches
32
+ * inside one transaction. Two shapes consume the budget faster than one key per
33
+ * record: a model declaring more than one sortable field needs one key per
34
+ * field, and the attribute form of `update` needs up to two per record per
35
+ * field, since the record's current scope and its destination scope are
36
+ * different keys.
37
+ *
38
+ * Only the batch preflight enforces this. A per-record save, destroy or
39
+ * undestroy takes its keys without counting them, so what they take is counted
40
+ * by the next preflight in the same transaction but cannot itself raise.
41
+ */
42
+ export const MAX_SORTABLE_BATCH_SCOPE_LOCKS = 1000;
43
+ /**
44
+ * @internal
45
+ *
46
+ * The operation-level scope-lock preflight for the batched, row-locking query
47
+ * APIs — `Query#destroy({ lock: true })` and the attribute form of
48
+ * `Query#update(attributes, { lock: true })`.
49
+ *
50
+ * Why it exists: those operations take *row* locks (`takeAll({ lock: true })`)
51
+ * and then run per-record sortable work that takes *advisory* scope locks. Two
52
+ * transactions running in that order can each hold what the other is waiting
53
+ * for. Acquiring every advisory key the batch needs — across every sortable
54
+ * field and every candidate record — in one sorted pass, before a single row is
55
+ * claimed, removes the inversion: the advisory locks are always taken first, and
56
+ * always in the same order.
57
+ *
58
+ * The key set cannot simply be read once: `Query.lockedBatches` plucks only
59
+ * primary keys, so the candidates' scope columns have to be read separately, and
60
+ * that first read is not protected by any lock. So the read is stabilized the
61
+ * same way the per-record paths stabilize theirs, through the same
62
+ * `stabilizeSortableScopeLocks` protocol — acquire on what the unlocked read
63
+ * implies, re-read under those locks, take any additional key the re-read
64
+ * reveals — with the same bound, and the same `SortableScopeDidNotStabilize`
65
+ * when a batch of rows keeps moving between scopes faster than it can be read.
66
+ *
67
+ * A model with no sortable fields never reaches the driver seam, so this is
68
+ * inert — and free — for every other model.
69
+ *
70
+ * How many keys the transaction may hold is bounded — see
71
+ * {@link MAX_SORTABLE_BATCH_SCOPE_LOCKS} — and a batch that would carry it past
72
+ * the bound is refused before it claims any row. The first round refuses before
73
+ * anything at all is locked; a later round refuses after that loop's earlier
74
+ * rounds took their keys, which stay held until the transaction ends like every
75
+ * advisory lock this branch takes.
76
+ *
77
+ * What the preflight can and cannot see: `incomingScopeValue` recognizes an
78
+ * attribute that names a scope column directly or the BelongsTo association
79
+ * backing it. A destination scope produced any other way — by a custom setter,
80
+ * a virtual attribute, or a `beforeSave` hook that moves the record between
81
+ * scopes during the per-record save — is invisible here, and those shapes are
82
+ * preflight fallbacks like the callback form of `update` and a multi-batch run
83
+ * inside a caller-owned transaction: the missing key is taken as the
84
+ * per-record save reaches it, after this batch's row locks, and the residual
85
+ * failure is Postgres's `40P01` deadlock error — a visible, retryable failure
86
+ * rather than a silent wrong ordering.
87
+ *
88
+ * @param dreamInstance - a bare instance of the Query's model, used for the
89
+ * table, the connection, and the scope column resolution
90
+ * @param txn - the batch's transaction; the locks live until it ends
91
+ * @param primaryKeyValues - the candidate records this batch will try to claim
92
+ * @param incomingAttributes - the attributes the operation will write, when they
93
+ * are knowable before the claim (the attribute form of `update`); a scope
94
+ * column among them means the batch also needs the destination scope's key
95
+ */
96
+ export default async function acquireStabilizedSortableBatchLocks(dreamInstance, txn, primaryKeyValues, incomingAttributes) {
97
+ const configs = (dreamInstance.constructor['sortableFields'] ??
98
+ []);
99
+ if (!configs.length || !primaryKeyValues.length)
100
+ return;
101
+ const primaryKey = dreamInstance['_primaryKey'];
102
+ // `[scope member, column]` pairs, so that an incoming attribute naming the
103
+ // BelongsTo association rather than its foreign key still lines up
104
+ const scopeColumnsByPositionField = new Map();
105
+ const columns = new Set([primaryKey]);
106
+ for (const { positionField, scope } of configs) {
107
+ const scopeColumns = sortableScopeColumnPairs(dreamInstance, scope);
108
+ // the position columns are not part of the key derivation; they are read
109
+ // because this read is the one the per-record work would otherwise repeat,
110
+ // and a snapshot is a position as well as a sort scope
111
+ columns.add(positionField);
112
+ scopeColumns.forEach(([, column]) => columns.add(column));
113
+ scopeColumnsByPositionField.set(positionField, scopeColumns);
114
+ }
115
+ // the live set of keys this transaction holds, which every acquisition adds
116
+ // to: what the bound is counted against, so that neither a later round of
117
+ // this loop nor a later batch of a caller-owned transaction can pass the
118
+ // check a bound's worth at a time
119
+ const heldKeys = heldSortableScopeLockKeys(txn);
120
+ // the read of the pass that converged — taken under every lock this preflight
121
+ // holds — published to the row cache once the loop is through
122
+ let rowsReadUnderEveryLock = [];
123
+ // every pass re-reads the candidates: the first read is unprotected, and each
124
+ // later one is taken under the locks the previous pass acquired
125
+ await stabilizeSortableScopeLocks(dreamInstance, txn, async () => {
126
+ const rows = (await txn.kyselyTransaction
127
+ .selectFrom(dreamInstance.table)
128
+ .where(primaryKey, 'in', primaryKeyValues)
129
+ .select([...columns])
130
+ .execute());
131
+ const keys = new Set();
132
+ for (const row of rows) {
133
+ for (const config of configs) {
134
+ const scopeColumns = scopeColumnsByPositionField.get(config.positionField);
135
+ keys.add(sortableScopeLockKey(dreamInstance, config.positionField, scopeColumns.map(([, column]) => [column, row[column] ?? null])));
136
+ if (incomingAttributes) {
137
+ keys.add(sortableScopeLockKey(dreamInstance, config.positionField, scopeColumns.map(([singleScope, column]) => {
138
+ const incoming = incomingScopeValue(incomingAttributes, singleScope, column);
139
+ return [column, incoming === NOT_WRITTEN ? (row[column] ?? null) : incoming];
140
+ })));
141
+ }
142
+ }
143
+ }
144
+ // counted before this round acquires anything, and counted over everything
145
+ // the transaction would hold once it had: the keys it already holds plus
146
+ // the ones this round adds to them
147
+ let keyCount = heldKeys.size;
148
+ for (const key of keys)
149
+ if (!heldKeys.has(key))
150
+ keyCount++;
151
+ if (keyCount > MAX_SORTABLE_BATCH_SCOPE_LOCKS)
152
+ throw new SortableBatchRequiresTooManyScopeLocks(dreamInstance['sanitizedConstructorName'], keyCount, MAX_SORTABLE_BATCH_SCOPE_LOCKS);
153
+ rowsReadUnderEveryLock = rows;
154
+ return keys;
155
+ });
156
+ // published only now the loop has converged, so that the rows the per-record
157
+ // snapshots consume are the ones read under every lock the preflight holds —
158
+ // and so that a preflight which raises publishes nothing. Neither failure
159
+ // exit aborts the transaction, so a caller that catches one inside a
160
+ // transaction of its own would otherwise go on to find rows here that no lock
161
+ // of theirs protects.
162
+ cacheSortableRowsReadUnderLock(txn, dreamInstance.table, primaryKey, rowsReadUnderEveryLock);
163
+ }
164
+ /**
165
+ * Distinguishes "this operation does not write that scope column" from "this
166
+ * operation writes null into it", which are different destination scopes.
167
+ */
168
+ const NOT_WRITTEN = Symbol('sortable scope column not written by this operation');
169
+ /**
170
+ * The value a scope column is about to be written with, or {@link NOT_WRITTEN}
171
+ * when this operation does not touch it. A scope member can be named either by
172
+ * its column (`{ userId: 3 }`) or by the BelongsTo association the column backs
173
+ * (`{ user: someUser }`), and both forms reach `update` as attributes.
174
+ */
175
+ function incomingScopeValue(incomingAttributes, singleScope, column) {
176
+ if (Object.prototype.hasOwnProperty.call(incomingAttributes, column))
177
+ return incomingAttributes[column] ?? null;
178
+ if (Object.prototype.hasOwnProperty.call(incomingAttributes, singleScope)) {
179
+ const value = incomingAttributes[singleScope];
180
+ return value instanceof Dream ? value.primaryKeyValue() : (value ?? null);
181
+ }
182
+ return NOT_WRITTEN;
183
+ }
@@ -0,0 +1,40 @@
1
+ import { sortableSaveLockKeys } from './sortableScopeLockKeys.js';
2
+ import { readSortableSnapshots } from './sortableSnapshot.js';
3
+ import stabilizeSortableScopeLocks from './stabilizeSortableScopeLocks.js';
4
+ /**
5
+ * @internal
6
+ *
7
+ * Takes the scope locks a per-record position-mutating operation needs, and
8
+ * returns the database snapshot read under them. Every per-record path goes
9
+ * through this: a position- or scope-changing save (from `saveDream`'s sortable
10
+ * phase), a destroy (from the sortable destroy phase), and an undestroy (from
11
+ * `doUndestroyDream`). A destroy and an undestroy have no incoming scope
12
+ * values, so their instance-derived key set is a single key per sortable field —
13
+ * the scope the instance believes the row occupies — and the snapshot is what
14
+ * corrects it.
15
+ *
16
+ * The first pass locks the key set the instance implies, since the snapshot
17
+ * cannot be read until something is locked; every later pass locks what the
18
+ * snapshot read under those locks reveals. `stabilizeSortableScopeLocks` owns
19
+ * that protocol, its bound, and its failure — including the guarantee that a
20
+ * second pass always runs, so the snapshot is read under the locks even when
21
+ * the transaction already held every key the instance implies (a record being
22
+ * walked inside a locked batch, whose preflight took the whole batch's keys).
23
+ *
24
+ * @param dream - the record whose scope is being locked
25
+ * @param txn - the transaction the operation's write runs in
26
+ * @param configs - the sortable fields this operation performs position work for
27
+ * @returns the snapshot read under the locks, alongside whether the row was
28
+ * physically there to read — an empty snapshot map on its own cannot say
29
+ */
30
+ export default async function acquireStabilizedSortableScopeLocks(dream, txn, configs) {
31
+ let read = { rowExists: true, snapshots: new Map() };
32
+ await stabilizeSortableScopeLocks(dream, txn, async (round) => {
33
+ // round 0 runs before anything is locked, so there is nothing safe to read
34
+ // yet: the instance's own values are what the first acquisition goes on
35
+ if (round > 0)
36
+ read = await readSortableSnapshots(dream, txn, configs);
37
+ return sortableSaveLockKeys(dream, configs, read.snapshots);
38
+ });
39
+ return read;
40
+ }
@@ -0,0 +1,194 @@
1
+ import cachedTypeForAttribute from '../../../../helpers/db/cachedTypeForAttribute.js';
2
+ const UTF8 = new TextEncoder();
3
+ /**
4
+ * @internal
5
+ *
6
+ * The canonical text form of one sort-scope value — the thing an advisory lock
7
+ * key and a resort group identity are actually built from.
8
+ *
9
+ * Two writers of the same physical sort scope have to derive the same key, and
10
+ * they do not always hold the value in the same JavaScript shape. A save that
11
+ * has not been written yet holds the *pending* value the caller supplied
12
+ * (`'1.00'`, an uppercase UUID, an object whose keys are in source order),
13
+ * while a snapshot read, a create's post-INSERT read and `resort`'s table read
14
+ * hold the value Postgres *hydrated* (`1`, a lowercase UUID, an object whose
15
+ * keys are in jsonb's own order). Postgres considers those pairs one value;
16
+ * `typeof`-only normalization considers them two, and two keys do not exclude
17
+ * each other.
18
+ *
19
+ * So canonicalization is keyed on the column's runtime `dbType`, and the
20
+ * canonical form of each type is **the form the hydrated value already takes**.
21
+ * That anchoring is what makes this safe to ship without a migration or a
22
+ * coordinated deploy: no key derived from a database-hydrated value moves, and
23
+ * the pending-side keys that do move are exactly the ones that disagree with
24
+ * every hydrated-path writer of the same scope today.
25
+ *
26
+ * The classes handled, and what the hydrated value looks like:
27
+ *
28
+ * - `uuid` — Postgres renders UUIDs lowercase, so the canonical form is
29
+ * lowercase.
30
+ * - `numeric` / `real` / `double precision` — hydrated through
31
+ * `parsePostgresDecimal`, i.e. `parseFloat`, so `'1.00'` and `1` both
32
+ * canonicalize to `'1'`.
33
+ * - `jsonb` — hydrated by `JSON.parse` of jsonb's own output, whose object keys
34
+ * are ordered by key length and then bytewise. Objects are re-emitted in that
35
+ * order, recursively, so property insertion order stops mattering.
36
+ * - `boolean` — hydrated as a JavaScript boolean; Postgres's own input literals
37
+ * (`'t'`, `'yes'`, `'0'`, …) canonicalize to `'true'`/`'false'`.
38
+ * - `date` and the four timestamp/time types — hydrated as `CalendarDate`,
39
+ * `DateTime`, `ClockTime` or `ClockTimeTz`, whose JSON form is the quoted ISO
40
+ * string. Strings are quoted the same way so a pending `'2026-02-07'` and a
41
+ * hydrated `CalendarDate` agree.
42
+ *
43
+ * Everything else — `bigint` and `character varying`, which is to say the
44
+ * foreign keys and text columns nearly every sort scope is made of — is left on
45
+ * the untyped normalization below, unchanged.
46
+ *
47
+ * Two residuals are known and accepted rather than fixed:
48
+ *
49
+ * - **`citext`**: Postgres stores the case a row was written with while
50
+ * comparing case-insensitively, so two rows of one physical scope hydrate to
51
+ * values that differ in case and split the key on their own. Lowercasing
52
+ * would fix the split but would *move* hydrated keys, which is the one thing
53
+ * this design refuses to do.
54
+ * - **Temporal values that agree on the instant but not on the text**: a zone
55
+ * offset or a fractional-second precision that differs from the hydrated
56
+ * rendering still yields a different key. Only the string-vs-object quoting
57
+ * mismatch is canonicalized.
58
+ *
59
+ * Both are over-serialization or a missed exclusion between two *pending*-side
60
+ * writers, never a corrupt position under a key two writers do share.
61
+ *
62
+ * @param dream - the record whose scope column is being canonicalized
63
+ * @param column - the physical database column
64
+ * @param value - the pending or hydrated value held for that column
65
+ * @returns the canonical text form, or `null` for an absent value
66
+ */
67
+ export default function canonicalScopeValue(dream, column, value) {
68
+ if (value === null || value === undefined)
69
+ return null;
70
+ switch (cachedTypeForAttribute(dream.constructor, column)) {
71
+ case 'uuid':
72
+ return canonicalUuid(value);
73
+ case 'numeric':
74
+ case 'real':
75
+ case 'double precision':
76
+ return canonicalNumeric(value);
77
+ case 'jsonb':
78
+ return canonicalJsonb(value);
79
+ case 'boolean':
80
+ return canonicalBoolean(value);
81
+ case 'date':
82
+ case 'timestamp without time zone':
83
+ case 'timestamp with time zone':
84
+ case 'time without time zone':
85
+ case 'time with time zone':
86
+ return canonicalTemporal(value);
87
+ default:
88
+ return untypedScopeValue(value);
89
+ }
90
+ }
91
+ /**
92
+ * @internal
93
+ *
94
+ * The type-blind fallback, and the shape every unrecognized `dbType` keeps:
95
+ * the same foreign key can arrive as a string or a number depending on the path
96
+ * that loaded it, so values are rendered as text and null is kept distinct from
97
+ * every string, including the empty one.
98
+ */
99
+ export function untypedScopeValue(value) {
100
+ if (value === null || value === undefined)
101
+ return null;
102
+ switch (typeof value) {
103
+ case 'string':
104
+ return value;
105
+ case 'number':
106
+ case 'bigint':
107
+ case 'boolean':
108
+ return value.toString();
109
+ default:
110
+ return JSON.stringify(value) ?? null;
111
+ }
112
+ }
113
+ function canonicalUuid(value) {
114
+ return typeof value === 'string' ? value.toLowerCase() : untypedScopeValue(value);
115
+ }
116
+ function canonicalNumeric(value) {
117
+ const parsed = typeof value === 'number' ? value : parseFloat(String(value));
118
+ return Number.isFinite(parsed) ? String(parsed) : untypedScopeValue(value);
119
+ }
120
+ /**
121
+ * @internal
122
+ *
123
+ * Postgres's own boolean input literals, lower-cased and trimmed, which a
124
+ * pending value may hold while the hydrated value is a JavaScript boolean.
125
+ * Exported so the spec drives its cases from the sets themselves rather than
126
+ * from a hand-copied list that can silently drift out of step with them.
127
+ */
128
+ export const BOOLEAN_TRUE_LITERALS = new Set(['t', 'true', 'y', 'yes', 'on', '1']);
129
+ export const BOOLEAN_FALSE_LITERALS = new Set(['f', 'false', 'n', 'no', 'off', '0']);
130
+ function canonicalBoolean(value) {
131
+ if (typeof value === 'boolean')
132
+ return value ? 'true' : 'false';
133
+ if (typeof value === 'string') {
134
+ const literal = value.trim().toLowerCase();
135
+ if (BOOLEAN_TRUE_LITERALS.has(literal))
136
+ return 'true';
137
+ if (BOOLEAN_FALSE_LITERALS.has(literal))
138
+ return 'false';
139
+ }
140
+ return untypedScopeValue(value);
141
+ }
142
+ /**
143
+ * Temporal columns hydrate as `CalendarDate`/`DateTime`/`ClockTime` instances
144
+ * whose `toJSON` is the ISO string, so JSON-encoding both shapes puts a pending
145
+ * ISO string and its hydrated counterpart on the same quoted text.
146
+ */
147
+ function canonicalTemporal(value) {
148
+ return JSON.stringify(value) ?? null;
149
+ }
150
+ /**
151
+ * jsonb neither stores nor renders property insertion order: it orders object
152
+ * keys by length and then bytewise, and a hydrated value therefore always
153
+ * arrives in that order. Re-emitting in the same order makes a pending object
154
+ * literal agree with it, and leaves every hydrated value byte-identical.
155
+ */
156
+ function canonicalJsonb(value) {
157
+ const parsed = typeof value === 'string' ? parseJson(value) : value;
158
+ return JSON.stringify(inJsonbKeyOrder(parsed)) ?? null;
159
+ }
160
+ function parseJson(value) {
161
+ try {
162
+ return JSON.parse(value);
163
+ }
164
+ catch {
165
+ return value;
166
+ }
167
+ }
168
+ function inJsonbKeyOrder(value) {
169
+ if (Array.isArray(value))
170
+ return value.map(inJsonbKeyOrder);
171
+ if (value === null || typeof value !== 'object')
172
+ return value;
173
+ return Object.keys(value)
174
+ .sort(compareJsonbKeys)
175
+ .reduce((acc, key) => {
176
+ acc[key] = inJsonbKeyOrder(value[key]);
177
+ return acc;
178
+ }, {});
179
+ }
180
+ /**
181
+ * jsonb's own key collation: shorter keys first, ties broken by comparing the
182
+ * UTF-8 bytes.
183
+ */
184
+ function compareJsonbKeys(a, b) {
185
+ const bytesA = UTF8.encode(a);
186
+ const bytesB = UTF8.encode(b);
187
+ if (bytesA.length !== bytesB.length)
188
+ return bytesA.length - bytesB.length;
189
+ for (let i = 0; i < bytesA.length; i++) {
190
+ if (bytesA[i] !== bytesB[i])
191
+ return bytesA[i] - bytesB[i];
192
+ }
193
+ return 0;
194
+ }
@@ -1,5 +1,23 @@
1
1
  import sortableCacheValuesName from './sortableCacheValuesName.js';
2
+ import { clearSortableSnapshot } from './sortableSnapshot.js';
2
3
  export default function clearCachedSortableValues(dream, positionField) {
3
4
  const cachedValuesName = sortableCacheValuesName(positionField);
4
5
  dream[cachedValuesName] = undefined;
5
6
  }
7
+ /**
8
+ * @internal
9
+ *
10
+ * Discards both halves of what a save's sortable phase stashes on the
11
+ * instance: the cached values the position work reads, and the snapshot of the
12
+ * row it would have computed its shift against. The position work clears them
13
+ * itself as it consumes them, so this is for the paths where it does not run —
14
+ * the row was already gone when the snapshot read found it, or the save's
15
+ * transaction rolled back — leaving a later save of the same instance to read
16
+ * the row for itself rather than inherit this save's reading of it.
17
+ */
18
+ export function clearSortableSaveState(dream, configs) {
19
+ for (const { positionField } of configs) {
20
+ clearCachedSortableValues(dream, positionField);
21
+ clearSortableSnapshot(dream, positionField);
22
+ }
23
+ }
@@ -1,7 +1,7 @@
1
1
  import ops from '../../../../ops/index.js';
2
- import getColumnForSortableScope from './getColumnForSortableScope.js';
3
- import scopeArray from './scopeArray.js';
4
- export default async function decrementPositionForScopedRecordsGreaterThanPosition(position, { dream, positionField, query, scope, }) {
2
+ import filterQueryToScopeValues from './filterQueryToScopeValues.js';
3
+ import { snapshotScopeValue } from './sortableSnapshot.js';
4
+ export default async function decrementPositionForScopedRecordsGreaterThanPosition(position, { dream, positionField, query, scope, snapshot, }) {
5
5
  let kyselyQuery = query
6
6
  .whereNot({ [dream['_primaryKey']]: dream.primaryKeyValue() })
7
7
  .where({
@@ -13,11 +13,11 @@ export default async function decrementPositionForScopedRecordsGreaterThanPositi
13
13
  [positionField]: eb(positionField, '-', 1),
14
14
  };
15
15
  });
16
- for (const singleScope of scopeArray(scope)) {
17
- const column = getColumnForSortableScope(dream, singleScope);
18
- if (column) {
19
- kyselyQuery = kyselyQuery.where(column, '=', dream[column]);
20
- }
21
- }
16
+ // A null scope value has to be matched with `is null`: `column = NULL` is
17
+ // never true, so the compaction would silently shift nothing at all in a
18
+ // null-valued sort scope. `filterQueryToScopeValues` is the one place that
19
+ // branch lives, and it is the same filter every other position-mutating path
20
+ // builds.
21
+ kyselyQuery = filterQueryToScopeValues(dream, kyselyQuery, column => snapshotScopeValue(snapshot, column, () => dream[column]), scope);
22
22
  await kyselyQuery.execute();
23
23
  }
@@ -0,0 +1,20 @@
1
+ import sortableScopeColumns from './sortableScopeColumns.js';
2
+ /**
3
+ * @internal
4
+ *
5
+ * Narrows a Dream `Query` to one sort scope, **reading the scope values off the
6
+ * in-memory instance**. That is only the scope the row physically occupies when
7
+ * the instance is known to agree with the row — inside a transaction that has
8
+ * just written those values, or on a record read under the scope lock.
9
+ *
10
+ * Where the row may have been moved by another writer, the caller wants
11
+ * `filterQueryToScopeValues` instead, which takes each column's value from the
12
+ * caller (the snapshot read under the lock) rather than from the instance.
13
+ */
14
+ export default function filterQueryToInstanceScope(query, dream, scope) {
15
+ for (const column of sortableScopeColumns(dream, scope)) {
16
+ const value = dream[column];
17
+ query = query.where({ [column]: value ?? null });
18
+ }
19
+ return query;
20
+ }
@@ -0,0 +1,32 @@
1
+ import sortableScopeColumns from './sortableScopeColumns.js';
2
+ /**
3
+ * @internal
4
+ *
5
+ * Narrows a kysely query to one sort scope, taking each scope column's value
6
+ * **from the caller** rather than from the in-memory instance — the snapshot
7
+ * read under the scope lock on the paths that compact a scope a record is
8
+ * leaving, which is not necessarily the scope the instance was loaded from.
9
+ *
10
+ * This is also the one place the null branch lives: a null scope value has to
11
+ * be matched with `is null`, since `column = NULL` is never true and the filter
12
+ * would silently match nothing at all in a null-valued sort scope.
13
+ *
14
+ * The instance-reading sibling is `filterQueryToInstanceScope`.
15
+ *
16
+ * @param dream - the record whose class resolves the scope members to columns
17
+ * @param kyselyQuery - the query to narrow
18
+ * @param whereValueCB - the value to match each scope column against
19
+ * @param scope - the sortable field's scope
20
+ */
21
+ export default function filterQueryToScopeValues(dream, kyselyQuery, whereValueCB, scope) {
22
+ for (const column of sortableScopeColumns(dream, scope)) {
23
+ const columnValue = whereValueCB(column);
24
+ if (columnValue === null) {
25
+ kyselyQuery = kyselyQuery.where(column, 'is', null);
26
+ }
27
+ else {
28
+ kyselyQuery = kyselyQuery.where(column, '=', columnValue);
29
+ }
30
+ }
31
+ return kyselyQuery;
32
+ }
@@ -0,0 +1,34 @@
1
+ const heldKeysByTransaction = new WeakMap();
2
+ /**
3
+ * @internal
4
+ *
5
+ * The sortable scope-lock keys this transaction already holds.
6
+ *
7
+ * Advisory transaction locks are released only when the transaction ends, so a
8
+ * key taken once is held for the rest of it: every later acquisition of that
9
+ * key is a statement whose entire effect is to observe that the transaction
10
+ * already has what it is asking for. Tracking them turns those statements into
11
+ * nothing at all — the per-field acquisition inside the position write, and
12
+ * every per-record acquisition inside a locked batch whose preflight already
13
+ * took the batch's whole key set.
14
+ *
15
+ * Being the transaction's whole ledger is also what lets the locked-batch
16
+ * preflight count its bound cumulatively rather than per batch: how many scope
17
+ * locks one more batch would leave the transaction holding is this set's size
18
+ * plus whatever of the batch's keys is not already in it.
19
+ *
20
+ * Keyed by the transaction object rather than by the connection, which is what
21
+ * makes it correct rather than merely fast: a nested transaction gets its own
22
+ * `DreamTransaction` and therefore its own set, so keys taken inside a
23
+ * subtransaction that rolls back — releasing its locks with it — are never
24
+ * remembered as held by the transaction that outlives it. The entry is
25
+ * discarded with the transaction object.
26
+ */
27
+ export default function heldSortableScopeLockKeys(txn) {
28
+ const held = heldKeysByTransaction.get(txn);
29
+ if (held)
30
+ return held;
31
+ const newHeld = new Set();
32
+ heldKeysByTransaction.set(txn, newHeld);
33
+ return newHeld;
34
+ }
@@ -0,0 +1,21 @@
1
+ import { STI_SCOPE_NAME } from '../../../class/STI.js';
2
+ import afterSortableDestroy from '../hooks/afterSortableDestroy.js';
3
+ /**
4
+ * @internal
5
+ *
6
+ * The compaction of a sortable destroy: closes the vacancy each sortable
7
+ * field's sort scope is left with, computed from the snapshot the preparation
8
+ * phase (`prepareSortableFieldsForDestroy`) read before the delete. Called
9
+ * directly from `destroyDream` after the delete and before any user
10
+ * `afterDestroy` hook — never registered as a hook among them — so every
11
+ * after-destroy hook observes the compacted scope regardless of where it was
12
+ * declared, exactly as every after-save hook observes the computed position.
13
+ */
14
+ export default async function performSortableDestroyWork(dream, txn) {
15
+ const dreamClass = dream.constructor;
16
+ const sortableFields = dreamClass['sortableFields'];
17
+ for (const { positionField, scope } of sortableFields) {
18
+ const query = dreamClass.query().removeDefaultScope(STI_SCOPE_NAME).txn(txn);
19
+ await afterSortableDestroy({ dream, positionField, query, scope, txn });
20
+ }
21
+ }
@@ -0,0 +1,36 @@
1
+ import { STI_SCOPE_NAME } from '../../../class/STI.js';
2
+ import afterSortableCreate from '../hooks/afterSortableCreate.js';
3
+ import afterUpdateSortable from '../hooks/afterSortableUpdate.js';
4
+ import acquireSortableScopeLocks from './acquireSortableScopeLocks.js';
5
+ import { sortableCurrentScopeLockKeys } from './sortableScopeLockKeys.js';
6
+ /**
7
+ * @internal
8
+ *
9
+ * The position work of a sortable save: computes each sortable field's real
10
+ * position under the scope lock and writes it. Called directly from
11
+ * `saveDream` after the driver write — never registered as an after-hook — so
12
+ * no user hook code can interleave with it while the scope lock is held, and
13
+ * on a self-opened save it is the last work before COMMIT releases the lock.
14
+ */
15
+ export default async function performSortablePositionWork(dream, txn, sortableFieldConfigs, { alreadyPersisted }) {
16
+ const dreamClass = dream.constructor;
17
+ // A create acquires its whole advisory key set here — every sortable field,
18
+ // one sorted pass — before any field's position is computed, so a
19
+ // multi-field create takes its keys in the same global order as an update or
20
+ // a destroy and cannot deadlock against them. The per-field acquisition
21
+ // inside `setPosition` then re-acquires keys this transaction already holds,
22
+ // which is a no-op. (An update acquired its keys before the driver write, in
23
+ // `saveDream`.)
24
+ if (!alreadyPersisted) {
25
+ await acquireSortableScopeLocks(dream, txn, sortableCurrentScopeLockKeys(dream, sortableFieldConfigs));
26
+ }
27
+ for (const { positionField, scope } of sortableFieldConfigs) {
28
+ const query = dreamClass.query().removeDefaultScope(STI_SCOPE_NAME).txn(txn);
29
+ if (alreadyPersisted) {
30
+ await afterUpdateSortable({ dream, positionField, query, txn, scope });
31
+ }
32
+ else {
33
+ await afterSortableCreate({ dream, positionField, query, txn, scope });
34
+ }
35
+ }
36
+ }