feat(deploy): production deployment setup and fixes

- Debian-based api image (bookworm-slim), docker/debian mirrors, prisma
  binaryTargets for openssl 3.0
- nginx: admin SPA under /admin, TLS via acme.sh (ZeroSSL) + auto-renewal
  cron, http->https redirect
- prisma: add origin_goods.delisted migration, sync missing schema
  (good_image/tag_font_color/good_tags), fix users.createdAt Timestamptz
- api: CORS wildcard reflection, helmet CORP cross-origin, price
  backfill in persistProductDetail, categoryIcon ancestor fallback,
  mediaByColor per-color gallery in public goods detail
- admin: /admin base path (vite + router)
- import-data.mjs: udt_name casting, serial sequence advance fix
This commit is contained in:
yeuimu
2026-08-26 14:23:09 +08:00
parent be0b90e68f
commit 6c61a4e871
982 changed files with 74156 additions and 179393 deletions
@@ -1,149 +0,0 @@
---
title: Unmount Hooks May Not Fire Inside Transitions During Fast Replacement
impact: MEDIUM
impactDescription: Components inside transitions can be destroyed without unmount hooks firing under race conditions
type: gotcha
tags: [vue3, lifecycle, transition, onUnmounted, unmounted, cleanup, race-condition]
---
# Unmount Hooks May Not Fire Inside Transitions During Fast Replacement
**Impact: MEDIUM** - When a component inside a `<transition>` is replaced by another component during the transition's loading phase, the unmount hooks (`onBeforeUnmount`, `onUnmounted`) may not be called even though the component is removed from the DOM. This can cause memory leaks and resource leaks from unclean side effects.
This is a known edge case that occurs when the timing is specific - if a parent component with a child inside a transition is replaced while the child is still mounting. The child's mount hooks fire, but unmount hooks never do.
## Task Checklist
- [ ] Be aware that unmount hooks are not 100% guaranteed inside transitions
- [ ] For critical cleanup, consider alternative cleanup strategies
- [ ] Use `mode="out-in"` on transitions to ensure old component fully unmounts before new mounts
- [ ] For essential resources, consider cleanup at parent component level
- [ ] Test component replacement scenarios during development
**Problematic Scenario:**
```vue
<!-- Parent component with lazy-loaded child in transition -->
<template>
<transition>
<Suspense>
<component :is="currentComponent" />
</Suspense>
</transition>
</template>
```
```javascript
// Child component - unmount hooks may not fire if parent changes quickly
export default {
setup() {
const socket = new WebSocket('wss://example.com')
onMounted(() => {
console.log('Mounted - this will run')
socket.connect()
})
onUnmounted(() => {
// WARNING: This may NOT run if component is inside transition
// and parent navigates away during mounting phase!
console.log('Unmounted - might not run')
socket.close()
})
}
}
```
**Safer Patterns:**
```vue
<!-- SAFER: Use out-in mode to ensure proper sequencing -->
<template>
<transition mode="out-in">
<component :is="currentComponent" :key="currentKey" />
</transition>
</template>
```
```javascript
// SAFER: Cleanup at parent level for critical resources
// Parent component
export default {
setup() {
const childSocket = ref(null)
// Parent controls resource lifecycle
provide('registerSocket', (socket) => {
childSocket.value = socket
})
onUnmounted(() => {
// Parent ensures cleanup even if child unmount hook doesn't fire
childSocket.value?.close()
})
}
}
// Child component
export default {
setup() {
const registerSocket = inject('registerSocket')
const socket = new WebSocket('wss://example.com')
// Register with parent for backup cleanup
registerSocket(socket)
onMounted(() => {
socket.connect()
})
onUnmounted(() => {
socket.close() // Still attempt cleanup here
})
}
}
```
```javascript
// SAFER: Use AbortController pattern for cancellable operations
export default {
setup() {
const abortController = new AbortController()
onMounted(() => {
fetch('/api/data', { signal: abortController.signal })
.then(handleData)
.catch(err => {
if (err.name !== 'AbortError') {
handleError(err)
}
})
})
onUnmounted(() => {
// If this doesn't fire, request continues but response is ignored
// Not a memory leak - just potentially wasted network call
abortController.abort()
})
}
}
```
## Testing for This Issue
```javascript
// Test by rapidly switching components during async loading
async function testUnmountHooks() {
// Mount component A (has async setup)
await mountComponent('A')
// Immediately switch to B before A finishes mounting
await mountComponent('B')
// Check if A's unmount hooks fired
// They may not have!
}
```
## Reference
- [Vue.js GitHub Issue #6260](https://github.com/vuejs/core/issues/6260)
- [Vue.js Transition](https://vuejs.org/guide/built-ins/transition.html)
- [Vue.js Lifecycle Hooks](https://vuejs.org/guide/essentials/lifecycle.html)