| Title: | Session Management for 'blockr' |
|---|---|
| Description: | Persist, restore and manage 'blockr' boards from within a running app. Provides a 'manage_project' plugin for 'blockr.core' that replaces the built-in file upload and download interface with storage backed by the 'pins' package. On 'Posit Connect' each visitor reads and writes pins under their own account, with board sharing, visibility controls and version history. |
| Authors: | Nicolas Bennett [aut, cre], Christoph Sax [aut], Bristol Myers Squibb [fnd] |
| Maintainer: | Nicolas Bennett <[email protected]> |
| License: | GPL (>= 3) |
| Version: | 0.1.1 |
| Built: | 2026-10-01 13:46:43 UTC |
| Source: | https://github.com/bristolmyerssquibb/blockr.session |
Enhanced session management with navbar-compatible UI. Provides a
blockr.core::preserve_board() plugin with full navbar layout including
workflows, version history, and editable title.
manage_project(server = manage_project_server, ui = manage_project_ui) manage_project_server(id, board, ...) manage_project_ui(id, x)manage_project(server = manage_project_server, ui = manage_project_ui) manage_project_server(id, board, ...) manage_project_ui(id, x)
server, ui
|
Server/UI for the plugin module |
id |
Namespace ID |
board |
Reactive values object containing board state |
... |
Extra arguments (may include dock object) |
x |
Board object |
See blockr.core::preserve_board().
Set the session_autosave blockr.core::blockr_option() to an interval in
seconds to have unsaved work parked for recovery. On each tick the board is
serialized and, when it differs from what was last parked, written to the
configured backend as a draft (see rack_create()): an ordinary record at
a reserved id, which overwrites in place rather than keeping a history and
is held out of the workflow listing. An idle board costs one local
serialization per tick and no backend traffic at all.
Recovery is what a draft is for, so nothing is published by it: a save
remains the only way work reaches a record other people can load. A session
holding a loaded record parks under that record, one slot reused; a board
with no record yet parks under an opaque per-session id. Saving clears the
latter. A later session lists what is left and offers each entry for restore
or discard, leaving anything untouched available next time; the offer is
reached from the navbar or by loading the app with a bare recover query
parameter. A draft against a saved workflow is dropped by the save that
supersedes it and otherwise kept, there being one per workflow however long
anyone edits. A draft of a board with no record yet does accumulate, so it
is swept once it passes the session_draft_ttl option's retention, in days,
seven by default – measured from when the draft was opened, so set it well
clear of how long a session typically runs.
plg <- manage_project() blockr.core::is_plugin(plg)plg <- manage_project() blockr.core::is_plugin(plg)
rack_create() serialises data to JSON and stores it as a new record
keyed on id – the board's own stable id, so the record id and the board id
match. It is a strict insert: it errors (class rack_create_exists) if id
already names a record rather than appending a version. name is written to
the backend's native display field. rack_append() adds a new version to
the existing record identified by id, erroring (class
rack_append_missing) if there is none, and never touches the name. Together
they replace the former rack_save(), separating insert from append. To
change a record's name, use rack_rename(). rack_records() lists what a
backend holds, of one kind at a time.
rack_create(backend, data, id, name, draft = FALSE, ...) rack_append(id, backend, data, ...) rack_records(backend, draft = FALSE, ...)rack_create(backend, data, id, name, draft = FALSE, ...) rack_append(id, backend, data, ...) rack_records(backend, draft = FALSE, ...)
backend |
A rack backend object (e.g. a |
data |
An R object to serialise and store (typically the session list returned by the blockr session machinery). |
id |
For |
name |
Character scalar. The display name for the new record. |
draft |
Which records this concerns: |
... |
Additional arguments forwarded to |
rack_create() and rack_append() return a rack_id object
identifying the newly created version, rack_records() a list of
rack_records.
A draft is unsaved work parked for crash recovery. It is an ordinary rack
record in every respect a backend can see – same store, same tags, read
with rack_load() and dropped with rack_purge() – and all that sets it
apart is a reserved id, minted by rack_create(draft = ) and matched by
rack_records(draft = ). Nothing below this API knows drafts exist, so a
backend needs no draft support of its own.
Passing draft also relaxes what rack_create() does, because a draft is a
single slot rather than a history: the id-already-taken check is skipped, so
a repeated write overwrites, and the write is unversioned. With
draft = FALSE the reserved namespace is refused instead, so a user cannot
save a record into it.
A draft is of one kind. A record draft follows a saved workflow and is
keyed on it, so its id is the same in every session: there is one slot per
workflow by construction, it is overwritten as the work changes, and the
save it was shadowing drops it. Nothing about it expires, since a workflow
bounds it. A session draft belongs to a board with no record yet, keyed on
an opaque per-session id, and those do accumulate – so one carries the time
it was opened and is swept on a retention read straight off the id. That
needs neither the optional metadata capability nor any field beyond those
new_rack_record() guarantees. The key is hashed into the id rather than
embedded, since a workflow id may hold characters a backend rejects in a
record name.
The id a draft write returns carries no version: a draft keeps one, and the next write to that slot deletes it.
rack_load() for the complementary load function, rack_rename()
to change a record's name, rack_upload() and rack_list() for the
underlying generics.
Downloads and parses a stored session identified by id from the given
backend. This is a convenience wrapper around rack_download() that
additionally deserialises the JSON payload into an R object.
rack_load(id, backend, ...)rack_load(id, backend, ...)
id |
A |
backend |
A rack backend object (e.g. a |
... |
Additional arguments forwarded to |
The deserialised session data as an R object (typically a named list).
rack_create() and rack_append() for the complementary save
functions, rack_download() for the underlying download generic.
A blockr.core::board_loader() for blockr.core::serve() that picks the
board to build from the request URL. The board named by the URL handle
(board_name / user / version) loads from the session_mgmt_backend
backend; a new handle (minted when the user hits New) yields an empty
board under that fresh id, so saving it forks a distinct record rather than
overwriting the served board; a request without any handle (a cold load)
gets a cleared copy of the served board. The backend is resolved with the
loader's own request (at the GET, before any session) or session (at the WS
connect), so a user-scoped backend such as user_pins_board() resolves
under the visitor's own Posit Connect credentials at both phases; a board
the visitor may not read does not resolve, whatever the user / board_name
in the URL. Pair it with manage_project() when calling
blockr.core::serve():
serve( board, plugins = custom_plugins(manage_project()), loader = rack_loader() )
rack_loader()rack_loader()
A blockr.core::board_loader() object.
The rack layer is the storage abstraction behind blockr session
persistence: it turns a save into a stored, versioned record and a load back
into board state. The shipped backend dispatches on pins boards (see
user_pins_board()), but the contract is storage-agnostic – a database or
document store is supported by implementing the generics documented here and
carrying the rack_backend class, which is what wiring a non-pins backend
into the session_mgmt_backend blockr.core::blockr_option() requires. The
high-level rack_create(),
rack_append() and rack_load() entry points that application code calls
are built on these generics.
new_rack_id(id, version = NULL, user = NULL, ..., class = character()) new_rack_record(id, name, ..., class = character()) rack_content_hash(id, backend, ...) rack_name(id, backend, ...) rack_rename(id, backend, name, ...) rack_exists(id, backend, ...) rack_list(backend, tags = NULL, ...) as_rack_id(x, backend, ...) rack_info(id, backend, ...) rack_download(id, backend, ...) rack_upload(backend, path, ...) rack_delete(id, backend, ...) rack_purge(id, backend, ...) rack_capabilities(backend, ...) rack_tags(id, backend, ...) rack_set_tags(id, backend, tags, ...) rack_acl(id, backend, ...) rack_set_acl(id, backend, acl_type, ...) rack_share(id, backend, with_sub, ...) rack_unshare(id, backend, with_sub, ...) rack_shares(id, backend, ...) rack_find_users(backend, query, ...)new_rack_id(id, version = NULL, user = NULL, ..., class = character()) new_rack_record(id, name, ..., class = character()) rack_content_hash(id, backend, ...) rack_name(id, backend, ...) rack_rename(id, backend, name, ...) rack_exists(id, backend, ...) rack_list(backend, tags = NULL, ...) as_rack_id(x, backend, ...) rack_info(id, backend, ...) rack_download(id, backend, ...) rack_upload(backend, path, ...) rack_delete(id, backend, ...) rack_purge(id, backend, ...) rack_capabilities(backend, ...) rack_tags(id, backend, ...) rack_set_tags(id, backend, tags, ...) rack_acl(id, backend, ...) rack_set_acl(id, backend, acl_type, ...) rack_share(id, backend, with_sub, ...) rack_unshare(id, backend, with_sub, ...) rack_shares(id, backend, ...) rack_find_users(backend, query, ...)
id |
A |
version |
For |
user |
For |
... |
Passed on to methods; for the constructors, extra named fields stored on the object. |
class |
Character vector prepended to the object's class, so a backend
carries its own |
name |
Display name for the record, written to the backend's native
name field and never used as the storage key; |
backend |
A rack backend: the storage object dispatched on. A
pins board is recognized directly; any other backend carries the
|
tags |
Character vector of tags: the filter applied by |
x |
The object to coerce with |
path |
Path to the local file to upload as a new version. |
acl_type |
Access-control level to set, backend-defined (e.g.
|
with_sub |
Backend identifier of the principal to share a record with or revoke. |
query |
Search prefix for |
Two kinds of object flow through the contract. A backend is the board or
store itself, the dispatch target for as_rack_id(), rack_capabilities(),
rack_list(), rack_upload() and rack_find_users(). A rack_id is a
handle to one stored record, the dispatch target for the remaining generics.
as_rack_id() coerces a raw identifier – a component list or a
rack_record listing row – into the backend's own rack_id subclass (via
new_rack_id(..., class = )) from the components (id, version, user)
carried in a session URL, and returns an existing rack_id unchanged.
rack_list() returns new_rack_record() summaries, the listing rows the
workflow menu renders.
new_rack_id() and new_rack_record() return a rack_id and rack_record
respectively. rack_capabilities() returns the named list of flags above.
rack_list() returns a list of rack_records and rack_info() a data
frame of versions (columns version, created, ref, newest first).
as_rack_id(), rack_upload() and rack_rename() return a rack_id;
rack_download() a local file path; rack_name() a string; rack_exists()
a scalar logical; rack_content_hash() the stored payload hash;
rack_tags(), rack_acl() and rack_shares() the stored tags, access
level and shares. The mutators (rack_set_tags(), rack_set_acl(),
rack_share(), rack_unshare(), rack_delete(), rack_purge()) return
invisibly.
rack_capabilities() is the feature switch: it returns a named list of
logical flags declaring which optional features the backend supports, and
the management UI gates the matching generics on it.
versioning: a version history is kept per record: rack_info() lists it
and rack_download() / rack_delete() accept a version.
tags: rack_tags() and rack_set_tags() are implemented.
metadata: a display name (rack_name(), rack_rename()) and a content
hash (rack_content_hash()) are stored alongside the payload.
sharing: rack_share(), rack_unshare() and rack_shares() are
implemented.
visibility: rack_acl() and rack_set_acl() are implemented.
user_discovery: rack_find_users() is implemented.
A minimal backend implements only the core create / load / list set –
as_rack_id(), rack_upload(), rack_download(), rack_exists(),
rack_list(), rack_info() and rack_name() – and returns FALSE for
every optional capability; the generics behind a FALSE flag are then never
reached and need no method.
rack_create(), rack_append() and rack_load() for the
high-level save / load API built on the contract, and user_pins_board()
for the default pins backend.
Resolves the storage backend for the session_mgmt_backend
blockr.core::blockr_option() – of which it is the default – in three
tiers, degrading instead of erroring on a missing credential:
user_pins_board(session = get_session())user_pins_board(session = get_session())
session |
Shiny session whose request carries the Connect user session token; defaults to the current reactive domain. |
When the visitor carries a Posit Connect user session token (the Connect
API Integration is enabled), it is exchanged via connectapi::connect()
for a viewer-scoped API key, so each user reads and writes pins under
their own namespace (pins::board_connect()). Requires the
connectapi package.
With no visitor token but application Connect credentials in the
environment (CONNECT_SERVER and CONNECT_API_KEY), a board on the
application's own account.
Otherwise (e.g. local development, off Connect), pins::board_local().
Set the session_connect_tag blockr.core::blockr_option() to an existing
Connect tag's name (or a "Category/Name" path) to have the workflow listing
filter server-side by that native tag; saves then apply it. A top-level tag
is a category: naming one lists everything tagged beneath it, but saves
cannot apply it, as Connect assigns only nested tags. Unset, the listing
returns every pin and checks blockr membership only on load.
A pins_board: a viewer- or application-scoped board_connect, or
board_local when no Connect credentials are available.
## Not run: # Resolves the backend for the current visitor; call from within a # running Shiny app, as it reads the session request for a Connect # user session token. It is the default value of the # `session_mgmt_backend` blockr option. board <- user_pins_board() ## End(Not run)## Not run: # Resolves the backend for the current visitor; call from within a # running Shiny app, as it reads the session request for a Connect # user session token. It is the default value of the # `session_mgmt_backend` blockr option. board <- user_pins_board() ## End(Not run)