etaf/etaf-render-port.el

385 lines
16 KiB
EmacsLisp

;;; etaf-render-port.el --- Versioned Ebox renderer port -*- lexical-binding: t; -*-
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Commentary:
;; This file is ETAF's only Ebox framework-SPI bootstrap owner. It requires
;; one compatible Ebox v2 provider, snapshots it once, and exposes one immutable
;; port to downstream ETAF code without repeated protocol guesses.
;;; Code:
(require 'cl-lib)
(require 'subr-x)
(require 'ebox)
(define-error 'etaf-spi-bootstrap-error
"Malformed Ebox framework SPI provider")
(define-error 'etaf-spi-incompatible-error
"Incompatible Ebox framework SPI provider"
'etaf-spi-bootstrap-error)
(defconst etaf-render-port--required-spi-version 2
"Ebox framework SPI version consumed by this ETAF build.")
(defconst etaf-render-port--required-schema-version
'ebox-framework-spi-schema/v2
"Ebox provider schema consumed by this ETAF build.")
(defconst etaf-render-port--required-capabilities
'(initial-paired-stage-rollback
update-paired-stage-rollback
combined-participant-ordering
same-object-legacy-report
initial-observation-replay)
"Capabilities required from an Ebox framework SPI v2 provider.")
(defconst etaf-render-port--accepted-tp-protocols
'(tp-transaction-protocol-v1+v2 tp-transaction-protocol-v2)
"TP protocols accepted from an Ebox SPI v2 provider.
The dual-capability manifest is accepted during dependency-order migration
because it contains v2; ETAF never dispatches through its v1 capability.")
(defconst etaf-render-port--required-stage-order
'(ebox-mirror/native framework-stage)
"Required stage order inside the combined Ebox participant.")
(defconst etaf-render-port--required-rollback-order
'(framework-rollback ebox-mirror/native tp)
"Required rollback order inside the combined Ebox participant.")
(cl-defstruct
(etaf-render-port
(:constructor etaf-render-port--create)
(:conc-name etaf-render-port--))
"Immutable selected Ebox rendering capability."
(route nil :read-only t)
(spi-version nil :read-only t)
(schema-version nil :read-only t)
(capabilities nil :read-only t)
(tp-protocol nil :read-only t)
(initial-function nil :read-only t)
(update-function nil :read-only t)
(revision-function nil :read-only t)
(bootstrap-outcome nil :read-only t)
(provider nil :read-only t)
(snapshot-function nil :read-only t))
(defun etaf-render-port-route (port)
"Return selected PORT route, always `v2'."
(etaf-render-port--route port))
(defun etaf-render-port-spi-version (port)
"Return PORT's selected framework SPI version."
(etaf-render-port--spi-version port))
(defun etaf-render-port-schema-version (port)
"Return PORT's selected provider schema version."
(etaf-render-port--schema-version port))
(defun etaf-render-port-capabilities (port)
"Return a defensive copy of PORT's capabilities."
(copy-sequence (etaf-render-port--capabilities port)))
(defun etaf-render-port-tp-protocol (port)
"Return PORT's selected TP transaction protocol."
(etaf-render-port--tp-protocol port))
(defun etaf-render-port-initial-function (port)
"Return PORT's initial mount function symbol."
(etaf-render-port--initial-function port))
(defun etaf-render-port-update-function (port)
"Return PORT's update function symbol."
(etaf-render-port--update-function port))
(defun etaf-render-port-revision-function (port)
"Return PORT's current-revision query function symbol."
(etaf-render-port--revision-function port))
(defun etaf-render-port-snapshot-function (port)
"Return PORT's explicit committed-snapshot query function symbol."
(etaf-render-port--snapshot-function port))
(defun etaf-render-port-bootstrap-outcome (port)
"Return PORT's immutable bootstrap outcome tag."
(etaf-render-port--bootstrap-outcome port))
(defun etaf-render-port--bootstrap-error (reason &optional detail)
"Signal a fail-closed bootstrap error for REASON and DETAIL."
(signal 'etaf-spi-bootstrap-error
(list :reason reason :detail detail)))
(defun etaf-render-port--ensure-accessors ()
"Require Ebox's public v2 accessors and explicit snapshot query."
(dolist
(function
'(ebox-framework-spi-provider-p
ebox-framework-spi-provider-spi-version
ebox-framework-spi-provider-schema-version
ebox-framework-spi-provider-capabilities
ebox-framework-spi-provider-tp-protocol
ebox-framework-spi-provider-stage-order
ebox-framework-spi-provider-rollback-order
ebox-framework-spi-provider-report-semantics
ebox-framework-spi-provider-initial-operation
ebox-framework-spi-provider-update-operation
ebox-framework-spi-operation-p
ebox-framework-spi-operation-kind
ebox-framework-spi-operation-function
ebox-framework-spi-operation-argument-schema
ebox-framework-spi-operation-result-schema
ebox-framework-spi-operation-paired-stage-rollback-p
ebox-framework-spi-initial-observation-reports
ebox-surface-buffer-snapshot))
(unless (fboundp function)
(etaf-render-port--bootstrap-error
'missing-provider-accessor function))))
(defun etaf-render-port--operation-snapshot (operation label)
"Return validated immutable field snapshot for OPERATION named LABEL."
(unless (ebox-framework-spi-operation-p operation)
(etaf-render-port--bootstrap-error
'malformed-operation (list :label label :value operation)))
(let ((kind (ebox-framework-spi-operation-kind operation))
(function (ebox-framework-spi-operation-function operation))
(arguments
(ebox-framework-spi-operation-argument-schema operation))
(result (ebox-framework-spi-operation-result-schema operation))
(paired
(ebox-framework-spi-operation-paired-stage-rollback-p operation)))
(unless (and (symbolp kind)
(symbolp function)
(fboundp function)
(proper-list-p arguments)
(cl-every #'symbolp arguments)
(symbolp result)
(memq paired '(nil t)))
(etaf-render-port--bootstrap-error
'malformed-operation-fields
(list :label label :kind kind :function function
:arguments arguments :result result :paired paired)))
(list :kind kind :function function
:arguments (copy-sequence arguments)
:result result :paired paired)))
(defun etaf-render-port--provider-snapshot (provider)
"Return a validated field snapshot of Ebox v2 PROVIDER."
(etaf-render-port--ensure-accessors)
(unless (ebox-framework-spi-provider-p provider)
(etaf-render-port--bootstrap-error 'non-provider-record provider))
(let ((spi-version (ebox-framework-spi-provider-spi-version provider))
(schema-version
(ebox-framework-spi-provider-schema-version provider))
(capabilities
(ebox-framework-spi-provider-capabilities provider))
(tp-protocol (ebox-framework-spi-provider-tp-protocol provider))
(stage-order (ebox-framework-spi-provider-stage-order provider))
(rollback-order
(ebox-framework-spi-provider-rollback-order provider))
(report-semantics
(ebox-framework-spi-provider-report-semantics provider)))
(unless (and (integerp spi-version)
(> spi-version 0)
(symbolp schema-version)
(proper-list-p capabilities)
(cl-every #'symbolp capabilities)
(symbolp tp-protocol)
(proper-list-p stage-order)
(cl-every #'symbolp stage-order)
(proper-list-p rollback-order)
(cl-every #'symbolp rollback-order)
(symbolp report-semantics))
(etaf-render-port--bootstrap-error
'malformed-provider-fields
(list :spi-version spi-version :schema-version schema-version
:capabilities capabilities :tp-protocol tp-protocol
:stage-order stage-order :rollback-order rollback-order
:report-semantics report-semantics)))
(list
:provider provider
:spi-version spi-version
:schema-version schema-version
:capabilities (copy-sequence capabilities)
:tp-protocol tp-protocol
:stage-order (copy-sequence stage-order)
:rollback-order (copy-sequence rollback-order)
:report-semantics report-semantics
:initial
(etaf-render-port--operation-snapshot
(ebox-framework-spi-provider-initial-operation provider) 'initial)
:update
(etaf-render-port--operation-snapshot
(ebox-framework-spi-provider-update-operation provider) 'update))))
(defun etaf-render-port--incompatibilities (snapshot)
"Return deterministic compatibility failures in provider SNAPSHOT."
(let (failures)
(unless (eql (plist-get snapshot :spi-version)
etaf-render-port--required-spi-version)
(push (list :spi-version (plist-get snapshot :spi-version)) failures))
(unless (eq (plist-get snapshot :schema-version)
etaf-render-port--required-schema-version)
(push (list :schema-version (plist-get snapshot :schema-version))
failures))
(let ((capabilities (plist-get snapshot :capabilities)))
(dolist (capability etaf-render-port--required-capabilities)
(unless (memq capability capabilities)
(push (list :missing-capability capability) failures))))
(unless (memq (plist-get snapshot :tp-protocol)
etaf-render-port--accepted-tp-protocols)
(push (list :tp-protocol (plist-get snapshot :tp-protocol)) failures))
(unless (equal (plist-get snapshot :stage-order)
etaf-render-port--required-stage-order)
(push (list :stage-order (plist-get snapshot :stage-order)) failures))
(unless (equal (plist-get snapshot :rollback-order)
etaf-render-port--required-rollback-order)
(push (list :rollback-order (plist-get snapshot :rollback-order))
failures))
(unless (eq (plist-get snapshot :report-semantics)
'same-object-legacy-report)
(push (list :report-semantics
(plist-get snapshot :report-semantics))
failures))
(dolist
(expectation
`((:initial initial ebox-framework-spi-initial
(buffer canonical-input framework-stage framework-rollback))
(:update update ebox-framework-spi-update
(buffer canonical-input-or-candidate
framework-stage framework-rollback))))
(let* ((slot (nth 0 expectation))
(operation (plist-get snapshot slot)))
(unless (and (eq (plist-get operation :kind) (nth 1 expectation))
(eq (plist-get operation :function) (nth 2 expectation))
(equal (plist-get operation :arguments)
(nth 3 expectation))
(eq (plist-get operation :result)
'ebox-legacy-report/same-object)
(eq (plist-get operation :paired) t))
(push (list slot operation) failures))))
(nreverse failures)))
(defun etaf-render-port--v2-port (snapshot)
"Return an immutable selected v2 port from compatible SNAPSHOT."
(let ((failures (etaf-render-port--incompatibilities snapshot)))
(when failures
(signal 'etaf-spi-incompatible-error
(list :incompatibilities failures)))
(etaf-render-port--create
:route 'v2
:spi-version (plist-get snapshot :spi-version)
:schema-version (plist-get snapshot :schema-version)
:capabilities (copy-sequence (plist-get snapshot :capabilities))
:tp-protocol (plist-get snapshot :tp-protocol)
:initial-function
(plist-get (plist-get snapshot :initial) :function)
:update-function
(plist-get (plist-get snapshot :update) :function)
:revision-function 'ebox-surface-buffer-revision
:snapshot-function 'ebox-surface-buffer-snapshot
:bootstrap-outcome 'valid-v2-selected
:provider (plist-get snapshot :provider))))
(defun etaf-render-port--bootstrap ()
"Probe Ebox exactly once and return one immutable selected render port."
(let ((feature-present-p (featurep 'ebox-framework-spi-v2))
(predicate-present-p (fboundp 'ebox-framework-spi-capabilities)))
(cond
((and (not feature-present-p) (not predicate-present-p))
(etaf-render-port--bootstrap-error 'v2-provider-missing))
((not feature-present-p)
(etaf-render-port--bootstrap-error 'predicate-without-v2-feature))
((not predicate-present-p)
(etaf-render-port--bootstrap-error 'v2-feature-without-predicate))
(t
(condition-case condition
(etaf-render-port--v2-port
(etaf-render-port--provider-snapshot
(ebox-framework-spi-capabilities)))
((etaf-spi-incompatible-error etaf-spi-bootstrap-error)
(signal (car condition) (cdr condition)))
((error quit)
(etaf-render-port--bootstrap-error
'provider-predicate-failure condition)))))))
(defconst etaf-render-port--selected-port
(etaf-render-port--bootstrap)
"Process-wide immutable Ebox render port selected during ETAF bootstrap.")
(defun etaf-render-port-selected ()
"Return the process-wide immutable Ebox render port."
etaf-render-port--selected-port)
(defun etaf-render-port-initial
(buffer input framework-stage framework-rollback &optional observer)
"Run selected initial operation for BUFFER and canonical INPUT.
FRAMEWORK-STAGE and FRAMEWORK-ROLLBACK are one required callback pair.
OBSERVER, when non-nil, receives TP and Ebox snapshots measured during the
initial v2 publication and replayed only after successful final accept."
(let ((report
(funcall
(etaf-render-port-initial-function etaf-render-port--selected-port)
buffer input framework-stage framework-rollback)))
(when observer
(dolist (provider-report
(ebox-framework-spi-initial-observation-reports report))
(funcall observer buffer provider-report)))
report))
(defun etaf-render-port-update
(buffer input framework-stage framework-rollback)
"Run selected update operation for BUFFER and INPUT.
FRAMEWORK-STAGE and FRAMEWORK-ROLLBACK are one required callback pair."
(funcall (etaf-render-port-update-function
etaf-render-port--selected-port)
buffer input framework-stage framework-rollback))
(defun etaf-render-port-unmount (buffer)
"Release the retained Ebox surface owned by mounted BUFFER."
(ebox-unmount-buffer (get-buffer buffer)))
(defun etaf-render-port-mounted-p (buffer)
"Return non-nil when BUFFER owns a live retained Ebox surface."
(ebox-surface-buffer-mounted-p buffer))
(defun etaf-render-port-revision (buffer)
"Return BUFFER's current Ebox revision, or zero when it is unmounted.
During an active TP transaction this can be a provisional revision for the
paired publication stage. Public committed queries must exclude that extent."
(let ((buffer (get-buffer buffer)))
(if (not (and (buffer-live-p buffer)
(ebox-surface-buffer-mounted-p buffer)))
0
(let ((revision
(funcall
(etaf-render-port-revision-function
etaf-render-port--selected-port)
buffer)))
(unless (and (integerp revision) (> revision 0))
(error "Mounted Ebox surface has no committed revision: %S"
revision))
revision))))
(defun etaf-render-port-snapshot (buffer)
"Export BUFFER's committed canonical input, revision, and mount identity.
Ebox owns this explicit O(N) detached export and rejects unavailable or
transactional reads. The port validates only its envelope and never copies,
publishes, or queries private renderer state."
(let ((snapshot
(funcall
(etaf-render-port-snapshot-function etaf-render-port--selected-port)
buffer)))
(unless (and (proper-list-p snapshot)
(ebox-canonical-input-p (plist-get snapshot :input))
(integerp (plist-get snapshot :revision))
(> (plist-get snapshot :revision) 0)
(integerp (plist-get snapshot :mount-id)))
(error "Malformed Ebox committed snapshot"))
snapshot))
(provide 'etaf-render-port)
;;; etaf-render-port.el ends here