| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "sync" |
| 6 | ) |
| 7 | |
| 8 | // Lifecycle cleanup takes only mu, never operations. Browser RPC may reenter |
| 9 | // App, so operations must never be acquired while App.mu is held. |
| 10 | type fileBrowserPreviewRegistry struct { |
| 11 | mu sync.Mutex |
| 12 | operations sync.Mutex |
| 13 | bindings map[string]fileBrowserPreviewBinding |
| 14 | pending *fileBrowserPreviewOperation |
| 15 | } |
| 16 | |
| 17 | type fileBrowserPreviewOperation struct { |
| 18 | key string |
| 19 | preview fileBrowserPreviewBinding |
| 20 | previousURL string |
| 21 | revoked bool // guarded by registry.mu |
| 22 | } |
| 23 | |
| 24 | func (r *fileBrowserPreviewRegistry) begin(key string, prepared preparedFileBrowserPreview) (*fileBrowserPreviewOperation, fileBrowserPreviewBinding, bool) { |
| 25 | r.mu.Lock() |
| 26 | defer r.mu.Unlock() |
| 27 | binding, reusable := r.bindings[key] |
| 28 | op := &fileBrowserPreviewOperation{key: key, previousURL: binding.URL, |
| 29 | preview: fileBrowserPreviewBinding{BrowserTabID: binding.BrowserTabID, URL: prepared.url, release: prepared.release}} |
| 30 | r.pending = op |
| 31 | return op, binding, reusable |
| 32 | } |
| 33 | |
| 34 | func (r *fileBrowserPreviewRegistry) end(op *fileBrowserPreviewOperation) { |
| 35 | r.mu.Lock() |
| 36 | if r.pending == op { |
| 37 | r.pending = nil |
| 38 | } |
| 39 | r.mu.Unlock() |
| 40 | } |
| 41 | |
| 42 | func (r *fileBrowserPreviewRegistry) retire(key, rawURL string) { |
| 43 | r.mu.Lock() |
| 44 | defer r.mu.Unlock() |
| 45 | if binding, ok := r.bindings[key]; ok && binding.URL == rawURL { |
| 46 | delete(r.bindings, key) |
| 47 | binding.release() |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | // Publish under the same App -> registry order as tab removal. A late host |
| 52 | // reply can neither resurrect a removed binding nor attach to a new session. |
| 53 | func (a *App) publishFileBrowserPreview(tab *WorkspaceTab, generation uint64, key string, op *fileBrowserPreviewOperation, binding fileBrowserPreviewBinding) error { |
| 54 | a.mu.RLock() |
| 55 | defer a.mu.RUnlock() |
| 56 | r := &a.filePreviews |
| 57 | r.mu.Lock() |
| 58 | defer r.mu.Unlock() |
| 59 | if a.tabs[tab.ID] != tab || tab.removed || tab.SessionGeneration != generation || op.revoked { |
| 60 | return errors.New("the session changed before the preview opened") |
| 61 | } |
| 62 | if r.bindings == nil { |
| 63 | r.bindings = make(map[string]fileBrowserPreviewBinding) |
| 64 | } |
| 65 | r.bindings[key] = binding |
| 66 | return nil |
| 67 | } |
| 68 | |
| 69 | // Release handles were captured when this app minted the resource. Revoking |
| 70 | // them is in-memory and cannot reacquire App.mu or wait for a browser request. |
| 71 | func (r *fileBrowserPreviewRegistry) releaseMatching(match func(string, fileBrowserPreviewBinding) bool) { |
| 72 | r.mu.Lock() |
| 73 | defer r.mu.Unlock() |
| 74 | for key, binding := range r.bindings { |
| 75 | if match(key, binding) { |
| 76 | delete(r.bindings, key) |
| 77 | if binding.release != nil { |
| 78 | binding.release() |
| 79 | } |
| 80 | } |
| 81 | } |
| 82 | if op := r.pending; op != nil { |
| 83 | previous := op.preview |
| 84 | previous.URL = op.previousURL |
| 85 | if match(op.key, op.preview) || match(op.key, previous) { |
| 86 | op.revoked = true |
| 87 | op.preview.release() |
| 88 | } |
| 89 | } |
| 90 | } |
| 91 |