Lifetimes, ties and timers

Most of what a plugin sets up is undone for it: entries, layers, hook entries, timers, jobs. These forms cover the rest: an action of your own when your run ends (on-end), knowing why it ends (run-ending), things a provider makes for its caller (on-caller-end), and timers.

(on-end body …)core

Does. Runs body when this run ends, for any reason: reload, turn-off, or Tuna’s exit. Inside as-caller, it runs when the caller’s run ends.

Underneath. One more item on the receipt, undone in order, newest first, with (run-ending) set. An end must finish in one step: body cannot pause (no prompt, no waiting). An error in it is reported and the rest of the end goes on.

Errors. If body tries to wait: suspend: a plugin's end cannot wait.

Tuna · journal-store/plugin.ss (part)
(define out (open-file-output-port path (file-options no-fail no-truncate append)))
(on-end (close-port out))
Emacs
(add-hook 'my-mode-unload-hook (lambda () (delete-process my-proc)))
(add-hook 'kill-emacs-hook (lambda () (delete-process my-proc)))

Where Tuna differs. Emacs has separate hooks for unloading a library and for exiting, and nothing runs when a file is loaded again over itself. One on-end covers all three.

(run-ending) → reload, off, shutdown or #fcore

Does. While a run’s end is carried out, says why. reload: a new run of the same plugin follows (its code was reloaded, a plugin it used changed, or a failed start is retried with the old code). off: it is turned off or swapped out. shutdown: Tuna is exiting. #f at other times.

Tuna
(on-end
  (unless (eq? (run-ending) 'reload)
    (display "goodbye\n")))
Emacs
(add-hook 'kill-emacs-hook (lambda () (message "goodbye")))

(on-caller-end (c:proc arg …))core

Who uses it. A provider, inside a contract procedure that makes something for whoever called it. Callers write nothing.

Does. Ties the thing to the caller: when the caller turns off, is swapped away, or Tuna exits, the core makes the call (c:proc arg …). On a reload of the caller, the tie moves to its next run instead, so the thing survives the reload. It reads: when the caller ends, kill this buffer.

Underneath. Syntax over a call. The build checks that the head is a contract procedure’s name. The arguments are evaluated at once and must be plain data. The call is recorded as plain data, the procedure’s name and the argument values, and that record is also the tie’s key, so tying the same call again replaces the earlier tie. Because ties are plain data, they are saved with the session and come back after a crash, and a tie always calls whichever plugin provides the name when it runs, never an old provider’s code. The caller is found as for as-caller.

Errors. on-caller-end: no live caller; at build, on-caller-end: kill-view! is not a contract procedure.

Tuna · buffers/plugin.ss (part)
(define (view name)
  (let ([id (or (find-view name) (make-view! name))])
    (on-caller-end (buffers:kill! id))
    id))
Emacs
;; No equivalent. A package's *Help*-style buffer stays
;; when the package is unloaded; the package would have
;; to kill it from its own unload hook.
(get-buffer-create "*my-status*")

Where Tuna differs. In Emacs, who cleans up a buffer one package made for another is nobody’s job. In Tuna the provider says, per procedure, whose the thing is: buffers:view ties the view to its caller, buffers:create makes the user’s buffer, which stays.

(forget-caller-end (c:proc arg …))core

Does. Drops the tie for that call, wherever it is. A provider calls it when the thing goes by other means (the user killed the view), so no receipt keeps ties to things that no longer exist.

Errors. None; an unknown tie does nothing.

Tuna · buffers/plugin.ss, inside kill!
(when (pmap-contains? views id)
  (set! views (pmap-delete views id))
  (forget-caller-end (buffers:kill! id)))
Emacs
;; No equivalent.
(remove-hook 'kill-emacs-hook #'my-cleanup)

(run-with-timer seconds repeat procedure) → timercore

Does. Runs procedure on the editor thread after seconds, then every repeat seconds if repeat is a number (#f: once). Returns the timer, which makes something new, hence no !.

Underneath. The timer is on the run’s receipt and stops when the run ends. The command loop sleeps until the next timer is due, so an idle editor runs nothing.

Tuna
(define autosave (run-with-timer 30 30 save-all!))
Emacs
(defvar my-autosave (run-with-timer 30 30 #'my-save-all))

Where Tuna differs. An Emacs timer keeps running after its package is unloaded, calling a function that may be gone. A Tuna timer stops with the run that started it.

(run-with-idle-timer seconds repeat procedure) → timercore

Does. Runs procedure once Tuna has had no input for seconds; with repeat true, again after each later idle period of that length. Owned by the run, as above.

Tuna
(run-with-idle-timer 0.5 #t (lambda () (for-each check! (buffers:list))))
Emacs
(run-with-idle-timer 0.5 t #'my-check-all)

(cancel-timer! timer)core

Does. Stops a timer.

Tuna
(cancel-timer! autosave)
Emacs
(cancel-timer my-autosave)