Shared scenes and live receiver ownership
15.1 / 11.4c3 connects shared scene lifecycle and HTTP/WebSocket receiver ownership. Browser main, strict Worker, retained native QuickJS and the actual desktop script service use the same contract. Applications provide layer panels, buttons and tool UI. 15.2 adds static GeoJSON display resources; 15.5 connects common route playback; tracks and line animation remain later phases. Mobile hosts/devices and external Windows/macOS require their own acceptance.
import {map} from 'sqsmap';
const capabilities = await map.sqs.getCapabilities();
if (!capabilities.sceneReceiverOwnership) throw Error('Shared scene ownership is unavailable');
const scene = await map.sqs.scenes.create({id:'positions',name:'Position updates'});
map.addSource('positions',{type:'geojson',data:{type:'FeatureCollection',features:[]},sqsLive:{fields:{n:'number'},allowDelta:true}});
await map.sqs.flush();
const source = await map.getSource('positions');
const receiver = await source.sqs.connectLive({owner:scene,transport:{type:'websocket',url:'wss://your-server.example/live',reconnectDelayMs:2000}});
const subscription = map.on('sqs.scenechange', event => {
// Update your scene panel; event.scene is a historical snapshot.
});
await scene.start();
await scene.pause();
await scene.resume();
await scene.stop();
await map.sqs.scenes.remove(scene);
subscription.unsubscribe();
Replace the placeholder with a service implementing gis-live-v1. HTTP uses {type:'http-poll',url:'https://your-server.example/live',intervalMs:2000}. owner accepts a scene handle or plain {id,sceneInstanceId}. Core receives only identity data and owns no VM, callback or host object. start confirms queued networking, not data readiness; observe sourcedata/error.
create returns an immutable handle; get(id) returns the current handle or undefined; list returns immutable small snapshots. getState returns {id,sceneInstanceId,name,state,revision,resourceCount,receivers,sources}; each receiver is {source:{id,sourceInstanceId},receiverInstanceId}. Identity/revision strings are opaque: never convert them to JS numbers. Snapshots include registered stopped receivers; resourceCount is not the number of active connections. Source deletion or receiver replacement updates actual registrations.
| State | Controls and result |
|---|---|
| draft | No network request; start enters running; stop/remove terminate |
| running | pause closes owned connections and preserves committed data; stop/remove terminate |
| paused | resume creates a new HTTP request/WebSocket connection; stop/remove terminate |
| stopped | Terminal; cannot resume/start; remove releases identity |
Repeated start while running, pause while paused, resume while running and stop while stopped do not increase scene revisions or publish events. Other inapplicable transitions yield CONFLICT.
Scene pause and manual receiver pause are independent. Scene resume does not restart manually paused or stopped receivers. receiver.resume under a draft/paused/stopped owner yields CONFLICT. Receiver state and source events add optional owner/ownerState. Unowned receivers retain their prior behavior. Each resumed WebSocket requires a full snapshot first; late obsolete generations cannot write. Errors retain committed data and use the existing transport retry policy.
Maps/sources own scenes, receivers and data after scripts or Runners exit. scene.remove stops owned receivers and releases identity, preserving source data still displayed by layers. Source deletion still requires removing referencing layers first. Map destruction closes connections and releases resources. Rebuilding an id gives a new identity; stale handles, deleted sources and foreign map identities yield STALE_HANDLE. Isolated Workers use random per-map identity namespaces so equal numeric counters cannot authorize foreign scenes.
sqs.scenechange carries reason and an immutable historical scene snapshot. Reasons include create/start/pause/resume/stop/remove/bind/unbind. No URL, credentials, geometry or body appears. Subscribe before querying the current snapshot to establish UI state. The native one-shot script entry does not support persistent subscriptions; use retained sessions. Group controls prepare all receivers, cancellations and event/output budgets before committing; failures never partially control a scene.
Limits:16 scenes per map including drafts/stopped scenes; remove releases quota. Eight active/paused receivers per map include draft registrations. Each scene allows128 registered items, with actual receivers still limited to8 per map. id/name have128 UTF-8 byte limits, no empty/trimmed/control text. Scene JSON input/output is64KiB and the core scene-event queue128 entries. Host event/network queues have additional budgets; these are not whole-process memory limits.
await map.sqs.getCapabilities() and browser map.capabilities expose an extensible catalogue: sceneLifecycle/sceneReceiverOwnership true; sceneStaticResources true; scenePlayback/sceneRoutePlayback true; sceneLinePlayback/sceneTracks true; see line playback. Missing or false means unavailable. Legacy desktop map.scenes keeps its existing compatibility scope and is not the shared entry point.
Native libraries add sqsm_map_scene_json(handle,json,len,out) and sqsm_map_capabilities_json(handle,out) with ABI1 compatibility; the current high-level header has36 functions. Request shape: {"method":"create","args":[{"id":"positions"}]}. Methods are create/get/list/getState/start/pause/resume/stop/remove. getState and lifecycle controls take args:[{id,sceneInstanceId}]; get takes args:[id]; list takes args:[]. C returns a scene snapshot/list; missing get is null. Release outputs with sqsm_map_bytes_free. Errors clear output and failed output allocation changes no scene. Capability JSON is extensible; legacy uint32 flag meanings stay unchanged. Pump requests/events/responses on the map creation thread and actually close cancelled connections. See native reference.
The full HTML includes all seven button handlers, credits and pagehide destruction. Deploy alongside the matched SDK. Download complete HTML and portable acceptance script. Replace credits with the actual provider requirements; loopback3131/3133 is an acceptance fixture only.
The same43-check script passes on all four hosts. Browser checks also cover real HTTP cancellation, socket closure, draft no-I/O, same-id foreign ownership, VM-exit ownership and four rendered frames. Linux software Vulkan/Gl does not establish hardware WebGPU, external desktop or mobile-device acceptance. Development snapshots are not formal npm/native releases or remote deployment.
Complete HTML / CSS / JavaScript
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>SqsMap shared scenes</title>
<style>body{margin:0;font:16px system-ui}#map{height:70vh}form{display:flex;flex-wrap:wrap;gap:.5rem;padding:1rem}input{flex:1;min-width:16rem}button,input{font:inherit;padding:.4rem}pre,footer{padding:0 1rem;white-space:pre-wrap}</style>
<div id="map"></div><form id="tools"><input id="url" type="url" required placeholder="Your gis-live-v1 WebSocket endpoint"><button id="connect">Create draft</button><button type="button" id="start">Start</button><button type="button" id="pause">Pause</button><button type="button" id="resume">Resume</button><button type="button" id="stop">Stop</button><button type="button" id="remove">Remove scene</button><button type="button" id="freeze">Freeze data</button></form><pre id="status">Enter your WebSocket endpoint.</pre><footer id="credits"></footer>
<script type="module">
import {MapView} from './dist/index.js';
const status=document.querySelector('#status');let map,source,receiver,scene;const offs=[];
async function show(){status.textContent=JSON.stringify({scene:scene?await scene.getState():null,receiver:receiver?await receiver.getState():null,protocol:await source.sqs.getLiveState()},null,2);}
async function act(fn){try{await fn();}catch(error){status.textContent=String(error);}}
function requireScene(){if(!scene)throw Error('Create draft first.');return scene;}
try{
map=await MapView.create(document.querySelector('#map'),{worker:false});if(!map.capabilities.sceneReceiverOwnership)throw Error('Matched scene SDK/WASM required');
map.jumpTo({center:[0,0],zoom:12});map.addSource('positions',{type:'geojson',data:{type:'FeatureCollection',features:[]},sqsLive:{fields:{n:'number'},allowDelta:true},attribution:'Live positions supplied by the endpoint owner'});map.addLayer({id:'positions',type:'circle',source:'positions',paint:{'circle-radius':8,'circle-color':'#ff0000'}});await map.flush();source=await map.getSource('positions');
for(const type of ['sourcedataloading','sourcedata','sourcedataabort','error'])offs.push(map.on(type,event=>{if(event.sourceId==='positions')status.textContent=JSON.stringify({type,protocol:event.sqsLive,receiver:event.sqsLiveReceiver},null,2);}));
offs.push(map.on('sqs.scenechange',event=>{status.textContent=JSON.stringify({type:event.type,reason:event.reason,scene:event.scene},null,2);}));offs.push(map.on('sqs.attributionschange',event=>{document.querySelector('#credits').textContent=event.attributions.text;}));document.querySelector('#credits').textContent=(await map.getAttributions()).text;
document.querySelector('#tools').onsubmit=event=>{event.preventDefault();act(async()=>{if(scene){await map.sqs.scenes.remove(scene);scene=null;}scene=await map.sqs.scenes.create({id:'positions',name:'Live positions'});receiver=await source.sqs.connectLive({owner:scene,transport:{type:'websocket',url:document.querySelector('#url').value,reconnectDelayMs:1000}});await show();});};
for(const method of ['start','pause','resume','stop'])document.querySelector('#'+method).onclick=()=>act(async()=>{await requireScene()[method]();await show();});
document.querySelector('#remove').onclick=()=>act(async()=>{await map.sqs.scenes.remove(requireScene());scene=null;await show();});
document.querySelector('#freeze').onclick=()=>act(async()=>{status.textContent=JSON.stringify(await source.sqs.freezeLiveSnapshot(),null,2);});
window.addEventListener('pagehide',()=>{offs.forEach(off=>off());map.destroy();},{once:true});
}catch(error){status.textContent=String(error);offs.forEach(off=>off());map?.destroy();}
</script></html>
Independent C scene example uses the same committed core as the header and script declarations.