195 lines
6.9 KiB
EmacsLisp
195 lines
6.9 KiB
EmacsLisp
;;; etaf-resource.el --- Resource state and error boundary -*- lexical-binding: t; -*-
|
|
|
|
;; SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
;;; Commentary:
|
|
|
|
;; Resources are small Scope-owned data loaders. A synchronous loader updates
|
|
;; one reactive state cell to loading, success, or error. Loader failures are
|
|
;; represented in that state; unrelated errors still propagate through their
|
|
;; ordinary boundary unless explicitly wrapped with `etaf-error-boundary-run'.
|
|
|
|
;;; Code:
|
|
|
|
(require 'cl-lib)
|
|
(require 'etaf-observer)
|
|
(require 'etaf-reactive)
|
|
|
|
(define-error 'etaf-resource-error "Invalid ETAF Resource operation"
|
|
'etaf-reactive-error)
|
|
|
|
(cl-defstruct (etaf-resource
|
|
(:constructor etaf--resource-create))
|
|
"A Scope-owned synchronous resource."
|
|
loader
|
|
state
|
|
scope
|
|
cleanup
|
|
name
|
|
(active-p t))
|
|
|
|
(cl-defstruct (etaf-resource-result
|
|
(:constructor etaf--resource-result-create))
|
|
"A Resource loader result with VALUE and optional CLEANUP."
|
|
value
|
|
cleanup)
|
|
|
|
(defun etaf--resource-state (status value error)
|
|
"Return a Resource state plist for STATUS, VALUE, and ERROR."
|
|
(list :status status :value value :error error))
|
|
|
|
(defun etaf--resource-parent-scope (scope)
|
|
"Return explicit SCOPE or the current active Scope."
|
|
(or scope (etaf-current-effect-scope)))
|
|
|
|
(defun etaf--resource-child-scope (scope name)
|
|
"Create a Resource Scope named NAME under SCOPE when present, otherwise detached."
|
|
(if scope
|
|
(etaf-scope-run scope
|
|
(lambda () (etaf-effect-scope :name name)))
|
|
(etaf-effect-scope :detached t :name name)))
|
|
|
|
(defun etaf--resource-require-active (resource)
|
|
"Return active RESOURCE or signal a Resource error."
|
|
(unless (and (etaf-resource-p resource)
|
|
(etaf-resource-active-p resource))
|
|
(signal 'etaf-resource-error
|
|
(list "ETAF Resource is not active")))
|
|
resource)
|
|
|
|
(defun etaf--resource-set-state (resource status value error)
|
|
"Set RESOURCE state to STATUS, VALUE, and ERROR."
|
|
(setf (etaf-value (etaf-resource-state resource))
|
|
(etaf--resource-state status value error)))
|
|
|
|
(defun etaf--resource-run-cleanup (resource)
|
|
"Run RESOURCE's current cleanup once."
|
|
(when-let* ((cleanup (etaf-resource-cleanup resource)))
|
|
(setf (etaf-resource-cleanup resource) nil)
|
|
(funcall cleanup)))
|
|
|
|
(defun etaf--resource-normalize-result (result)
|
|
"Return (VALUE CLEANUP) from loader RESULT."
|
|
(if (etaf-resource-result-p result)
|
|
(list (etaf-resource-result-value result)
|
|
(etaf-resource-result-cleanup result))
|
|
(list result nil)))
|
|
|
|
;;;###autoload
|
|
(cl-defun etaf-resource-result (value &key cleanup)
|
|
"Return a Resource loader result containing VALUE and optional CLEANUP.
|
|
|
|
CLEANUP must be nil or a zero-argument function. It runs before the next
|
|
successful load replaces the resource value, and when the Resource is
|
|
disposed."
|
|
(when (and cleanup (not (functionp cleanup)))
|
|
(signal 'wrong-type-argument (list 'functionp cleanup)))
|
|
(etaf--resource-result-create :value value :cleanup cleanup))
|
|
|
|
;;;###autoload
|
|
(cl-defun etaf-resource (loader &key (immediate t) scope name)
|
|
"Create a Resource backed by synchronous LOADER.
|
|
|
|
The Resource owns a child Scope under SCOPE, or under the current active Scope
|
|
when SCOPE is nil. Without any parent Scope it creates a detached Scope.
|
|
When IMMEDIATE is non-nil, load the Resource before returning it."
|
|
(unless (functionp loader)
|
|
(signal 'wrong-type-argument (list 'functionp loader)))
|
|
(let* ((parent (etaf--resource-parent-scope scope))
|
|
(owned-scope (etaf--resource-child-scope parent name))
|
|
resource)
|
|
(setq resource
|
|
(etaf--resource-create
|
|
:loader loader
|
|
:state (etaf-ref (etaf--resource-state 'loading nil nil)
|
|
:name name)
|
|
:scope owned-scope
|
|
:name name))
|
|
(etaf-scope-run
|
|
owned-scope
|
|
(lambda ()
|
|
(etaf-on-scope-dispose
|
|
(lambda ()
|
|
(setf (etaf-resource-active-p resource) nil)
|
|
(etaf--resource-run-cleanup resource)))))
|
|
(when immediate
|
|
(etaf-resource-load resource))
|
|
resource))
|
|
|
|
;;;###autoload
|
|
(defun etaf-resource-status (resource)
|
|
"Return RESOURCE status: `loading', `success', or `error'."
|
|
(plist-get (etaf-value (etaf-resource-state resource)) :status))
|
|
|
|
;;;###autoload
|
|
(defun etaf-resource-value (resource)
|
|
"Return RESOURCE's successful value, or nil before success."
|
|
(plist-get (etaf-value (etaf-resource-state resource)) :value))
|
|
|
|
;;;###autoload
|
|
(defun etaf-resource-error (resource)
|
|
"Return RESOURCE's captured loader error, or nil when not in error."
|
|
(plist-get (etaf-value (etaf-resource-state resource)) :error))
|
|
|
|
;;;###autoload
|
|
(defun etaf-resource-load (resource)
|
|
"Reload RESOURCE and return it.
|
|
|
|
Only LOADER errors are captured into Resource state. Cleanup failures and
|
|
wrong Resource usage continue to signal normally."
|
|
(etaf--resource-require-active resource)
|
|
(etaf--resource-run-cleanup resource)
|
|
(etaf--resource-set-state resource 'loading nil nil)
|
|
(let (result error-data)
|
|
(condition-case err
|
|
(setq result
|
|
(etaf-observer-with-stage ('resource 'load)
|
|
(etaf-scope-run
|
|
(etaf-resource-scope resource)
|
|
(etaf-resource-loader resource))))
|
|
(error (setq error-data err)))
|
|
(if error-data
|
|
(etaf--resource-set-state resource 'error nil error-data)
|
|
(pcase-let ((`(,value ,cleanup)
|
|
(etaf--resource-normalize-result result)))
|
|
(when (and cleanup (not (functionp cleanup)))
|
|
(signal 'wrong-type-argument (list 'functionp cleanup)))
|
|
(setf (etaf-resource-cleanup resource) cleanup)
|
|
(etaf--resource-set-state resource 'success value nil))))
|
|
resource)
|
|
|
|
;;;###autoload
|
|
(defun etaf-resource-dispose (resource)
|
|
"Dispose RESOURCE and return cleanup errors collected by its Scope."
|
|
(unless (etaf-resource-p resource)
|
|
(signal 'wrong-type-argument (list 'etaf-resource-p resource)))
|
|
(when (etaf-resource-active-p resource)
|
|
(setf (etaf-resource-active-p resource) nil)
|
|
(etaf-scope-stop (etaf-resource-scope resource))))
|
|
|
|
;;;###autoload
|
|
(defalias 'etaf-resource-cancel #'etaf-resource-dispose
|
|
"Cancel RESOURCE and return cleanup errors collected by its Scope.")
|
|
|
|
;;;###autoload
|
|
(cl-defun etaf-error-boundary-run (function handler &key name)
|
|
"Run FUNCTION and handle its signaled error with HANDLER.
|
|
|
|
HANDLER receives the raw condition object and its return value becomes the
|
|
boundary result. NAME is accepted for caller diagnostics and does not change
|
|
control flow. Errors signaled outside FUNCTION, or by HANDLER itself, are not
|
|
swallowed by this boundary."
|
|
(ignore name)
|
|
(unless (functionp function)
|
|
(signal 'wrong-type-argument (list 'functionp function)))
|
|
(unless (functionp handler)
|
|
(signal 'wrong-type-argument (list 'functionp handler)))
|
|
(condition-case err
|
|
(funcall function)
|
|
(error
|
|
(funcall handler err))))
|
|
|
|
(provide 'etaf-resource)
|
|
|
|
;;; etaf-resource.el ends here
|