@zixcel/projection-client 0.10.3 → 0.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zixcel/projection-client",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Bounded renderer-independent projection adoption and presentation runtime",
@@ -19,14 +19,22 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@zixcel/projection-contracts": "0.10.3"
23
- },
24
- "scripts": {
25
- "test": "node --test test/*.test.mjs"
22
+ "@zixcel/projection-contracts": "0.10.4"
26
23
  },
27
24
  "repository": {
28
25
  "type": "git",
29
26
  "url": "git+https://github.com/zixcel/data-presentation.git",
30
27
  "directory": "packages/projection-client"
28
+ },
29
+ "keywords": [
30
+ "data-presentation",
31
+ "projections",
32
+ "client",
33
+ "javascript"
34
+ ],
35
+ "scripts": {
36
+ "test": "node --test test/*.test.mts",
37
+ "build": "tsc -p tsconfig.build.json",
38
+ "typecheck": "tsc -p tsconfig.build.json --noEmit"
31
39
  }
32
- }
40
+ }
package/src/index.d.mts CHANGED
@@ -1,30 +1,40 @@
1
- // Hatter downstream 2026: relations retain the canonical owner shape.
2
- import type {SceneProjection,ProjectionIdentity,Action,Item,Unresolved,Relation,Region} from '@zixcel/projection-contracts/client'
3
- export type {SceneProjection,ProjectionIdentity} from '@zixcel/projection-contracts/client'
4
- export interface Camera {readonly x:number;readonly y:number;readonly z:number}
5
- export type DisplayBinding=NonNullable<SceneProjection['data']['presentation']>[number]
6
- export type TemporalPlan=NonNullable<SceneProjection['data']['temporal']>&{readonly cursor:string|null}
7
- export type ViewMode='spatial'|'structured'
8
- export interface PresentationState {readonly activeScene:string|null;readonly focus:string|null;readonly selectedItems:readonly string[];readonly expandedItems:readonly string[];readonly activeRegion:string|null;readonly navigation:readonly string[];readonly navigationIndex:number;readonly inspector:string|null;readonly loading:boolean;readonly viewMode:ViewMode;readonly camera:Camera;readonly timeCursor:string|null}
9
- export interface ActionHandle {readonly projectionKey:string;readonly projectionRevision:string;readonly sceneId:string;readonly actionId:string}
10
- export interface SceneCommandIntent extends ActionHandle {readonly ownerCommandDescriptor:Action}
11
- export type AdoptionContext={kind:'hydrate',expected:ProjectionIdentity}|{kind:'current',expectedCurrent:string|null}
12
- export interface RenderItem {readonly id:string;readonly content:Item|Unresolved;readonly focused:boolean;readonly selected:boolean;readonly expanded:boolean;readonly related:boolean;readonly display:DisplayBinding|null}
13
- export interface RenderPlan {readonly projectionKey:string;readonly projectionRevision:string;readonly sceneId:string;readonly purpose:string;readonly viewMode:ViewMode;readonly camera:Camera;readonly temporal:TemporalPlan|null;readonly relations:readonly (Relation & {readonly active:boolean})[];readonly regions:readonly (Region & {readonly items:readonly RenderItem[]})[];readonly actions:readonly {descriptor:Action;handle:ActionHandle}[];readonly inspector:Item|Unresolved|null;readonly unplaced:readonly RenderItem[];readonly empty:boolean;readonly truncated:boolean;readonly omittedByPolicy:number}
14
- export function projectionIdentity(snapshot:SceneProjection):ProjectionIdentity
15
- export function renderPlan(projection:SceneProjection,presentation:PresentationState,capabilities?:{roles:readonly string[]}):RenderPlan
16
- export class ClientProjectionRuntime {
17
- readonly projection:SceneProjection|null
18
- readonly presentation:PresentationState
19
- current(key:string):SceneProjection|null
20
- adopt(snapshot:unknown,context:AdoptionContext):SceneProjection
21
- input(input:unknown,context:{expectedCurrent:string|null}):SceneProjection
22
- present(change:Partial<Pick<PresentationState,'focus'|'selectedItems'|'expandedItems'|'activeRegion'|'inspector'|'loading'|'viewMode'|'camera'|'timeCursor'>>):PresentationState
23
- navigate(key:string):PresentationState
24
- back():PresentationState
25
- forward():PresentationState
26
- action(id:string):ActionHandle
27
- intent(handle:unknown):SceneCommandIntent
28
- inspect():{keys:number;snapshots:number;closed:boolean}
29
- close():void
1
+ import { renderPlan } from './render-plan.mjs';
2
+ export { renderPlan };
3
+ import { projectionIdentity } from './state.mjs';
4
+ export { projectionIdentity };
5
+ import type { SceneProjection } from '@zixcel/projection-contracts/client';
6
+ import type { PresentationState, PresentationChange, AdoptionContext } from './types.mjs';
7
+ export type * from './types.mjs';
8
+ export declare class ClientProjectionRuntime {
9
+ #private;
10
+ get projection(): SceneProjection | null;
11
+ get presentation(): PresentationState;
12
+ current(key: string): SceneProjection | null;
13
+ adopt(snapshot: unknown, context: AdoptionContext): SceneProjection;
14
+ input(value: unknown, context: {
15
+ expectedCurrent: string | null;
16
+ }): SceneProjection;
17
+ present(change: PresentationChange): PresentationState;
18
+ navigate(key: string): PresentationState;
19
+ back(): PresentationState;
20
+ forward(): PresentationState;
21
+ action(id: string): Readonly<{
22
+ projectionKey: string;
23
+ projectionRevision: string;
24
+ sceneId: string;
25
+ actionId: string;
26
+ }>;
27
+ intent(value: unknown): Readonly<{
28
+ projectionKey: string;
29
+ projectionRevision: string;
30
+ sceneId: string;
31
+ actionId: string;
32
+ ownerCommandDescriptor: import("@zixcel/projection-contracts/client").Action;
33
+ }>;
34
+ inspect(): {
35
+ keys: number;
36
+ snapshots: number;
37
+ closed: boolean;
38
+ };
39
+ close(): void;
30
40
  }
package/src/index.mjs CHANGED
@@ -1,138 +1,128 @@
1
- // Hatter downstream 2026: one exact Scene, presentation-only view and relation emphasis.
2
- import {validateSnapshot,canonical,copy,frozen,shape,text,roles,ProjectionError} from '@zixcel/projection-contracts/client'
3
-
4
- const fail=code=>{throw new ProjectionError(code)}
5
- const check=(value,code)=>{if(!value)fail(code)}
6
- const empty=()=>({activeScene:null,focus:null,selectedItems:[],expandedItems:[],activeRegion:null,
7
- navigation:[],navigationIndex:-1,inspector:null,loading:false,viewMode:'spatial',camera:{x:0,y:0,z:1},timeCursor:null})
8
- export const projectionIdentity=s=>frozen(copy({key:s.key,revision:s.revision,lineage:s.lineage}))
9
- function sceneSnapshot(snapshot){validateSnapshot(snapshot);check(snapshot.kind==='Scene','SceneUnavailable');return frozen(copy(snapshot))}
10
- function targets(snapshot){return new Set([...snapshot.data.items,...snapshot.data.unresolved].map(i=>i.id))}
11
- function rebase(previous,snapshot){
12
- const ids=targets(snapshot),regions=new Set(snapshot.data.regions.map(r=>r.id))
13
- return frozen({...previous,activeScene:snapshot.key,focus:ids.has(previous.focus)?previous.focus:null,
14
- selectedItems:previous.selectedItems.filter(id=>ids.has(id)),expandedItems:previous.expandedItems.filter(id=>ids.has(id)),
15
- activeRegion:regions.has(previous.activeRegion)?previous.activeRegion:null,inspector:ids.has(previous.inspector)?previous.inspector:null,
16
- timeCursor:snapshot.data.temporal?.items.some(item=>item.itemId===previous.timeCursor)?previous.timeCursor:null})
17
- }
18
-
1
+ import { canonical, copy, frozen, shape, text } from '@zixcel/projection-contracts/client';
2
+ import { renderPlan } from './render-plan.mjs';
3
+ export { renderPlan };
4
+ import { check, empty, sceneSnapshot, targets, rebase, projectionIdentity } from './state.mjs';
5
+ export { projectionIdentity };
19
6
  export class ClientProjectionRuntime {
20
- #cache=new Map();#presentation=frozen(empty());#closed=false
21
- get projection(){return this.#cache.get(this.#presentation.activeScene)?.at(-1)??null}
22
- get presentation(){return this.#presentation}
23
- #open(){check(!this.#closed,'ClientRuntimeClosed')}
24
- current(key){this.#open();return this.#cache.get(key)?.at(-1)??null}
25
- adopt(snapshot,context){
26
- this.#open();shape(context,['kind'],['expected','expectedCurrent'])
27
- const next=sceneSnapshot(snapshot),old=this.current(next.key)
28
- if(context.kind==='hydrate'){
29
- check(canonical(projectionIdentity(next))===canonical(context.expected),'HydrationProjectionMismatch')
30
- check(!old||old.revision===next.revision,'HydrationProjectionMismatch')
31
- }else{
32
- check(context.kind==='current'&&Object.hasOwn(context,'expectedCurrent'),'ProjectionAdoptionError')
33
- check((old?.revision??null)===context.expectedCurrent,'StaleProjectionDelivery')
7
+ #cache = new Map();
8
+ #presentation = frozen(empty());
9
+ #closed = false;
10
+ get projection() { return this.#cache.get(this.#presentation.activeScene ?? '')?.at(-1) ?? null; }
11
+ get presentation() { return this.#presentation; }
12
+ #open() { check(!this.#closed, 'ClientRuntimeClosed'); }
13
+ current(key) { this.#open(); return this.#cache.get(key)?.at(-1) ?? null; }
14
+ adopt(snapshot, context) {
15
+ this.#open();
16
+ shape(context, ['kind'], ['expected', 'expectedCurrent']);
17
+ const next = sceneSnapshot(snapshot), old = this.current(next.key);
18
+ if (context.kind === 'hydrate') {
19
+ check(canonical(projectionIdentity(next)) === canonical(context.expected), 'HydrationProjectionMismatch');
20
+ check(!old || old.revision === next.revision, 'HydrationProjectionMismatch');
21
+ }
22
+ else {
23
+ check(context.kind === 'current' && Object.hasOwn(context, 'expectedCurrent'), 'ProjectionAdoptionError');
24
+ check((old?.revision ?? null) === context.expectedCurrent, 'StaleProjectionDelivery');
25
+ }
26
+ if (!old && this.#cache.size === 8) {
27
+ const evict = [...this.#cache.keys()].find(k => k !== this.#presentation.activeScene);
28
+ check(evict !== undefined, 'SceneUnavailable');
29
+ this.#cache.delete(evict);
30
+ }
31
+ const versions = this.#cache.get(next.key) ?? [];
32
+ const retained = versions.filter(v => v.revision !== next.revision).slice(-1);
33
+ this.#cache.delete(next.key);
34
+ this.#cache.set(next.key, [...retained, next]);
35
+ if (this.#presentation.activeScene === next.key)
36
+ this.#presentation = rebase(this.#presentation, next);
37
+ else if (this.#presentation.activeScene === null)
38
+ this.navigate(next.key);
39
+ return next;
34
40
  }
35
- if(!old&&this.#cache.size===8){
36
- const evict=[...this.#cache.keys()].find(k=>k!==this.#presentation.activeScene)
37
- this.#cache.delete(evict)
41
+ input(value, context) {
42
+ this.#open();
43
+ shape(context, ['expectedCurrent']);
44
+ const input = value;
45
+ shape(value, ['kind', 'revision'], input?.kind === 'Snapshot' ? ['snapshot'] : []);
46
+ text(input.revision);
47
+ if (input.kind === 'Snapshot') {
48
+ const snapshot = input.snapshot;
49
+ check(snapshot?.revision === input.revision, 'ProjectionAdoptionError');
50
+ return this.adopt(input.snapshot, { kind: 'current', expectedCurrent: context.expectedCurrent });
51
+ }
52
+ check(input.kind === 'NoChange', 'ProjectionAdoptionError');
53
+ check(this.projection && this.projection.revision === input.revision && input.revision === context.expectedCurrent, 'StaleProjectionDelivery');
54
+ return this.projection;
38
55
  }
39
- const versions=this.#cache.get(next.key)??[]
40
- const retained=versions.filter(v=>v.revision!==next.revision).slice(-1)
41
- this.#cache.delete(next.key);this.#cache.set(next.key,[...retained,next])
42
- if(this.#presentation.activeScene===next.key)this.#presentation=rebase(this.#presentation,next)
43
- else if(this.#presentation.activeScene===null)this.navigate(next.key)
44
- return next
45
- }
46
- input(input,context){
47
- this.#open();shape(context,['expectedCurrent']);shape(input,['kind','revision'],input?.kind==='Snapshot'?['snapshot']:[]);text(input.revision)
48
- if(input.kind==='Snapshot'){
49
- check(input.snapshot?.revision===input.revision,'ProjectionAdoptionError')
50
- return this.adopt(input.snapshot,{kind:'current',expectedCurrent:context.expectedCurrent})
56
+ present(change) {
57
+ this.#open();
58
+ check(this.projection, 'SceneUnavailable');
59
+ shape(change, [], ['focus', 'selectedItems', 'expandedItems', 'activeRegion', 'inspector', 'loading', 'viewMode', 'camera', 'timeCursor']);
60
+ const ids = targets(this.projection), regions = new Set(this.projection.data.regions.map(r => r.id));
61
+ for (const key of ['selectedItems', 'expandedItems'])
62
+ if (Object.hasOwn(change, key)) {
63
+ check(Array.isArray(change[key]) && change[key].length <= 256, 'PresentationLimitExceeded');
64
+ check(new Set(change[key]).size === change[key].length && change[key].every(id => ids.has(id)), 'InvalidPresentationTarget');
65
+ }
66
+ for (const key of ['focus', 'inspector'])
67
+ if (Object.hasOwn(change, key))
68
+ check(change[key] === null || ids.has(change[key] ?? ''), 'InvalidPresentationTarget');
69
+ if (Object.hasOwn(change, 'activeRegion'))
70
+ check(change.activeRegion === null || regions.has(change.activeRegion ?? ''), 'InvalidPresentationTarget');
71
+ if (Object.hasOwn(change, 'loading'))
72
+ check(typeof change.loading === 'boolean', 'InvalidPresentationTarget');
73
+ if (Object.hasOwn(change, 'viewMode'))
74
+ check(['spatial', 'structured'].includes(change.viewMode ?? ''), 'InvalidPresentationTarget');
75
+ if (Object.hasOwn(change, 'camera')) {
76
+ shape(change.camera, ['x', 'y', 'z']);
77
+ check(change.camera, 'InvalidPresentationTarget');
78
+ for (const key of ['x', 'y', 'z'])
79
+ check(Number.isFinite(change.camera[key]) && change.camera[key] >= (key === 'z' ? .65 : -1) && change.camera[key] <= (key === 'z' ? 1.5 : 1), 'InvalidPresentationTarget');
80
+ }
81
+ if (Object.hasOwn(change, 'timeCursor'))
82
+ check(change.timeCursor === null || this.projection.data.temporal?.items.some(item => item.itemId === change.timeCursor), 'InvalidPresentationTarget');
83
+ // Camera is bounded local presentation, not integer-only canonical source data.
84
+ const { camera, ...rest } = change;
85
+ this.#presentation = frozen({ ...this.#presentation, ...copy(rest), ...(camera ? { camera: { ...camera } } : {}) });
86
+ return this.#presentation;
51
87
  }
52
- check(input.kind==='NoChange','ProjectionAdoptionError')
53
- check(this.projection?.revision===input.revision&&input.revision===context.expectedCurrent,'StaleProjectionDelivery')
54
- return this.projection
55
- }
56
- present(change){
57
- this.#open();check(this.projection,'SceneUnavailable')
58
- shape(change,[],['focus','selectedItems','expandedItems','activeRegion','inspector','loading','viewMode','camera','timeCursor'])
59
- const ids=targets(this.projection),regions=new Set(this.projection.data.regions.map(r=>r.id))
60
- for(const key of ['selectedItems','expandedItems'])if(Object.hasOwn(change,key)){
61
- check(Array.isArray(change[key])&&change[key].length<=256,'PresentationLimitExceeded')
62
- check(new Set(change[key]).size===change[key].length&&change[key].every(id=>ids.has(id)),'InvalidPresentationTarget')
88
+ navigate(key) {
89
+ this.#open();
90
+ const next = this.current(key);
91
+ check(next, 'SceneUnavailable');
92
+ if (this.#presentation.activeScene === key)
93
+ return this.#presentation;
94
+ const navigation = [...this.#presentation.navigation.slice(0, this.#presentation.navigationIndex + 1), key].slice(-64);
95
+ this.#presentation = rebase({ ...empty(), navigation, navigationIndex: navigation.length - 1 }, next);
96
+ return this.#presentation;
63
97
  }
64
- for(const key of ['focus','inspector'])if(Object.hasOwn(change,key))check(change[key]===null||ids.has(change[key]),'InvalidPresentationTarget')
65
- if(Object.hasOwn(change,'activeRegion'))check(change.activeRegion===null||regions.has(change.activeRegion),'InvalidPresentationTarget')
66
- if(Object.hasOwn(change,'loading'))check(typeof change.loading==='boolean','InvalidPresentationTarget')
67
- if(Object.hasOwn(change,'viewMode'))check(['spatial','structured'].includes(change.viewMode),'InvalidPresentationTarget')
68
- if(Object.hasOwn(change,'camera')){
69
- shape(change.camera,['x','y','z'])
70
- for(const key of ['x','y','z'])check(Number.isFinite(change.camera[key])&&change.camera[key]>=(key==='z'?.65:-1)&&change.camera[key]<=(key==='z'?1.5:1),'InvalidPresentationTarget')
98
+ #move(delta) {
99
+ this.#open();
100
+ const index = this.#presentation.navigationIndex + delta;
101
+ if (index < 0 || index >= this.#presentation.navigation.length)
102
+ return this.#presentation;
103
+ const next = this.current(this.#presentation.navigation[index]);
104
+ check(next, 'SceneUnavailable');
105
+ this.#presentation = rebase({ ...empty(), navigation: this.#presentation.navigation, navigationIndex: index }, next);
106
+ return this.#presentation;
71
107
  }
72
- if(Object.hasOwn(change,'timeCursor'))check(change.timeCursor===null||this.projection.data.temporal?.items.some(item=>item.itemId===change.timeCursor),'InvalidPresentationTarget')
73
- // Camera is bounded local presentation, not integer-only canonical source data.
74
- const {camera,...rest}=change
75
- this.#presentation=frozen({...this.#presentation,...copy(rest),...(camera?{camera:{...camera}}:{})})
76
- return this.#presentation
77
- }
78
- navigate(key){
79
- this.#open();const next=this.current(key);check(next,'SceneUnavailable')
80
- if(this.#presentation.activeScene===key)return this.#presentation
81
- const navigation=[...this.#presentation.navigation.slice(0,this.#presentation.navigationIndex+1),key].slice(-64)
82
- this.#presentation=rebase({...empty(),navigation,navigationIndex:navigation.length-1},next)
83
- return this.#presentation
84
- }
85
- #move(delta){
86
- this.#open();const index=this.#presentation.navigationIndex+delta
87
- if(index<0||index>=this.#presentation.navigation.length)return this.#presentation
88
- const next=this.current(this.#presentation.navigation[index]);check(next,'SceneUnavailable')
89
- this.#presentation=rebase({...empty(),navigation:this.#presentation.navigation,navigationIndex:index},next)
90
- return this.#presentation
91
- }
92
- back(){return this.#move(-1)}
93
- forward(){return this.#move(1)}
94
- action(id){
95
- this.#open();check(this.projection?.data.actions.some(a=>a.id===id),'StaleSceneAction')
96
- return frozen({projectionKey:this.projection.key,projectionRevision:this.projection.revision,sceneId:this.projection.key,actionId:id})
97
- }
98
- intent(handle){
99
- this.#open();shape(handle,['projectionKey','projectionRevision','sceneId','actionId'])
100
- const p=this.projection
101
- check(p&&handle.projectionKey===p.key&&handle.sceneId===p.key&&handle.projectionRevision===p.revision,'StaleSceneAction')
102
- const action=p.data.actions.find(a=>a.id===handle.actionId);check(action,'StaleSceneAction')
103
- return frozen({...copy(handle),ownerCommandDescriptor:action})
104
- }
105
- inspect(){return {keys:this.#cache.size,snapshots:[...this.#cache.values()].reduce((n,v)=>n+v.length,0),closed:this.#closed}}
106
- close(){this.#closed=true;this.#cache.clear();this.#presentation=frozen(empty())}
107
- }
108
-
109
- export function renderPlan(projection,presentation,capabilities={roles}){
110
- check(projection&&presentation.activeScene===projection.key,'RenderPlanError')
111
- const values=new Map([...projection.data.items,...projection.data.unresolved].map(v=>[v.id,v]))
112
- const selected=new Set(presentation.selectedItems),expanded=new Set(presentation.expandedItems)
113
- const active=id=>selected.has(id)||presentation.focus===id,related=new Set()
114
- const relations=projection.data.relations.map(relation=>{
115
- const highlighted=active(relation.from)||active(relation.to)
116
- if(highlighted){related.add(relation.from);related.add(relation.to)}
117
- return {...relation,active:highlighted}
118
- })
119
- const item=content=>({id:content.id,content,focused:presentation.focus===content.id,
120
- selected:selected.has(content.id),expanded:expanded.has(content.id),related:related.has(content.id),
121
- display:projection.data.presentation?.find(binding=>binding.itemId===content.id)??null})
122
- const regions=projection.data.regions.map(region=>{
123
- check(capabilities.roles.includes(region.role),'UnsupportedRenderer')
124
- return {...region,items:region.itemIds.map(id=>{
125
- check(values.has(id),'RenderPlanError')
126
- return item(values.get(id))
127
- })}
128
- })
129
- return frozen({projectionKey:projection.key,projectionRevision:projection.revision,sceneId:projection.key,
130
- purpose:projection.data.purpose,viewMode:presentation.viewMode,camera:presentation.camera,
131
- temporal:projection.data.temporal?{...projection.data.temporal,cursor:presentation.timeCursor}:null,
132
- regions,relations,actions:projection.data.actions.map(action=>({descriptor:action,
133
- handle:{projectionKey:projection.key,projectionRevision:projection.revision,sceneId:projection.key,actionId:action.id}})),
134
- inspector:presentation.inspector===null?null:values.get(presentation.inspector)??null,
135
- unplaced:[...values.values()].filter(v=>!projection.data.regions.some(r=>r.itemIds.includes(v.id)))
136
- .map(item),
137
- empty:values.size===0,truncated:projection.data.truncated,omittedByPolicy:projection.data.omittedByPolicy})
108
+ back() { return this.#move(-1); }
109
+ forward() { return this.#move(1); }
110
+ action(id) {
111
+ this.#open();
112
+ const projection = this.projection;
113
+ check(projection && projection.data.actions.some(a => a.id === id), 'StaleSceneAction');
114
+ return frozen({ projectionKey: projection.key, projectionRevision: projection.revision, sceneId: projection.key, actionId: id });
115
+ }
116
+ intent(value) {
117
+ this.#open();
118
+ shape(value, ['projectionKey', 'projectionRevision', 'sceneId', 'actionId']);
119
+ const handle = value;
120
+ const p = this.projection;
121
+ check(p && handle.projectionKey === p.key && handle.sceneId === p.key && handle.projectionRevision === p.revision, 'StaleSceneAction');
122
+ const action = p.data.actions.find(a => a.id === handle.actionId);
123
+ check(action, 'StaleSceneAction');
124
+ return frozen({ projectionKey: p.key, projectionRevision: p.revision, sceneId: p.key, actionId: action.id, ownerCommandDescriptor: action });
125
+ }
126
+ inspect() { return { keys: this.#cache.size, snapshots: [...this.#cache.values()].reduce((n, v) => n + v.length, 0), closed: this.#closed }; }
127
+ close() { this.#closed = true; this.#cache.clear(); this.#presentation = frozen(empty()); }
138
128
  }
@@ -0,0 +1,5 @@
1
+ import type { SceneProjection } from '@zixcel/projection-contracts/client';
2
+ import type { PresentationState, RenderPlan } from './types.mjs';
3
+ export declare function renderPlan(projection: SceneProjection, presentation: PresentationState, capabilities?: {
4
+ roles: readonly string[];
5
+ }): RenderPlan;
@@ -0,0 +1,37 @@
1
+ import { frozen, roles, ProjectionError } from '@zixcel/projection-contracts/client';
2
+ function check(value, code) { if (!value)
3
+ throw new ProjectionError(code); }
4
+ export function renderPlan(projection, presentation, capabilities = { roles }) {
5
+ check(projection && presentation.activeScene === projection.key, 'RenderPlanError');
6
+ const values = new Map([...projection.data.items, ...projection.data.unresolved].map(v => [v.id, v]));
7
+ const selected = new Set(presentation.selectedItems), expanded = new Set(presentation.expandedItems);
8
+ const active = (id) => selected.has(id) || presentation.focus === id, related = new Set();
9
+ const relations = projection.data.relations.map(relation => {
10
+ const highlighted = active(relation.from) || active(relation.to);
11
+ if (highlighted) {
12
+ related.add(relation.from);
13
+ related.add(relation.to);
14
+ }
15
+ return { ...relation, active: highlighted };
16
+ });
17
+ const item = (content) => ({ id: content.id, content, focused: presentation.focus === content.id,
18
+ selected: selected.has(content.id), expanded: expanded.has(content.id), related: related.has(content.id),
19
+ display: projection.data.presentation?.find(binding => binding.itemId === content.id) ?? null });
20
+ const regions = projection.data.regions.map(region => {
21
+ check(capabilities.roles.includes(region.role), 'UnsupportedRenderer');
22
+ return { ...region, items: region.itemIds.map(id => {
23
+ const content = values.get(id);
24
+ check(content, 'RenderPlanError');
25
+ return item(content);
26
+ }) };
27
+ });
28
+ return frozen({ projectionKey: projection.key, projectionRevision: projection.revision, sceneId: projection.key,
29
+ purpose: projection.data.purpose, viewMode: presentation.viewMode, camera: presentation.camera,
30
+ temporal: projection.data.temporal ? { ...projection.data.temporal, cursor: presentation.timeCursor } : null,
31
+ regions, relations, actions: projection.data.actions.map(action => ({ descriptor: action,
32
+ handle: { projectionKey: projection.key, projectionRevision: projection.revision, sceneId: projection.key, actionId: action.id } })),
33
+ inspector: presentation.inspector === null ? null : values.get(presentation.inspector) ?? null,
34
+ unplaced: [...values.values()].filter(v => !projection.data.regions.some(r => r.itemIds.includes(v.id)))
35
+ .map(item),
36
+ empty: values.size === 0, truncated: projection.data.truncated, omittedByPolicy: projection.data.omittedByPolicy });
37
+ }
@@ -0,0 +1,9 @@
1
+ import type { SceneProjection, ProjectionIdentity } from '@zixcel/projection-contracts/client';
2
+ import type { PresentationState } from './types.mjs';
3
+ export declare function fail(code: string): never;
4
+ export declare function check(value: unknown, code: string): asserts value;
5
+ export declare const empty: () => PresentationState;
6
+ export declare const projectionIdentity: (s: SceneProjection) => ProjectionIdentity;
7
+ export declare function sceneSnapshot(value: unknown): SceneProjection;
8
+ export declare function targets(snapshot: SceneProjection): Set<string>;
9
+ export declare function rebase(previous: PresentationState, snapshot: SceneProjection): PresentationState;
package/src/state.mjs ADDED
@@ -0,0 +1,16 @@
1
+ import { validateSnapshot, copy, frozen, ProjectionError } from '@zixcel/projection-contracts/client';
2
+ export function fail(code) { throw new ProjectionError(code); }
3
+ export function check(value, code) { if (!value)
4
+ fail(code); }
5
+ export const empty = () => ({ activeScene: null, focus: null, selectedItems: [], expandedItems: [], activeRegion: null,
6
+ navigation: [], navigationIndex: -1, inspector: null, loading: false, viewMode: 'spatial', camera: { x: 0, y: 0, z: 1 }, timeCursor: null });
7
+ export const projectionIdentity = (s) => frozen(copy({ key: s.key, revision: s.revision, lineage: s.lineage }));
8
+ export function sceneSnapshot(value) { const snapshot = validateSnapshot(value); check(snapshot.kind === 'Scene', 'SceneUnavailable'); return frozen(copy(snapshot)); }
9
+ export function targets(snapshot) { return new Set([...snapshot.data.items, ...snapshot.data.unresolved].map(i => i.id)); }
10
+ export function rebase(previous, snapshot) {
11
+ const ids = targets(snapshot), regions = new Set(snapshot.data.regions.map(r => r.id));
12
+ return frozen({ ...previous, activeScene: snapshot.key, focus: ids.has(previous.focus ?? '') ? previous.focus : null,
13
+ selectedItems: previous.selectedItems.filter(id => ids.has(id)), expandedItems: previous.expandedItems.filter(id => ids.has(id)),
14
+ activeRegion: regions.has(previous.activeRegion ?? '') ? previous.activeRegion : null, inspector: ids.has(previous.inspector ?? '') ? previous.inspector : null,
15
+ timeCursor: snapshot.data.temporal?.items.some(item => item.itemId === previous.timeCursor) ? previous.timeCursor : null });
16
+ }
@@ -0,0 +1,76 @@
1
+ import type { SceneProjection, ProjectionIdentity, Action, Item, Unresolved, Relation, Region } from '@zixcel/projection-contracts/client';
2
+ export type { SceneProjection, ProjectionIdentity } from '@zixcel/projection-contracts/client';
3
+ export interface Camera {
4
+ readonly x: number;
5
+ readonly y: number;
6
+ readonly z: number;
7
+ }
8
+ export type DisplayBinding = NonNullable<SceneProjection['data']['presentation']>[number];
9
+ export type TemporalPlan = NonNullable<SceneProjection['data']['temporal']> & {
10
+ readonly cursor: string | null;
11
+ };
12
+ export type ViewMode = 'spatial' | 'structured';
13
+ export interface PresentationState {
14
+ readonly activeScene: string | null;
15
+ readonly focus: string | null;
16
+ readonly selectedItems: readonly string[];
17
+ readonly expandedItems: readonly string[];
18
+ readonly activeRegion: string | null;
19
+ readonly navigation: readonly string[];
20
+ readonly navigationIndex: number;
21
+ readonly inspector: string | null;
22
+ readonly loading: boolean;
23
+ readonly viewMode: ViewMode;
24
+ readonly camera: Camera;
25
+ readonly timeCursor: string | null;
26
+ }
27
+ export interface ActionHandle {
28
+ readonly projectionKey: string;
29
+ readonly projectionRevision: string;
30
+ readonly sceneId: string;
31
+ readonly actionId: string;
32
+ }
33
+ export interface SceneCommandIntent extends ActionHandle {
34
+ readonly ownerCommandDescriptor: Action;
35
+ }
36
+ export type AdoptionContext = {
37
+ kind: 'hydrate';
38
+ expected: ProjectionIdentity;
39
+ } | {
40
+ kind: 'current';
41
+ expectedCurrent: string | null;
42
+ };
43
+ export interface RenderItem {
44
+ readonly id: string;
45
+ readonly content: Item | Unresolved;
46
+ readonly focused: boolean;
47
+ readonly selected: boolean;
48
+ readonly expanded: boolean;
49
+ readonly related: boolean;
50
+ readonly display: DisplayBinding | null;
51
+ }
52
+ export interface RenderPlan {
53
+ readonly projectionKey: string;
54
+ readonly projectionRevision: string;
55
+ readonly sceneId: string;
56
+ readonly purpose: string;
57
+ readonly viewMode: ViewMode;
58
+ readonly camera: Camera;
59
+ readonly temporal: TemporalPlan | null;
60
+ readonly relations: readonly (Relation & {
61
+ readonly active: boolean;
62
+ })[];
63
+ readonly regions: readonly (Region & {
64
+ readonly items: readonly RenderItem[];
65
+ })[];
66
+ readonly actions: readonly {
67
+ descriptor: Action;
68
+ handle: ActionHandle;
69
+ }[];
70
+ readonly inspector: Item | Unresolved | null;
71
+ readonly unplaced: readonly RenderItem[];
72
+ readonly empty: boolean;
73
+ readonly truncated: boolean;
74
+ readonly omittedByPolicy: number;
75
+ }
76
+ export type PresentationChange = Partial<Pick<PresentationState, 'focus' | 'selectedItems' | 'expandedItems' | 'activeRegion' | 'inspector' | 'loading' | 'viewMode' | 'camera' | 'timeCursor'>>;
package/src/types.mjs ADDED
@@ -0,0 +1 @@
1
+ export {};