@rvoh/dream 2.27.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.
- package/dist/cjs/src/Dream.js +32 -0
- package/dist/cjs/src/db/errors.js +4 -0
- package/dist/cjs/src/decorators/field/sortable/Sortable.js +43 -118
- package/dist/cjs/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.js +38 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.js +183 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js +40 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/canonicalScopeValue.js +194 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/clearCachedSortableValues.js +18 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.js +9 -9
- package/dist/cjs/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.js +20 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/filterQueryToScopeValues.js +32 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.js +34 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/performSortableDestroyWork.js +21 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/performSortablePositionWork.js +36 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js +56 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.js +84 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/resortAllRecords.js +186 -29
- package/dist/cjs/src/decorators/field/sortable/helpers/setPosition.js +90 -46
- package/dist/cjs/src/decorators/field/sortable/helpers/sortableRowCache.js +73 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeColumns.js +42 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeLockKey.js +99 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeLockKeys.js +72 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/sortableSnapshot.js +101 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.js +86 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/validPosition.js +21 -4
- package/dist/cjs/src/decorators/field/sortable/hooks/afterSortableDestroy.js +24 -7
- package/dist/cjs/src/decorators/field/sortable/hooks/afterSortableUpdate.js +10 -0
- package/dist/cjs/src/dream/Query.js +64 -9
- package/dist/cjs/src/dream/QueryDriver/Base.js +75 -2
- package/dist/cjs/src/dream/QueryDriver/Kysely.js +11 -4
- package/dist/cjs/src/dream/QueryDriver/Postgres.js +74 -0
- package/dist/cjs/src/dream/internal/destroyDream.js +60 -6
- package/dist/cjs/src/dream/internal/saveDream.js +109 -7
- package/dist/cjs/src/dream/internal/similarity/SimilarityBuilder.js +19 -4
- package/dist/cjs/src/dream/internal/softDeleteDream.js +13 -1
- package/dist/cjs/src/dream/internal/undestroyDream.js +56 -8
- package/dist/cjs/src/dream-app/index.js +21 -0
- package/dist/cjs/src/errors/SortableBatchRequiresTooManyScopeLocks.js +57 -0
- package/dist/cjs/src/errors/SortableRequiresAdvisoryTransactionLocks.js +26 -0
- package/dist/cjs/src/errors/SortableScopeDidNotStabilize.js +39 -0
- package/dist/cjs/src/errors/SortableScopeLockWaitTimedOut.js +50 -0
- package/dist/cjs/src/package-exports/errors.js +3 -0
- package/dist/esm/src/Dream.js +32 -0
- package/dist/esm/src/db/errors.js +4 -0
- package/dist/esm/src/decorators/field/sortable/Sortable.js +43 -118
- package/dist/esm/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.js +38 -0
- package/dist/esm/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.js +183 -0
- package/dist/esm/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js +40 -0
- package/dist/esm/src/decorators/field/sortable/helpers/canonicalScopeValue.js +194 -0
- package/dist/esm/src/decorators/field/sortable/helpers/clearCachedSortableValues.js +18 -0
- package/dist/esm/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.js +9 -9
- package/dist/esm/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.js +20 -0
- package/dist/esm/src/decorators/field/sortable/helpers/filterQueryToScopeValues.js +32 -0
- package/dist/esm/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.js +34 -0
- package/dist/esm/src/decorators/field/sortable/helpers/performSortableDestroyWork.js +21 -0
- package/dist/esm/src/decorators/field/sortable/helpers/performSortablePositionWork.js +36 -0
- package/dist/esm/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js +56 -0
- package/dist/esm/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.js +84 -0
- package/dist/esm/src/decorators/field/sortable/helpers/resortAllRecords.js +186 -29
- package/dist/esm/src/decorators/field/sortable/helpers/setPosition.js +90 -46
- package/dist/esm/src/decorators/field/sortable/helpers/sortableRowCache.js +73 -0
- package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeColumns.js +42 -0
- package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeLockKey.js +99 -0
- package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeLockKeys.js +72 -0
- package/dist/esm/src/decorators/field/sortable/helpers/sortableSnapshot.js +101 -0
- package/dist/esm/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.js +86 -0
- package/dist/esm/src/decorators/field/sortable/helpers/validPosition.js +21 -4
- package/dist/esm/src/decorators/field/sortable/hooks/afterSortableDestroy.js +24 -7
- package/dist/esm/src/decorators/field/sortable/hooks/afterSortableUpdate.js +10 -0
- package/dist/esm/src/dream/Query.js +64 -9
- package/dist/esm/src/dream/QueryDriver/Base.js +75 -2
- package/dist/esm/src/dream/QueryDriver/Kysely.js +11 -4
- package/dist/esm/src/dream/QueryDriver/Postgres.js +74 -0
- package/dist/esm/src/dream/internal/destroyDream.js +60 -6
- package/dist/esm/src/dream/internal/saveDream.js +109 -7
- package/dist/esm/src/dream/internal/similarity/SimilarityBuilder.js +19 -4
- package/dist/esm/src/dream/internal/softDeleteDream.js +13 -1
- package/dist/esm/src/dream/internal/undestroyDream.js +56 -8
- package/dist/esm/src/dream-app/index.js +21 -0
- package/dist/esm/src/errors/SortableBatchRequiresTooManyScopeLocks.js +57 -0
- package/dist/esm/src/errors/SortableRequiresAdvisoryTransactionLocks.js +26 -0
- package/dist/esm/src/errors/SortableScopeDidNotStabilize.js +39 -0
- package/dist/esm/src/errors/SortableScopeLockWaitTimedOut.js +50 -0
- package/dist/esm/src/package-exports/errors.js +3 -0
- package/dist/types/src/Dream.d.ts +32 -0
- package/dist/types/src/db/errors.d.ts +2 -0
- package/dist/types/src/decorators/field/sortable/Sortable.d.ts +33 -0
- package/dist/types/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.d.ts +25 -0
- package/dist/types/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.d.ts +91 -0
- package/dist/types/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.d.ts +31 -0
- package/dist/types/src/decorators/field/sortable/helpers/canonicalScopeValue.d.ts +85 -0
- package/dist/types/src/decorators/field/sortable/helpers/clearCachedSortableValues.d.ts +13 -0
- package/dist/types/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.d.ts +8 -1
- package/dist/types/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.d.ts +24 -0
- package/dist/types/src/decorators/field/sortable/helpers/filterQueryToScopeValues.d.ts +22 -0
- package/dist/types/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.d.ts +27 -0
- package/dist/types/src/decorators/field/sortable/helpers/performSortableDestroyWork.d.ts +14 -0
- package/dist/types/src/decorators/field/sortable/helpers/performSortablePositionWork.d.ts +15 -0
- package/dist/types/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.d.ts +41 -0
- package/dist/types/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.d.ts +35 -0
- package/dist/types/src/decorators/field/sortable/helpers/resortAllRecords.d.ts +32 -0
- package/dist/types/src/decorators/field/sortable/helpers/setPosition.d.ts +27 -11
- package/dist/types/src/decorators/field/sortable/helpers/sortableRowCache.d.ts +48 -0
- package/dist/types/src/decorators/field/sortable/helpers/sortableScopeColumns.d.ts +30 -0
- package/dist/types/src/decorators/field/sortable/helpers/sortableScopeLockKey.d.ts +44 -0
- package/dist/types/src/decorators/field/sortable/helpers/sortableScopeLockKeys.d.ts +51 -0
- package/dist/types/src/decorators/field/sortable/helpers/sortableSnapshot.d.ts +92 -0
- package/dist/types/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.d.ts +59 -0
- package/dist/types/src/decorators/field/sortable/helpers/validPosition.d.ts +14 -2
- package/dist/types/src/decorators/field/sortable/hooks/afterSortableCreate.d.ts +1 -1
- package/dist/types/src/decorators/field/sortable/hooks/afterSortableDestroy.d.ts +3 -1
- package/dist/types/src/decorators/field/sortable/hooks/afterSortableUpdate.d.ts +1 -1
- package/dist/types/src/dream/Query.d.ts +41 -7
- package/dist/types/src/dream/QueryDriver/Base.d.ts +61 -1
- package/dist/types/src/dream/QueryDriver/Kysely.d.ts +6 -2
- package/dist/types/src/dream/QueryDriver/Postgres.d.ts +40 -0
- package/dist/types/src/dream/internal/softDeleteDream.d.ts +7 -1
- package/dist/types/src/dream-app/index.d.ts +18 -2
- package/dist/types/src/errors/SortableBatchRequiresTooManyScopeLocks.d.ts +37 -0
- package/dist/types/src/errors/SortableRequiresAdvisoryTransactionLocks.d.ts +13 -0
- package/dist/types/src/errors/SortableScopeDidNotStabilize.d.ts +23 -0
- package/dist/types/src/errors/SortableScopeLockWaitTimedOut.d.ts +34 -0
- package/dist/types/src/package-exports/errors.d.ts +3 -0
- package/docs/assets/hierarchy.js +1 -1
- package/docs/assets/navigation.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/db.DreamMigrationHelpers.html +11 -11
- package/docs/classes/db.KyselyQueryDriver.html +88 -41
- package/docs/classes/db.PostgresQueryDriver.html +82 -42
- package/docs/classes/db.QueryDriverBase.html +86 -39
- package/docs/classes/errors.CheckConstraintViolation.html +3 -3
- package/docs/classes/errors.ColumnOverflow.html +3 -3
- package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
- package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
- package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
- package/docs/classes/errors.DecryptionError.html +2 -2
- package/docs/classes/errors.DecryptionParseError.html +2 -2
- package/docs/classes/errors.DecryptionRotationError.html +3 -3
- package/docs/classes/errors.GlobalNameNotSet.html +3 -3
- package/docs/classes/errors.InvalidCalendarDate.html +2 -2
- package/docs/classes/errors.InvalidClockTime.html +2 -2
- package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
- package/docs/classes/errors.InvalidDateTime.html +2 -2
- package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
- package/docs/classes/errors.NonLoadedAssociation.html +3 -3
- package/docs/classes/errors.NotNullViolation.html +3 -3
- package/docs/classes/errors.RecordNotFound.html +3 -3
- package/docs/classes/errors.SortableBatchRequiresTooManyScopeLocks.html +61 -0
- package/docs/classes/errors.SortableScopeDidNotStabilize.html +48 -0
- package/docs/classes/errors.SortableScopeLockWaitTimedOut.html +58 -0
- package/docs/classes/errors.ValidationError.html +3 -3
- package/docs/classes/index.CalendarDate.html +33 -33
- package/docs/classes/index.ClockTime.html +32 -32
- package/docs/classes/index.ClockTimeTz.html +35 -35
- package/docs/classes/index.DateTime.html +86 -86
- package/docs/classes/index.Decorators.html +19 -19
- package/docs/classes/index.Dream.html +151 -124
- package/docs/classes/index.DreamApp.html +11 -10
- package/docs/classes/index.DreamTransaction.html +2 -2
- package/docs/classes/index.Env.html +2 -2
- package/docs/classes/index.Query.html +94 -84
- package/docs/classes/system.CliFileWriter.html +4 -4
- package/docs/classes/system.DreamBin.html +2 -2
- package/docs/classes/system.DreamCLI.html +7 -7
- package/docs/classes/system.DreamImporter.html +2 -2
- package/docs/classes/system.DreamLogos.html +2 -2
- package/docs/classes/system.DreamSerializerBuilder.html +11 -11
- package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
- package/docs/classes/system.PathHelpers.html +3 -3
- package/docs/classes/utils.Encrypt.html +3 -3
- package/docs/classes/utils.Range.html +2 -2
- package/docs/functions/db.closeAllDbConnections.html +1 -1
- package/docs/functions/db.dreamDbConnections.html +1 -1
- package/docs/functions/db.untypedDb.html +1 -1
- package/docs/functions/db.validateColumn.html +1 -1
- package/docs/functions/db.validateTable.html +1 -1
- package/docs/functions/errors.pgErrorType.html +1 -1
- package/docs/functions/index.DreamSerializer.html +1 -1
- package/docs/functions/index.ObjectSerializer.html +1 -1
- package/docs/functions/index.ReplicaSafe.html +1 -1
- package/docs/functions/index.STI.html +1 -1
- package/docs/functions/index.SoftDelete.html +1 -1
- package/docs/functions/utils.camelize.html +1 -1
- package/docs/functions/utils.capitalize.html +1 -1
- package/docs/functions/utils.cloneDeepSafe.html +1 -1
- package/docs/functions/utils.compact.html +1 -1
- package/docs/functions/utils.groupBy.html +1 -1
- package/docs/functions/utils.hyphenize.html +1 -1
- package/docs/functions/utils.intersection.html +1 -1
- package/docs/functions/utils.isEmpty.html +1 -1
- package/docs/functions/utils.normalizeUnicode.html +1 -1
- package/docs/functions/utils.pascalize.html +1 -1
- package/docs/functions/utils.percent.html +1 -1
- package/docs/functions/utils.range.html +1 -1
- package/docs/functions/utils.round.html +1 -1
- package/docs/functions/utils.sanitizeString.html +1 -1
- package/docs/functions/utils.snakeify.html +1 -1
- package/docs/functions/utils.sort.html +2 -2
- package/docs/functions/utils.sortBy.html +1 -1
- package/docs/functions/utils.sortObjectByKey.html +1 -1
- package/docs/functions/utils.sortObjectByValue.html +1 -1
- package/docs/functions/utils.uncapitalize.html +1 -1
- package/docs/functions/utils.uniq.html +1 -1
- package/docs/hierarchy.html +1 -1
- package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
- package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
- package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
- package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
- package/docs/interfaces/types.BelongsToStatement.html +2 -2
- package/docs/interfaces/types.DecoratorContext.html +2 -2
- package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
- package/docs/interfaces/types.DreamAppOpts.html +2 -2
- package/docs/interfaces/types.DreamDbConfig.html +5 -5
- package/docs/interfaces/types.DurationObject.html +2 -2
- package/docs/interfaces/types.EncryptOptions.html +2 -2
- package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
- package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
- package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
- package/docs/modules/errors.html +1 -1
- package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
- package/docs/types/openapi.OpenapiAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiFormats.html +1 -1
- package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
- package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
- package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
- package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
- package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaString.html +1 -1
- package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiTypeField.html +1 -1
- package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
- package/docs/types/types.CalendarDateDurationUnit.html +1 -1
- package/docs/types/types.CalendarDateObject.html +1 -1
- package/docs/types/types.Camelized.html +1 -1
- package/docs/types/types.ClockTimeObject.html +1 -1
- package/docs/types/types.DbConnectionType.html +1 -1
- package/docs/types/types.DbTypes.html +1 -1
- package/docs/types/types.DreamAssociationMetadata.html +1 -1
- package/docs/types/types.DreamAttributes.html +1 -1
- package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
- package/docs/types/types.DreamClassColumn.html +1 -1
- package/docs/types/types.DreamColumn.html +1 -1
- package/docs/types/types.DreamColumnNames.html +1 -1
- package/docs/types/types.DreamLogLevel.html +1 -1
- package/docs/types/types.DreamLogger.html +2 -2
- package/docs/types/types.DreamModelSerializerType.html +1 -1
- package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
- package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
- package/docs/types/types.DreamParamSafeAttributes.html +1 -1
- package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
- package/docs/types/types.DreamSerializable.html +1 -1
- package/docs/types/types.DreamSerializableArray.html +1 -1
- package/docs/types/types.DreamSerializerKey.html +1 -1
- package/docs/types/types.DreamSerializers.html +1 -1
- package/docs/types/types.DreamVirtualColumns.html +1 -1
- package/docs/types/types.DurationUnit.html +1 -1
- package/docs/types/types.EncryptAlgorithm.html +1 -1
- package/docs/types/types.HasManyStatement.html +1 -1
- package/docs/types/types.HasOneStatement.html +1 -1
- package/docs/types/types.Hyphenized.html +1 -1
- package/docs/types/types.Pascalized.html +1 -1
- package/docs/types/types.PrimaryKeyType.html +1 -1
- package/docs/types/types.RoundingPrecision.html +1 -1
- package/docs/types/types.SerializerCasing.html +1 -1
- package/docs/types/types.SimpleObjectSerializerType.html +1 -1
- package/docs/types/types.Snakeified.html +1 -1
- package/docs/types/types.StrictInterface.html +1 -1
- package/docs/types/types.UpdateableAssociationProperties.html +1 -1
- package/docs/types/types.UpdateableProperties.html +1 -1
- package/docs/types/types.ValidationType.html +1 -1
- package/docs/types/types.ViewModel.html +2 -2
- package/docs/types/types.ViewModelClass.html +1 -1
- package/docs/types/types.WeekdayName.html +1 -1
- package/docs/types/types.WhereStatementForDream.html +1 -1
- package/docs/types/types.WhereStatementForDreamClass.html +1 -1
- package/docs/variables/index.DreamConst.html +1 -1
- package/docs/variables/index.ops.html +1 -1
- package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
- package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
- package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
- package/docs/variables/system.primaryKeyTypes.html +1 -1
- package/package.json +1 -1
- package/dist/cjs/src/decorators/field/sortable/helpers/applySortableScopeToQuery.js +0 -13
- package/dist/cjs/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.js +0 -7
- package/dist/cjs/src/decorators/field/sortable/hooks/beforeSortableSave.js +0 -41
- package/dist/esm/src/decorators/field/sortable/helpers/applySortableScopeToQuery.js +0 -13
- package/dist/esm/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.js +0 -7
- package/dist/esm/src/decorators/field/sortable/hooks/beforeSortableSave.js +0 -41
- package/dist/types/src/decorators/field/sortable/helpers/applySortableScopeToQuery.d.ts +0 -12
- package/dist/types/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.d.ts +0 -12
- package/dist/types/src/decorators/field/sortable/hooks/beforeSortableSave.d.ts +0 -12
package/dist/cjs/src/Dream.js
CHANGED
|
@@ -1803,6 +1803,25 @@ export default class Dream {
|
|
|
1803
1803
|
* with no gaps, accounting for the scopes specified in the
|
|
1804
1804
|
* corresponding Sortable decorator.
|
|
1805
1805
|
*
|
|
1806
|
+
* Each sort scope is renumbered in its own transaction, under the same
|
|
1807
|
+
* advisory lock every other position-mutating path takes on that scope, from
|
|
1808
|
+
* a read of the scope taken inside that lock — so a concurrent create or
|
|
1809
|
+
* destroy cannot make the renumbering land on positions it never read.
|
|
1810
|
+
*
|
|
1811
|
+
* **Do not call `resort` from inside a transaction you own.** It opens those
|
|
1812
|
+
* transactions itself, on connections of its own, so it can neither join
|
|
1813
|
+
* yours nor roll back with it: its writes commit on their own. Worse, if your
|
|
1814
|
+
* transaction already holds the scope lock for a scope being renumbered — any
|
|
1815
|
+
* position-mutating write to that scope earlier in the transaction takes it —
|
|
1816
|
+
* `resort` waits on that lock from the outside, where no deadlock detector
|
|
1817
|
+
* can see the cycle, until the wait bound expires and it throws
|
|
1818
|
+
* `SortableScopeLockWaitTimedOut`.
|
|
1819
|
+
*
|
|
1820
|
+
* On an STI hierarchy the position space is shared by every class in the
|
|
1821
|
+
* hierarchy, exactly as it is for creates, updates and destroys. Calling
|
|
1822
|
+
* `resort` on a child class therefore renumbers the hierarchy's rows, not
|
|
1823
|
+
* only the receiver's.
|
|
1824
|
+
*
|
|
1806
1825
|
* ```ts
|
|
1807
1826
|
* class Post extends ApplicationModel {
|
|
1808
1827
|
* @deco.Sortable({ scope: ['user']})
|
|
@@ -3742,6 +3761,19 @@ export default class Dream {
|
|
|
3742
3761
|
* This is usually used for a soft-delete
|
|
3743
3762
|
* pattern
|
|
3744
3763
|
*
|
|
3764
|
+
* The veto is fail-closed, and the record is left
|
|
3765
|
+
* entirely untouched: on a soft-delete model the soft
|
|
3766
|
+
* delete is skipped as well, no `afterDestroy` or
|
|
3767
|
+
* `afterDestroyCommit` hooks run, since the record is
|
|
3768
|
+
* still present, and the destroy returns before the
|
|
3769
|
+
* `dependent: 'destroy'` cascade, so this record's
|
|
3770
|
+
* dependent associations survive too.
|
|
3771
|
+
*
|
|
3772
|
+
* A veto only stops the subtree rooted at the record
|
|
3773
|
+
* that called it. When a record is reached by another
|
|
3774
|
+
* record's cascade, vetoing spares it and its own
|
|
3775
|
+
* dependents; the destroy that reached it carries on.
|
|
3776
|
+
*
|
|
3745
3777
|
* ```ts
|
|
3746
3778
|
* class User extends ApplicationModel {
|
|
3747
3779
|
* @deco.BeforeDestroy()
|
|
@@ -12,6 +12,7 @@ export const FOREIGN_KEY_VIOLATION = 'FOREIGN_KEY_VIOLATION';
|
|
|
12
12
|
export const INTEGRITY_CONSTRAINT_VIOLATION = 'INTEGRITY_CONSTRAINT_VIOLATION';
|
|
13
13
|
export const INVALID_INPUT_SYNTAX = 'INVALID_INPUT_SYNTAX';
|
|
14
14
|
export const COLUMN_OVERFLOW = 'COLUMN_OVERFLOW';
|
|
15
|
+
export const LOCK_NOT_AVAILABLE = 'LOCK_NOT_AVAILABLE';
|
|
15
16
|
export const NOT_NULL_VIOLATION = 'NOT_NULL_VIOLATION';
|
|
16
17
|
export const RESTRICT_VIOLATION = 'RESTRICT_VIOLATION';
|
|
17
18
|
export const UNIQUE_VIOLATION = 'UNIQUE_VIOLATION';
|
|
@@ -26,6 +27,9 @@ export const PG_ERRORS = {
|
|
|
26
27
|
'23000': INTEGRITY_CONSTRAINT_VIOLATION,
|
|
27
28
|
'23001': RESTRICT_VIOLATION,
|
|
28
29
|
'23503': FOREIGN_KEY_VIOLATION,
|
|
30
|
+
// a statement Postgres cancelled because its `lock_timeout` elapsed while it
|
|
31
|
+
// waited for a lock — the bound Dream puts on the sortable scope-lock wait
|
|
32
|
+
'55P03': LOCK_NOT_AVAILABLE,
|
|
29
33
|
};
|
|
30
34
|
function pgErrorFromCode(code) {
|
|
31
35
|
return PG_ERRORS[code] || null;
|
|
@@ -1,17 +1,37 @@
|
|
|
1
|
-
import pascalize from '../../../helpers/pascalize.js';
|
|
2
|
-
import { STI_SCOPE_NAME } from '../../class/STI.js';
|
|
3
|
-
import { afterCreateImplementation } from '../lifecycle/AfterCreate.js';
|
|
4
|
-
import { afterCreateCommitImplementation } from '../lifecycle/AfterCreateCommit.js';
|
|
5
|
-
import { afterDestroyImplementation } from '../lifecycle/AfterDestroy.js';
|
|
6
|
-
import { afterDestroyCommitImplementation } from '../lifecycle/AfterDestroyCommit.js';
|
|
7
|
-
import { afterUpdateImplementation } from '../lifecycle/AfterUpdate.js';
|
|
8
|
-
import { afterUpdateCommitImplementation } from '../lifecycle/AfterUpdateCommit.js';
|
|
9
|
-
import { beforeSaveImplementation } from '../lifecycle/BeforeSave.js';
|
|
10
1
|
import scopeArray from './helpers/scopeArray.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Marks an integer column as a sortable position: Dream keeps the positions of
|
|
4
|
+
* every record in a sort scope contiguous, starting at 1, as records are
|
|
5
|
+
* created, moved, destroyed and undestroyed.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* class Post extends ApplicationModel {
|
|
9
|
+
* @deco.Sortable({ scope: 'user' })
|
|
10
|
+
* public position: number
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* await post.update({ position: 2 }) // the records at 2 and above shift up
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* A position past the end of the scope is clamped to the end, and a position
|
|
17
|
+
* below 1 — or none at all — lands the record at the end.
|
|
18
|
+
*
|
|
19
|
+
* **A save that changes the sort scope ignores a position given alongside it.**
|
|
20
|
+
* The record lands at the end of the scope it moves into, whatever position the
|
|
21
|
+
* same `update` supplied:
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* await post.update({ user: otherUser, position: 1 })
|
|
25
|
+
* // post is now the last record in otherUser's scope, not the first
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* Move it in two saves to place it: `await post.update({ user: otherUser })`,
|
|
29
|
+
* then `await post.update({ position: 1 })`.
|
|
30
|
+
*
|
|
31
|
+
* Sortable requires a query driver that supports advisory transaction locks —
|
|
32
|
+
* the `PostgresQueryDriver` does — since every position write serializes the
|
|
33
|
+
* writers of its sort scope on one.
|
|
34
|
+
*/
|
|
15
35
|
export default function Sortable(opts = {}) {
|
|
16
36
|
return function (_, context) {
|
|
17
37
|
const key = context.name;
|
|
@@ -27,116 +47,21 @@ export default function Sortable(opts = {}) {
|
|
|
27
47
|
// already declared on the base STI class.
|
|
28
48
|
dreamClass['sortableFields'] = [...dreamClass['sortableFields']];
|
|
29
49
|
}
|
|
50
|
+
// the decorator registers nothing but this metadata: none of a sortable
|
|
51
|
+
// field's runtime work runs as hooks. A save's preparation and position
|
|
52
|
+
// write run as phases in `saveDream` (`prepareSortableFieldsForSave`,
|
|
53
|
+
// `performSortablePositionWork`), a destroy's lock acquisition, snapshot
|
|
54
|
+
// read and compaction as phases in `destroyDream`
|
|
55
|
+
// (`prepareSortableFieldsForDestroy`, `performSortableDestroyWork`), and
|
|
56
|
+
// an undestroy's restore inline in `undestroyDream` — each seated
|
|
57
|
+
// relative to the user's hooks by the caller, so no user hook code can
|
|
58
|
+
// interleave with the position work and every after-hook observes
|
|
59
|
+
// computed positions and compacted scopes
|
|
30
60
|
;
|
|
31
61
|
dreamClass['sortableFields'].push({
|
|
32
62
|
scope: scopeArray(opts.scope),
|
|
33
63
|
positionField: key,
|
|
34
64
|
});
|
|
35
|
-
const positionField = key;
|
|
36
|
-
const beforeSaveMethodName = `_cacheValueFor${pascalize(key)}`;
|
|
37
|
-
const afterCreateMethodName = `_setNewValueFor${pascalize(key)}AfterCreate`;
|
|
38
|
-
const afterCreateCommitMethodName = `_setNewValueFor${pascalize(key)}AfterCreateCommit`;
|
|
39
|
-
const afterUpdateMethodName = `_updateValueFor${pascalize(key)}AfterUpdate`;
|
|
40
|
-
const afterUpdateCommitMethodName = `_updateValueFor${pascalize(key)}AfteUpdateCommit`;
|
|
41
|
-
const afterDestroyMethodName = `_setValuesAfterDestructionFor${pascalize(key)}AfterDestroy`;
|
|
42
|
-
const afterDestroyCommitMethodName = `_setValuesAfterDestructionFor${pascalize(key)}AfterDestroyCommit`;
|
|
43
|
-
const dreamPrototype = Object.getPrototypeOf(dream);
|
|
44
|
-
// before saving, we remember the new value for position, but clear it from our
|
|
45
|
-
// supervised attributes to prevent position from saving
|
|
46
|
-
dreamPrototype[beforeSaveMethodName] = function () {
|
|
47
|
-
beforeSortableSave({
|
|
48
|
-
dream: this,
|
|
49
|
-
positionField,
|
|
50
|
-
scope: opts.scope,
|
|
51
|
-
});
|
|
52
|
-
};
|
|
53
|
-
// once saved, we can now safely update position in isolation
|
|
54
|
-
dreamPrototype[afterUpdateMethodName] = async function (txn) {
|
|
55
|
-
// if no transaction is provided, leverage update commit hook instead
|
|
56
|
-
if (!txn)
|
|
57
|
-
return;
|
|
58
|
-
const query = dreamClass.query().removeDefaultScope(STI_SCOPE_NAME).txn(txn);
|
|
59
|
-
await afterUpdateSortable({
|
|
60
|
-
dream: this,
|
|
61
|
-
positionField,
|
|
62
|
-
query,
|
|
63
|
-
txn,
|
|
64
|
-
scope: opts.scope,
|
|
65
|
-
});
|
|
66
|
-
};
|
|
67
|
-
dreamPrototype[afterUpdateCommitMethodName] = async function (txn) {
|
|
68
|
-
// if transaction is provided, leverage update hook instead
|
|
69
|
-
if (txn)
|
|
70
|
-
return;
|
|
71
|
-
const query = dreamClass.query().removeDefaultScope(STI_SCOPE_NAME);
|
|
72
|
-
await afterUpdateSortable({
|
|
73
|
-
dream: this,
|
|
74
|
-
positionField,
|
|
75
|
-
query,
|
|
76
|
-
scope: opts.scope,
|
|
77
|
-
});
|
|
78
|
-
};
|
|
79
|
-
// after create, we always want to ensure the position is set, so if they provide one,
|
|
80
|
-
// we need to split existing records on position and update, but otherwise we simply set the new position
|
|
81
|
-
// to be the length of all existing records + 1
|
|
82
|
-
dreamPrototype[afterCreateMethodName] = async function (txn) {
|
|
83
|
-
// if no transaction is provided, leverage create commit hook instead
|
|
84
|
-
if (!txn)
|
|
85
|
-
return;
|
|
86
|
-
const query = dreamClass.query().removeDefaultScope(STI_SCOPE_NAME).txn(txn);
|
|
87
|
-
await afterSortableCreate({
|
|
88
|
-
dream: this,
|
|
89
|
-
positionField,
|
|
90
|
-
query,
|
|
91
|
-
scope: opts.scope,
|
|
92
|
-
txn,
|
|
93
|
-
});
|
|
94
|
-
};
|
|
95
|
-
dreamPrototype[afterCreateCommitMethodName] = async function (txn) {
|
|
96
|
-
// if transaction is provided, leverage create hook instead
|
|
97
|
-
if (txn)
|
|
98
|
-
return;
|
|
99
|
-
const query = dreamClass.query().removeDefaultScope(STI_SCOPE_NAME);
|
|
100
|
-
await afterSortableCreate({
|
|
101
|
-
dream: this,
|
|
102
|
-
positionField,
|
|
103
|
-
query,
|
|
104
|
-
scope: opts.scope,
|
|
105
|
-
});
|
|
106
|
-
};
|
|
107
|
-
// after destroy, auto-adjust positions of all related records with a greater position
|
|
108
|
-
// than this one to maintain incrementing order,
|
|
109
|
-
dreamPrototype[afterDestroyMethodName] = async function (txn) {
|
|
110
|
-
// if no transaction is provided, leverage destroy commit hook instead
|
|
111
|
-
if (!txn)
|
|
112
|
-
return;
|
|
113
|
-
const query = dreamClass.query().removeDefaultScope(STI_SCOPE_NAME).txn(txn);
|
|
114
|
-
await afterSortableDestroy({
|
|
115
|
-
dream: this,
|
|
116
|
-
positionField,
|
|
117
|
-
query,
|
|
118
|
-
scope: opts.scope,
|
|
119
|
-
});
|
|
120
|
-
};
|
|
121
|
-
dreamPrototype[afterDestroyCommitMethodName] = async function (txn) {
|
|
122
|
-
// if transaction is provided, leverage destroy hook instead
|
|
123
|
-
if (txn)
|
|
124
|
-
return;
|
|
125
|
-
const query = dreamClass.query().removeDefaultScope(STI_SCOPE_NAME);
|
|
126
|
-
await afterSortableDestroy({
|
|
127
|
-
dream: this,
|
|
128
|
-
positionField,
|
|
129
|
-
query,
|
|
130
|
-
scope: opts.scope,
|
|
131
|
-
});
|
|
132
|
-
};
|
|
133
|
-
beforeSaveImplementation(dream, beforeSaveMethodName);
|
|
134
|
-
afterCreateImplementation(dream, afterCreateMethodName);
|
|
135
|
-
afterCreateCommitImplementation(dream, afterCreateCommitMethodName);
|
|
136
|
-
afterUpdateImplementation(dream, afterUpdateMethodName);
|
|
137
|
-
afterUpdateCommitImplementation(dream, afterUpdateCommitMethodName);
|
|
138
|
-
afterDestroyImplementation(dream, afterDestroyMethodName);
|
|
139
|
-
afterDestroyCommitImplementation(dream, afterDestroyCommitMethodName);
|
|
140
65
|
});
|
|
141
66
|
};
|
|
142
67
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import Query from '../../../../dream/Query.js';
|
|
2
|
+
import SortableRequiresAdvisoryTransactionLocks from '../../../../errors/SortableRequiresAdvisoryTransactionLocks.js';
|
|
3
|
+
import heldSortableScopeLockKeys from './heldSortableScopeLockKeys.js';
|
|
4
|
+
/**
|
|
5
|
+
* @internal
|
|
6
|
+
*
|
|
7
|
+
* Serializes every writer of a set of sort scopes by taking those scopes'
|
|
8
|
+
* transaction-scoped advisory locks. They are released when the enclosing
|
|
9
|
+
* transaction ends, so there is nothing to release here.
|
|
10
|
+
*
|
|
11
|
+
* Keys this transaction already holds are dropped rather than re-acquired: the
|
|
12
|
+
* lock is held to the end of the transaction either way, so the statement would
|
|
13
|
+
* do nothing but cost a round trip inside the lock window. Whatever is left is
|
|
14
|
+
* taken in the order given — callers that need more than one sort them first,
|
|
15
|
+
* so that two operations touching the same pair of scopes cannot deadlock
|
|
16
|
+
* against each other — and in one statement, on drivers that can express it.
|
|
17
|
+
*
|
|
18
|
+
* Waiting for a key another transaction holds is bounded by the driver, which
|
|
19
|
+
* raises `SortableScopeLockWaitTimedOut` rather than waiting forever; see
|
|
20
|
+
* `QueryDriverBase.acquireAdvisoryTransactionLocks`.
|
|
21
|
+
*
|
|
22
|
+
* @param dream - the record whose connection provides the driver
|
|
23
|
+
* @param txn - the transaction the locks are scoped to
|
|
24
|
+
* @param keys - the scope keys to acquire, in acquisition order
|
|
25
|
+
*/
|
|
26
|
+
export default async function acquireSortableScopeLocks(dream, txn, keys) {
|
|
27
|
+
if (!keys.length)
|
|
28
|
+
return;
|
|
29
|
+
const queryDriverClass = Query.dbDriverClass(dream.connectionName || 'default');
|
|
30
|
+
if (!queryDriverClass.supportsAdvisoryTransactionLocks)
|
|
31
|
+
throw new SortableRequiresAdvisoryTransactionLocks(queryDriverClass.name);
|
|
32
|
+
const heldKeys = heldSortableScopeLockKeys(txn);
|
|
33
|
+
const keysToAcquire = keys.filter(key => !heldKeys.has(key));
|
|
34
|
+
if (!keysToAcquire.length)
|
|
35
|
+
return;
|
|
36
|
+
await queryDriverClass.acquireAdvisoryTransactionLocks(txn, keysToAcquire);
|
|
37
|
+
keysToAcquire.forEach(key => heldKeys.add(key));
|
|
38
|
+
}
|
|
@@ -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
|
+
}
|