- Workspace `1.1.2` → `1.1.3` (`Cargo.toml`). - Dashboard `0.8.0` → `0.9.0` (`package.json`). - CHANGELOG: full v1.1.3 entry covering ScriptKind, ModuleSource, PicloudModuleResolver, the two caches, dep-graph table, route + trigger module rejection, the latent cross-app trigger gap that this release closes, migrations 0015/0016, and downgrade caveats. - Blueprint: mark the "Can scripts `import` Rhai modules?" question as resolved; one-line pointer to the v1.1.3 semantics. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14 KiB
PiCloud Changelog
v1.1.3 — Modules (unreleased)
Real per-app Rhai module system. Scripts can import "<name>" as <alias>; other scripts in the same app as reusable libraries. The
v1.0 placeholder DummyModuleResolver is replaced by a per-call
PicloudModuleResolver that loads kind = 'module' scripts via a
new ModuleSource trait, compiles them into Rhai modules, caches
the compiled output, and enforces cross-app isolation, circular-
import detection, and an import-depth limit. Two LRU AST caches
(top-level script + per-module compiled module) eliminate the
per-invocation compile cost; both invalidate on updated_at change.
Added
scripts.kindcolumn —'endpoint' | 'module', default'endpoint'. Endpoints handle HTTP routes / trigger events; modules are libraries imported by other scripts. The dashboard scripts list + script detail page surface the distinction as a colored badge.script_importsdep-graph table — populated at script save- time from the literal-pathimport "<name>"declarations in the source. FK-CASCADE on both columns. No admin surface in v1.1.3 (drives a v1.2+ "Used by" dashboard panel and v1.3+ cluster-mode eager invalidation).ModuleSourcetrait —lookup(&SdkCallCx, name). Postgres implPostgresModuleSourcein manager-core.app_idderived fromcx.app_id(cross-app isolation boundary, mirrors KV / docs).PicloudModuleResolver— implementsrhai::ModuleResolver. Per-call instance ownsArc<SdkCallCx>, the in-progress imports stack, the depth counter. Bridges syncresolve()to asynclookup()viaHandle::block_on(safe under the executor'sspawn_blockingwrap). ReplacesDummyModuleResolverat line 139 ofexecutor-core::engine::build_engine.- Module-shape validation —
kind = 'module'source must contain onlyfndeclarations,constdeclarations, andimportstatements at top level (no executable expressions). Walksast.statements()viarhai/internals. Admin endpoint is the primary gate; the resolver re-runs the check at load time for defense in depth against DB-direct inserts. - Per-module compiled-Module cache —
LruCache<(AppId, name), (updated_at, Arc<rhai::Module>)>owned byEngine. Invalidated lazily onupdated_atmismatch. Size viaPICLOUD_MODULE_CACHE_SIZE(default 512). - Top-level script AST cache —
LruCache<ScriptId, (updated_at, Arc<rhai::AST>)>owned byLocalExecutorClient. Same staleness semantics. Size viaPICLOUD_SCRIPT_CACHE_SIZE(default 256). ScriptIdentity+ExecutorClient::execute_with_identity— new method on the trait; default impl forwards toexecutesoRemoteExecutorClient(and future transports) keep working.LocalExecutorClientoverrides it to consult the script cache and pass the resultingArc<rhai::AST>toEngine::execute_ast.Engine::execute_ast— companion toexecutethat takes a pre-compiled AST so callers (the orchestrator) can reuse one compile across many invocations.- Import depth limit —
Limits::module_import_depth_max(default 8). Not script-overridable. - Reserved module names — module-kind scripts cannot be named
log,regex,random,time,json,base64,hex,url,kv,docs,dead_letters,http,files,pubsub,secrets,email,users,queue. Defense against author confusion with stdlib namespaces.
Changed
- Workspace version:
1.1.2→1.1.3. - Rhai SDK version:
1.3→1.4(additive — every v1.3 script still runs unchanged; new surface:import "<name>" as <alias>;for endpoint scripts that consume modules in the same app). - Dashboard version:
0.8.0→0.9.0. Adds kind dropdown on script create + kind badges on the scripts list and detail page. Servicesbundle — grows amodules: Arc<dyn ModuleSource>field. Constructor signature becomesServices::new(kv, docs, dead_letters, events, modules).ScriptValidatortrait —validatenow returnsValidatedScript { imports: Vec<String> }so the repo can write dep-graph edges in the same transaction as the script row. Newvalidate_modulemethod enforces module-shape rules.- Trigger creation tightening —
POST /api/v1/admin/apps/{id}/triggers/{kv,docs,dead_letter}now load the target script and reject when (1) it doesn't exist, (2) it belongs to a different app (latent v1.1.1/v1.1.2 gap — closed in v1.1.3), or (3) it iskind = 'module'. - Route creation —
POST /api/v1/admin/scripts/{id}/routesreturns 400 when the target script iskind = 'module'.
Migrations
0015_scripts_kind.sql— addsscripts.kindwith CHECKIN ('endpoint','module'), composite index(app_id, kind), and a module-name shape CHECK (^[a-zA-Z_][a-zA-Z0-9_]{0,63}$).0016_script_imports.sql— adds the dep-graph table with FK CASCADE on both columns, PK(importer, imported), and a reverse-edge index onimported_script_id.
Downgrade caveats
Rolling back v1.1.3 → v1.1.2 with module-kind scripts present
strands them (no kind column means everything looks like an
endpoint; modules will then succeed as route targets and immediately
fail to execute meaningfully). Migration 0016_script_imports.sql
is safe to drop (the table is auxiliary). 0015_scripts_kind.sql
must be reversed by DROP COLUMN kind only after manually re-homing
or deleting module-kind rows.
v1.1.2 — Documents (unreleased)
docs::* SDK — schemaless JSONB document storage with a first-cut
query DSL — plus docs:* triggers as the second concrete kind on the
v1.1.1 triggers framework. Sets the precedent for the v1.2 query DSL
expansion and dead_letters::list.
Added
- Docs store —
docstable keyed(app_id, collection, id)with JSONB values and a GIN-on-jsonb_path_opsindex. Rhai SDK exposes the handle pattern:docs::collection(name).{create,get,find,find_one,update,delete,list}. Cursor-style pagination onlist. Cross-app isolation enforced viacx.app_id(never script-passed). Document envelope shape returned by reads:#{ id, data: #{...}, created_at, updated_at }— explicit metadata + user-data separation (sets precedent for v1.2dead_letters::list). - Query DSL (v1.1.2 subset) — implicit equality at top level
(
#{ tier: "gold" }), operator-object form (#{ created_at: #{ "$gt": "..." } }), dotted field paths up to 5 levels ("user.email"), and operators$eq/$ne/$gt/$gte/$lt/$lte/$in. Filter modifiers$sort(single field) and$limit. Unsupported operators ($or,$regex, etc.) reject with a clear v1.2-pointer error. - Docs triggers (
docs:*) —docs_trigger_detailstable mirrorskv_trigger_details. Admin endpointPOST /api/v1/admin/apps/{id}/triggers/docsaccepts the same DTO shape as the KV endpoint withopsofDocsEventOp(create / update / delete). Dispatcher routesOutboxSourceKind::Docsthrough the same generic path as KV + dead-letter. ctx.event.docs.prev_data— change-data-capture surface for docs trigger handlers.prev_datacarries the document state prior to the mutation (Nonefor create), letting handlers see what changed. The repo reads the old row in the same SQL statement as the write so the trigger event has the prior value.Capability::AppDocsRead(AppId)+AppDocsWrite(AppId)— granted to Viewer / Editor respectively in the per-app role table. Same trust shape as KV'sAppKvRead/AppKvWrite.
Changed
- Workspace version:
1.1.1→1.1.2. - Rhai SDK version:
1.2→1.3(additive — every v1.2 script still runs unchanged; new surfaces:docs::collection(name).{...},ctx.event.docsfor triggered handlers). - Dashboard version:
0.7.0→0.8.0. Workspace alignment; no docs-specific UI in v1.1.2 (the dashboard's Rhai-mode hints don't list KV completions either — focused UX pass is a separate task). Servicesbundle — grows adocs: Arc<dyn DocsService>field. Constructor signature becomesServices::new(kv, docs, dead_letters, events).- Scope mapping: API keys with
script:readscope can calldocs::find/get/list;script:writecan calldocs::create/update/delete. Same trust shape as KV — honors the seven-scope commitment from v1.1.0.
Migrations
0013_docs.sql—docstable + per-(app_id, collection)index + GIN-on-jsonb_path_opsindex.0014_docs_triggers.sql— extendstriggers.kindandoutbox.source_kindCHECK constraints to include'docs'; addsdocs_trigger_detailstable.
Downgrade caveats
Rolling a deployment back from v1.1.2 → v1.1.1 with docs-source
outbox rows still queued will cause the v1.1.1 dispatcher to fail
deserialising TriggerEvent::Docs (#[serde(tag = "source")]
rejects unknown variants). Drain or delete
outbox WHERE source_kind = 'docs' before downgrading. Trunk-only
deployments don't hit this.
Known limitations
- Text-lex comparison for
$gt/$gte/$lt/$lteis incorrect for unpadded numbers crossing digit-count boundaries ('10' < '9'is TRUE under any text collation). Workaround: zero-pad numeric strings. v1.2's advanced query expansion adds numeric-aware operators. - Concurrent
update()s on the same doc may both emit the pre-updateprev_data(last-writer-wins). Inherited from KV'ssetpattern; documented for forensic-trace use cases. - v1.1.2 has no partial-update DSL — scripts that want partial
update do
get + modify + update. Planned for v1.2.
v1.1.1 — Storage & Events (unreleased)
The triggers framework — KV store + universal outbox + dispatcher + NATS-style sync HTTP + per-route async dispatch + dead-letter handling + dashboard surface. Every subsequent v1.1.x service module (docs, files, pubsub, …) hangs off the dispatcher built here.
Added
- KV store —
kv_entriestable keyed(app_id, collection, key)with JSONB values. Rhai SDK exposes the handle pattern:kv::collection(name).{get,set,has,delete,list}. Cursor-style pagination with opaque base64 cursors. Cross-app isolation enforced viacx.app_id(never script-passed). - Triggers framework (Layout E) — parent
triggerstable + per-kind detail tables (kv_trigger_details,dead_letter_trigger_details). Trigger CRUD admin endpoints (/api/v1/admin/apps/{id}/triggers/{kv,dead_letter}) +Capability::AppManageTriggers(AppId). - Universal outbox + dispatcher — single tokio task that polls
the outbox via
FOR UPDATE SKIP LOCKED, routes due rows to the executor through the sharedExecutionGate. Retry with exponential backoff + ±jitter; on exhaustion, dead-letter. - NATS-style sync HTTP via outbox —
InboxRegistry(in-process oneshot map) lets the orchestrator await dispatcher delivery on every sync HTTP request. Cluster mode (v1.3+) swaps this forLISTEN/NOTIFYbehind the sameInboxResolvertrait. dispatch_mode: asyncon routes —POSTto a route withdispatch_mode = 'async'returns202 Acceptedimmediately; the script runs via the dispatcher (with retries / dead-letter).- Dead-letter handling — separate
dead_letterstable per design notes §4.dead_letters::{replay,resolve}Rhai SDK + admin endpoints +Capability::AppDeadLetterManage(AppId). Recursion-stop rule: dead-letter handler failures annotate the original row asresolution = 'handler_failed'and never produce a new dead-letter or retry. - Dashboard surface for dead letters — unresolved-count red
badge on the apps list + per-app page; per-app dead-letters list
view at
/admin/apps/{slug}/dead-letterswith Replay + Mark resolved per-row actions and expandable payload detail. abandoned_executionstable — forensic row written by the dispatcher when it tries to resolve an inbox the orchestrator already abandoned (timed out). Counter metric path reserved.- Trigger-depth limit —
cx.trigger_depth > max_trigger_depth(default 8) skips execution + logs; does NOT dead-letter (depth-exceeded means "you built a loop"). - GC sweepers — weekly retention sweeps for
dead_letters(30 days) andabandoned_executions(7 days), both withFOR UPDATE SKIP LOCKEDfor cluster-mode safety. - Env-overridable trigger config —
TriggerConfig::from_envreadsPICLOUD_MAX_TRIGGER_DEPTH,PICLOUD_TRIGGER_RETRY_*,PICLOUD_DEAD_LETTER_RETENTION_DAYS,PICLOUD_ABANDONED_EXECUTIONS_RETENTION_DAYS.
Changed
- Workspace version:
1.1.0→1.1.1. - Rhai SDK version:
1.1→1.2(additive — every v1.1 script still runs unchanged; new surfaces:kv::*,dead_letters::*,ctx.eventfor triggered handlers). - Dashboard version:
0.6.0→0.7.0for the dead-letters UI. Servicesbundle — replaces v1.1.0's no-argServices::new()with explicitServices::new(kv, dead_letters, events). Tests useServices::default()for an all-noop bundle.SdkCallCxgrowsis_dead_letter_handler: boolandevent: Option<TriggerEvent>fields.ExecRequestmirrors the newSdkCallCxfields and growseventfor serializable trigger payload transport.- Routes table grows
dispatch_mode TEXT NOT NULL DEFAULT 'sync'(CHECK in {sync, async}). - Schema version: 6 → 12 (migrations 0007 through 0012).
Migrations
0007_kv.sql—kv_entriestable + index0008_triggers.sql—triggers+kv_trigger_details+dead_letter_trigger_details0009_outbox.sql— universaloutboxtable + due-row partial index0010_dead_letters.sql—dead_letterstable + unresolved partial index + GC index0011_abandoned_executions.sql— forensic table + GC index0012_routes_dispatch_mode.sql—routes.dispatch_modecolumn
v1.1.0 — Foundation & Standard Library
See docs/v1.1.x-design-notes.md §7 for the full v1.1.x roadmap.