@astrale-os/shell-react 0.2.4 → 0.2.6

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/dist/index.d.ts CHANGED
@@ -26,8 +26,8 @@ export { SelectionScope, useSelection } from './selection/index.js';
26
26
  export type { Selection, SelectionScopeProps } from './selection/index.js';
27
27
  export { useCloseGuard, useTargetNode, useViewsFor, useWindow, useWindowList, useWindows, ViewHost, ViewStage, } from './window/index.js';
28
28
  export type { CloseGuard, CloseVerdict, ViewDescriptor, ViewHostProps, ViewSource, ViewHostHandle, ViewsForResult, WindowListEntry, WindowState, WindowView, } from './window/index.js';
29
- export { useIntent, useSendIntent } from './intents/index.js';
30
- export type { IntentCtx, IntentPayload, IntentReply, SendIntent } from './intents/index.js';
29
+ export { useEmitIntent, useIntent, useSendIntent } from './intents/index.js';
30
+ export type { EmitIntent, IntentCtx, IntentPayload, IntentReply, SendIntent, } from './intents/index.js';
31
31
  export { AppHost, AppLogo, useApps, useCurrentSpace, useOpenApp, useRoles, useSpace, useSpaces, useUser, } from './application/index.js';
32
32
  export type { AppHostFallbackState, AppHostProps, AppLogoProps, AppsResult, CurrentSpaceResult, RolesResult, SpaceResult, SpacesResult, } from './application/index.js';
33
33
  export type { ApplicationNode, AppSession, RoleNode, SpaceNode, UserNode } from '@astrale-os/shell';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACxE,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAChG,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAGxD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AACjE,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAKjD,OAAO,EACL,OAAO,EACP,WAAW,EACX,QAAQ,EACR,OAAO,EACP,SAAS,EACT,OAAO,EACP,QAAQ,EACR,SAAS,GACV,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,eAAe,EACf,cAAc,EACd,WAAW,EACX,UAAU,EACV,IAAI,EACJ,UAAU,EACV,WAAW,EACX,UAAU,EACV,gBAAgB,GACjB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAGhF,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAC/F,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACxD,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACtD,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,cAAc,EACd,cAAc,EACd,cAAc,EACd,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,oBAAoB,EACpB,mBAAmB,EACnB,eAAe,GAChB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EACL,IAAI,EACJ,QAAQ,EACR,aAAa,EACb,UAAU,EACV,YAAY,EACZ,WAAW,EACX,WAAW,EACX,WAAW,GACZ,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,SAAS,EACT,aAAa,EACb,UAAU,EACV,WAAW,EACX,cAAc,EACd,aAAa,EACb,WAAW,EACX,SAAS,EACT,QAAQ,EACR,kBAAkB,GACnB,MAAM,uBAAuB,CAAA;AAG9B,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACnE,YAAY,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAG1E,OAAO,EACL,aAAa,EACb,aAAa,EACb,WAAW,EACX,SAAS,EACT,aAAa,EACb,UAAU,EACV,QAAQ,EACR,SAAS,GACV,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,UAAU,EACV,YAAY,EACZ,cAAc,EACd,aAAa,EACb,UAAU,EACV,cAAc,EACd,cAAc,EACd,eAAe,EACf,WAAW,EACX,UAAU,GACX,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAC7D,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAG3F,OAAO,EACL,OAAO,EACP,OAAO,EACP,OAAO,EACP,eAAe,EACf,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,OAAO,GACR,MAAM,wBAAwB,CAAA;AAC/B,YAAY,EACV,oBAAoB,EACpB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,kBAAkB,EAClB,WAAW,EACX,WAAW,EACX,YAAY,GACb,MAAM,wBAAwB,CAAA;AAE/B,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAGnG,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAGhF,cAAc,yBAAyB,CAAA;AAGvC,OAAO,EACL,eAAe,EACf,QAAQ,EACR,UAAU,EACV,aAAa,EACb,MAAM,EACN,YAAY,EACZ,QAAQ,GACT,MAAM,2BAA2B,CAAA;AAClC,YAAY,EACV,YAAY,EACZ,SAAS,EACT,mBAAmB,EACnB,WAAW,EACX,eAAe,EACf,KAAK,EACL,SAAS,EACT,WAAW,EACX,KAAK,EACL,aAAa,EACb,YAAY,EACZ,eAAe,EACf,SAAS,GACV,MAAM,2BAA2B,CAAA;AAClC,YAAY,EACV,YAAY,EACZ,aAAa,EACb,UAAU,EACV,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,UAAU,EACV,aAAa,EACb,WAAW,GACZ,MAAM,iCAAiC,CAAA;AACxC,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAA;AACpE,YAAY,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAGpD,YAAY,EACV,WAAW,EACX,YAAY,EACZ,eAAe,EACf,KAAK,EACL,WAAW,EACX,MAAM,GACP,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ export { SelectionScope, useSelection } from './selection/index.js';
25
25
  // ─── window (§10) ────────────────────────────────────────────────────────────
26
26
  export { useCloseGuard, useTargetNode, useViewsFor, useWindow, useWindowList, useWindows, ViewHost, ViewStage, } from './window/index.js';
27
27
  // ─── intents (§11) ───────────────────────────────────────────────────────────
28
- export { useIntent, useSendIntent } from './intents/index.js';
28
+ export { useEmitIntent, useIntent, useSendIntent } from './intents/index.js';
29
29
  // ─── application — the OS plane (§12) ────────────────────────────────────────
30
30
  export { AppHost, AppLogo, useApps, useCurrentSpace, useOpenApp, useRoles, useSpace, useSpaces, useUser, } from './application/index.js';
31
31
  // ─── error taxonomy (§1.4) — guards + classes, re-exported from kernel-client ─
@@ -0,0 +1,17 @@
1
+ import type { IntentRegistry } from '@astrale-os/shell';
2
+ import type { IntentPayload } from './envelope.js';
3
+ /** Fire-and-forget intent sender returned by {@link useEmitIntent}. */
4
+ export interface EmitIntent {
5
+ <K extends keyof IntentRegistry>(name: K, payload: IntentPayload<K>,
6
+ /** `to`: `'parent'` (reserved) or a child windowId. Default: the parent, if any. */
7
+ opts?: {
8
+ to?: 'parent' | string;
9
+ }): void;
10
+ }
11
+ /**
12
+ * Return a stable sender for intents that do not need a reply. Emitted messages
13
+ * carry no correlation id, so listeners can handle navigation and notifications
14
+ * without retaining a pending request.
15
+ */
16
+ export declare function useEmitIntent(): EmitIntent;
17
+ //# sourceMappingURL=emit-intent.hook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-intent.hook.d.ts","sourceRoot":"","sources":["../../src/intents/emit-intent.hook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAIvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAMlD,uEAAuE;AACvE,MAAM,WAAW,UAAU;IACzB,CAAC,CAAC,SAAS,MAAM,cAAc,EAC7B,IAAI,EAAE,CAAC,EACP,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC;IACzB,oFAAoF;IACpF,IAAI,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAA;KAAE,GAChC,IAAI,CAAA;CACR;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,UAAU,CAgB1C"}
@@ -0,0 +1,22 @@
1
+ import { useCallback } from 'react';
2
+ import { useShell } from '../session/shell.hook.js';
3
+ import { intentMessage } from './envelope.js';
4
+ import { resolveIntentTarget } from './resolve-intent-target.js';
5
+ /**
6
+ * Return a stable sender for intents that do not need a reply. Emitted messages
7
+ * carry no correlation id, so listeners can handle navigation and notifications
8
+ * without retaining a pending request.
9
+ */
10
+ export function useEmitIntent() {
11
+ const shell = useShell();
12
+ return useCallback((name, payload, opts) => {
13
+ const target = resolveIntentTarget(shell, opts?.to);
14
+ if (target.kind === 'invalid')
15
+ throw new Error(`useEmitIntent: ${target.reason}`);
16
+ const message = intentMessage(name, payload);
17
+ if (target.kind === 'parent')
18
+ shell.parent.send(message);
19
+ else
20
+ shell.children.send(target.windowId, message);
21
+ }, [shell]);
22
+ }
@@ -1,5 +1,7 @@
1
1
  export { useIntent } from './listen-intent.hook.js';
2
2
  export type { IntentCtx } from './listen-intent.hook.js';
3
+ export { useEmitIntent } from './emit-intent.hook.js';
4
+ export type { EmitIntent } from './emit-intent.hook.js';
3
5
  export { useSendIntent } from './send-intent.hook.js';
4
6
  export type { SendIntent } from './send-intent.hook.js';
5
7
  export type { IntentPayload, IntentReply } from './envelope.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/intents/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AACnD,YAAY,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,YAAY,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AACvD,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/intents/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AACnD,YAAY,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,YAAY,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,YAAY,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AACvD,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA"}
@@ -1,3 +1,4 @@
1
1
  // intents — API §11 (useIntent, useSendIntent). See SHELL_REACT_API.md.
2
2
  export { useIntent } from './listen-intent.hook.js';
3
+ export { useEmitIntent } from './emit-intent.hook.js';
3
4
  export { useSendIntent } from './send-intent.hook.js';
@@ -0,0 +1,12 @@
1
+ import type { Shell } from '@astrale-os/shell';
2
+ export type IntentTarget = {
3
+ readonly kind: 'parent';
4
+ } | {
5
+ readonly kind: 'child';
6
+ readonly windowId: string;
7
+ } | {
8
+ readonly kind: 'invalid';
9
+ readonly reason: string;
10
+ };
11
+ export declare function resolveIntentTarget(shell: Shell, to: string | undefined): IntentTarget;
12
+ //# sourceMappingURL=resolve-intent-target.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-intent-target.d.ts","sourceRoot":"","sources":["../../src/intents/resolve-intent-target.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAE9C,MAAM,MAAM,YAAY,GACpB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACrD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAEzD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,CAYtF"}
@@ -0,0 +1,13 @@
1
+ export function resolveIntentTarget(shell, to) {
2
+ if (to === undefined) {
3
+ return shell.parent !== null
4
+ ? { kind: 'parent' }
5
+ : { kind: 'invalid', reason: 'no target — pass opts.to (a child windowId) from a root shell' };
6
+ }
7
+ if (to === 'parent') {
8
+ return shell.parent !== null
9
+ ? { kind: 'parent' }
10
+ : { kind: 'invalid', reason: "opts.to 'parent' but this shell has no parent channel" };
11
+ }
12
+ return { kind: 'child', windowId: to };
13
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"send-intent.hook.d.ts","sourceRoot":"","sources":["../../src/intents/send-intent.hook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,cAAc,EAAS,MAAM,mBAAmB,CAAA;AAW7E,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAK/D,0DAA0D;AAC1D,MAAM,WAAW,UAAU;IACzB,CAAC,CAAC,SAAS,MAAM,cAAc,EAC7B,IAAI,EAAE,CAAC,EACP,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC;IACzB,oFAAoF;IACpF,IAAI,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAA;KAAE,GAChC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;CAClC;AAqBD,wBAAgB,aAAa,IAAI,UAAU,CAiE1C"}
1
+ {"version":3,"file":"send-intent.hook.d.ts","sourceRoot":"","sources":["../../src/intents/send-intent.hook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAWtE,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAM/D,0DAA0D;AAC1D,MAAM,WAAW,UAAU;IACzB,CAAC,CAAC,SAAS,MAAM,cAAc,EAC7B,IAAI,EAAE,CAAC,EACP,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC;IACzB,oFAAoF;IACpF,IAAI,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAA;KAAE,GAChC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;CAClC;AAED,wBAAgB,aAAa,IAAI,UAAU,CAiE1C"}
@@ -8,19 +8,7 @@
8
8
  import { useCallback, useEffect, useRef } from 'react';
9
9
  import { useShell } from '../session/shell.hook.js';
10
10
  import { INTENT_REPLY, NOOP, intentMessage } from './envelope.js';
11
- function resolveTarget(shell, to) {
12
- if (to === undefined) {
13
- return shell.parent !== null
14
- ? { kind: 'parent' }
15
- : { kind: 'invalid', reason: 'no target — pass opts.to (a child windowId) from a root shell' };
16
- }
17
- if (to === 'parent') {
18
- return shell.parent !== null
19
- ? { kind: 'parent' }
20
- : { kind: 'invalid', reason: "opts.to 'parent' but this shell has no parent channel" };
21
- }
22
- return { kind: 'child', windowId: to };
23
- }
11
+ import { resolveIntentTarget } from './resolve-intent-target.js';
24
12
  export function useSendIntent() {
25
13
  const shell = useShell();
26
14
  // Unmount settlers for in-flight sends, so a pending reply resolves on teardown.
@@ -34,7 +22,7 @@ export function useSendIntent() {
34
22
  };
35
23
  }, []);
36
24
  return useCallback((name, payload, opts) => {
37
- const target = resolveTarget(shell, opts?.to);
25
+ const target = resolveIntentTarget(shell, opts?.to);
38
26
  if (target.kind === 'invalid') {
39
27
  return Promise.reject(new Error(`useSendIntent: ${target.reason}`));
40
28
  }
@@ -4,7 +4,7 @@
4
4
  * `createSchemaBoundView` per (session, schema) so a binding — and the BoundNodes
5
5
  * it mints — keep referential identity across renders. Each binding is wrapped so
6
6
  * every BoundNode it hands back is schema-tagged (the traversal hooks read it
7
- * back) and its `id tree path` recorded (the write capability's index).
7
+ * back) and its raw node recorded (the write capability's index).
8
8
  */
9
9
  import type { FnMap, NodeRefInput, SchemaBoundView } from '@astrale-os/kernel-client';
10
10
  import type { GraphMemory } from '@astrale-os/kernel-client/store';
@@ -1 +1 @@
1
- {"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../../src/schema/binding.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AACrF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAA;AAElE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAIpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAQ5D;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EACzC,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,CAAC,EACT,MAAM,EAAE,WAAW,GAClB,eAAe,CAAC,CAAC,EAAE,KAAK,CAAC,CAa3B;AAyCD,YAAY,EAAE,YAAY,EAAE,CAAA"}
1
+ {"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../../src/schema/binding.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AACrF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAA;AAElE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAIpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAQ5D;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EACzC,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,CAAC,EACT,MAAM,EAAE,WAAW,GAClB,eAAe,CAAC,CAAC,EAAE,KAAK,CAAC,CAa3B;AAmGD,YAAY,EAAE,YAAY,EAAE,CAAA"}
@@ -1,6 +1,6 @@
1
1
  import { createSchemaBoundView } from '@astrale-os/kernel-client';
2
2
  import { tagSchema } from './tag.js';
3
- import { recordPath, writableFor } from './write.js';
3
+ import { recordNode, writableFor } from './write.js';
4
4
  /** One tagged binding per (kernel, schema) — shared across the whole tree. */
5
5
  const bindingCache = new WeakMap();
6
6
  /**
@@ -18,38 +18,82 @@ export function bindingFor(kernel, schema, memory) {
18
18
  const cached = bySchema.get(schema);
19
19
  if (cached !== undefined)
20
20
  return cached;
21
- const { writable, pathIndex } = writableFor(kernel, memory);
22
- const binding = tagBinding(createSchemaBoundView(writable, schema), schema, pathIndex);
21
+ const { writable, nodeIndex } = writableFor(kernel, memory);
22
+ const binding = tagBinding(createSchemaBoundView(writable, schema), schema, nodeIndex);
23
23
  bySchema.set(schema, binding);
24
24
  return binding;
25
25
  }
26
26
  /**
27
27
  * Wrap a binding so `bind`/`bindAny`/`as` outputs carry a schema tag, and every
28
- * bound wire node's `id tree path` is recorded (the write cap's index); the
28
+ * bound wire node is recorded with its class and path (the write cap's index); the
29
29
  * class handles and every other member pass straight through.
30
30
  */
31
- function tagBinding(binding, schema, pathIndex) {
31
+ function tagBinding(binding, schema, nodeIndex) {
32
+ let taggedClasses;
32
33
  const handler = {
33
34
  get: (target, prop, receiver) => {
34
35
  const value = Reflect.get(target, prop, receiver);
36
+ if (prop === 'node' || prop === 'create') {
37
+ return async (...args) => tagBoundResult(await value(...args), schema, nodeIndex);
38
+ }
35
39
  if (prop === 'bind') {
36
40
  return (type, node, opts) => {
37
- recordPath(pathIndex, node);
41
+ recordNode(nodeIndex, node);
38
42
  return tagSchema(value(type, node, opts), schema);
39
43
  };
40
44
  }
41
45
  if (prop === 'bindAny') {
42
46
  return (node) => {
43
- recordPath(pathIndex, node);
47
+ recordNode(nodeIndex, node);
44
48
  return tagSchema(value(node), schema);
45
49
  };
46
50
  }
51
+ if (prop === 'classes') {
52
+ taggedClasses ??= tagClassHandles(value, schema, nodeIndex);
53
+ return taggedClasses;
54
+ }
47
55
  if (prop === 'as') {
48
56
  const as = value;
49
- return (credential) => tagBinding(as(credential), schema, pathIndex);
57
+ return (credential) => tagBinding(as(credential), schema, nodeIndex);
58
+ }
59
+ if (prop === 'withSchema') {
60
+ return (next) => tagBinding(value(next), next, nodeIndex);
50
61
  }
51
62
  return value;
52
63
  },
53
64
  };
54
65
  return new Proxy(binding, handler);
55
66
  }
67
+ function tagClassHandles(classes, schema, nodeIndex) {
68
+ const handles = new Map();
69
+ return new Proxy(classes, {
70
+ get(target, prop, receiver) {
71
+ const cached = handles.get(prop);
72
+ if (cached !== undefined)
73
+ return cached;
74
+ const handle = Reflect.get(target, prop, receiver);
75
+ if (typeof handle !== 'function')
76
+ return handle;
77
+ const tagged = new Proxy(handle, {
78
+ get(handleTarget, member, handleReceiver) {
79
+ const value = Reflect.get(handleTarget, member, handleReceiver);
80
+ if (member !== 'get')
81
+ return value;
82
+ return async (...args) => tagBoundResult(await value(...args), schema, nodeIndex);
83
+ },
84
+ });
85
+ handles.set(prop, tagged);
86
+ return tagged;
87
+ },
88
+ });
89
+ }
90
+ function tagBoundResult(result, schema, nodeIndex) {
91
+ const nodes = Array.isArray(result) ? result : [result];
92
+ for (const node of nodes) {
93
+ if (node !== null && typeof node === 'object') {
94
+ recordNode(nodeIndex, node);
95
+ tagSchema(node, schema);
96
+ }
97
+ }
98
+ return result;
99
+ }
@@ -1,4 +1,5 @@
1
1
  import type { GraphMemory } from '@astrale-os/kernel-client/store';
2
+ import type { Node } from '@astrale-os/kernel-core';
2
3
  import type { SessionView } from '../auth/act-as.context.js';
3
4
  /**
4
5
  * One memory-backed writable kernel per (effective) session view, plus the
@@ -7,9 +8,9 @@ import type { SessionView } from '../auth/act-as.context.js';
7
8
  */
8
9
  export interface WritableKernel {
9
10
  readonly writable: SessionView;
10
- readonly pathIndex: Map<string, string>;
11
+ readonly nodeIndex: Map<string, Node>;
11
12
  }
12
13
  export declare function writableFor(kernel: SessionView, memory: GraphMemory): WritableKernel;
13
- /** Record a bound node's `id tree path` so the write cap can address it. */
14
- export declare function recordPath(pathIndex: Map<string, string>, node: unknown): void;
14
+ /** Record a bound node's raw value so the write cap retains its class and tree path. */
15
+ export declare function recordNode(nodeIndex: Map<string, Node>, node: unknown): void;
15
16
  //# sourceMappingURL=write.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../src/schema/write.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAA;AAElE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAE5D;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAA;IAC9B,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACxC;AAGD,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,GAAG,cAAc,CAQpF;AAkDD,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAK9E"}
1
+ {"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../src/schema/write.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAA;AAClE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAA;AAEnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAE5D;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAA;IAC9B,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;CACtC;AAGD,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,GAAG,cAAc,CAQpF;AAqDD,wFAAwF;AACxF,wBAAgB,UAAU,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAO5E"}
@@ -2,8 +2,8 @@ const writableCache = new WeakMap();
2
2
  export function writableFor(kernel, memory) {
3
3
  let entry = writableCache.get(kernel);
4
4
  if (entry === undefined) {
5
- const pathIndex = new Map();
6
- entry = { writable: withMemoryWrites(kernel, memory, pathIndex), pathIndex };
5
+ const nodeIndex = new Map();
6
+ entry = { writable: withMemoryWrites(kernel, memory, nodeIndex), nodeIndex };
7
7
  writableCache.set(kernel, entry);
8
8
  }
9
9
  return entry;
@@ -11,41 +11,46 @@ export function writableFor(kernel, memory) {
11
11
  /**
12
12
  * Wrap a session view so `updateNode`/`createNode` route through the memory's
13
13
  * optimistic write shorthands. `BoundNode.update` addresses a node by `@<id>`,
14
- * but the memory indexes entities by tree path — so an `@<id>` ref is translated
15
- * through `pathIndex` (populated at bind time). A ref that is NOT indexed (a node
16
- * that was never read through a hook) falls back to the raw kernel-client floor:
14
+ * but the memory needs the node's class and tree path — so an `@<id>` ref is translated
15
+ * through `nodeIndex` (populated at bind time). A ref that is NOT indexed (a node
16
+ * that was never bound) falls back to the raw kernel-client floor:
17
17
  * it still dispatches, just without the overlay — matching the imperative floor.
18
18
  */
19
- function withMemoryWrites(kernel, memory, pathIndex) {
19
+ function withMemoryWrites(kernel, memory, nodeIndex) {
20
20
  const base = kernel;
21
21
  const rawUpdate = base.updateNode.bind(base);
22
22
  return extendCapabilities(kernel, {
23
23
  updateNode: (cls, ref, props) => {
24
- const path = treePathFor(pathIndex, ref);
25
- return path !== null ? memory.update(path, props) : rawUpdate(cls, ref, props);
24
+ const node = indexedNode(nodeIndex, ref);
25
+ return node !== null ? memory.update(node, props) : rawUpdate(cls, ref, props);
26
26
  },
27
27
  createNode: (cls, at, props) => memory.create(cls, at, props),
28
28
  // Credential rebind stays on the raw kernel-client floor (§6 note).
29
29
  as: (cred) => base.as(cred),
30
30
  });
31
31
  }
32
- /** Clone the complete session capability object before overlaying memory writes. */
32
+ /** Overlay memory writes without materializing a possibly virtual session surface. */
33
33
  function extendCapabilities(base, extension) {
34
- const result = Object.create(Object.getPrototypeOf(base));
35
- Object.defineProperties(result, Object.getOwnPropertyDescriptors(base));
36
- Object.defineProperties(result, Object.getOwnPropertyDescriptors(extension));
37
- return result;
34
+ return new Proxy(base, {
35
+ get(target, prop, receiver) {
36
+ return Object.prototype.hasOwnProperty.call(extension, prop)
37
+ ? Reflect.get(extension, prop, extension)
38
+ : Reflect.get(target, prop, receiver);
39
+ },
40
+ });
38
41
  }
39
42
  /** The tree path an `@<id>` write ref maps to, via the bind-time index; else null. */
40
- function treePathFor(pathIndex, ref) {
43
+ function indexedNode(nodeIndex, ref) {
41
44
  if (typeof ref !== 'string' || !ref.startsWith('@'))
42
45
  return null;
43
- return pathIndex.get(ref.slice(1)) ?? null;
46
+ return nodeIndex.get(ref.slice(1)) ?? null;
44
47
  }
45
- /** Record a bound node's `id tree path` so the write cap can address it. */
46
- export function recordPath(pathIndex, node) {
47
- const n = node;
48
+ /** Record a bound node's raw value so the write cap retains its class and tree path. */
49
+ export function recordNode(nodeIndex, node) {
50
+ const wrapped = node;
51
+ const candidate = wrapped?.raw !== null && typeof wrapped?.raw === 'object' ? wrapped.raw : node;
52
+ const n = candidate;
48
53
  if (n !== null && typeof n.id === 'string' && typeof n.path?.raw === 'string') {
49
- pathIndex.set(n.id, n.path.raw);
54
+ nodeIndex.set(n.id, candidate);
50
55
  }
51
56
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/shell-react",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Astrale shell-react — the React face of the shell (provider, hooks, view components)",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@astrale-domains/shell-schema": ">=0.3.2 <1.0.0",
29
- "@astrale-os/kernel-client": ">=0.4.6 <1.0.0",
29
+ "@astrale-os/kernel-client": ">=0.4.7 <1.0.0",
30
30
  "@astrale-os/kernel-core": ">=0.7.5 <1.0.0",
31
31
  "@astrale-os/kernel-dsl": ">=0.1.4 <1.0.0",
32
32
  "jose": "^6.2.3",
@@ -44,7 +44,7 @@
44
44
  "react-dom": "^19.2.0",
45
45
  "typescript": "~6.0.1-rc",
46
46
  "vitest": "^3.2.4",
47
- "@astrale-os/shell": "0.3.3"
47
+ "@astrale-os/shell": "0.3.4"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@astrale-os/shell": ">=0.2.0 <1.0.0",
package/src/index.ts CHANGED
@@ -117,8 +117,14 @@ export type {
117
117
  } from './window/index.js'
118
118
 
119
119
  // ─── intents (§11) ───────────────────────────────────────────────────────────
120
- export { useIntent, useSendIntent } from './intents/index.js'
121
- export type { IntentCtx, IntentPayload, IntentReply, SendIntent } from './intents/index.js'
120
+ export { useEmitIntent, useIntent, useSendIntent } from './intents/index.js'
121
+ export type {
122
+ EmitIntent,
123
+ IntentCtx,
124
+ IntentPayload,
125
+ IntentReply,
126
+ SendIntent,
127
+ } from './intents/index.js'
122
128
 
123
129
  // ─── application — the OS plane (§12) ────────────────────────────────────────
124
130
  export {
@@ -0,0 +1,42 @@
1
+ import type { IntentRegistry } from '@astrale-os/shell'
2
+
3
+ import { useCallback } from 'react'
4
+
5
+ import type { IntentPayload } from './envelope.js'
6
+
7
+ import { useShell } from '../session/shell.hook.js'
8
+ import { intentMessage } from './envelope.js'
9
+ import { resolveIntentTarget } from './resolve-intent-target.js'
10
+
11
+ /** Fire-and-forget intent sender returned by {@link useEmitIntent}. */
12
+ export interface EmitIntent {
13
+ <K extends keyof IntentRegistry>(
14
+ name: K,
15
+ payload: IntentPayload<K>,
16
+ /** `to`: `'parent'` (reserved) or a child windowId. Default: the parent, if any. */
17
+ opts?: { to?: 'parent' | string },
18
+ ): void
19
+ }
20
+
21
+ /**
22
+ * Return a stable sender for intents that do not need a reply. Emitted messages
23
+ * carry no correlation id, so listeners can handle navigation and notifications
24
+ * without retaining a pending request.
25
+ */
26
+ export function useEmitIntent(): EmitIntent {
27
+ const shell = useShell()
28
+ return useCallback(
29
+ <K extends keyof IntentRegistry>(
30
+ name: K,
31
+ payload: IntentPayload<K>,
32
+ opts?: { to?: 'parent' | string },
33
+ ): void => {
34
+ const target = resolveIntentTarget(shell, opts?.to)
35
+ if (target.kind === 'invalid') throw new Error(`useEmitIntent: ${target.reason}`)
36
+ const message = intentMessage(name, payload)
37
+ if (target.kind === 'parent') shell.parent!.send(message)
38
+ else shell.children.send(target.windowId, message)
39
+ },
40
+ [shell],
41
+ )
42
+ }
@@ -1,6 +1,8 @@
1
1
  // intents — API §11 (useIntent, useSendIntent). See SHELL_REACT_API.md.
2
2
  export { useIntent } from './listen-intent.hook.js'
3
3
  export type { IntentCtx } from './listen-intent.hook.js'
4
+ export { useEmitIntent } from './emit-intent.hook.js'
5
+ export type { EmitIntent } from './emit-intent.hook.js'
4
6
  export { useSendIntent } from './send-intent.hook.js'
5
7
  export type { SendIntent } from './send-intent.hook.js'
6
8
  export type { IntentPayload, IntentReply } from './envelope.js'
@@ -0,0 +1,20 @@
1
+ import type { Shell } from '@astrale-os/shell'
2
+
3
+ export type IntentTarget =
4
+ | { readonly kind: 'parent' }
5
+ | { readonly kind: 'child'; readonly windowId: string }
6
+ | { readonly kind: 'invalid'; readonly reason: string }
7
+
8
+ export function resolveIntentTarget(shell: Shell, to: string | undefined): IntentTarget {
9
+ if (to === undefined) {
10
+ return shell.parent !== null
11
+ ? { kind: 'parent' }
12
+ : { kind: 'invalid', reason: 'no target — pass opts.to (a child windowId) from a root shell' }
13
+ }
14
+ if (to === 'parent') {
15
+ return shell.parent !== null
16
+ ? { kind: 'parent' }
17
+ : { kind: 'invalid', reason: "opts.to 'parent' but this shell has no parent channel" }
18
+ }
19
+ return { kind: 'child', windowId: to }
20
+ }
@@ -1,4 +1,4 @@
1
- import type { IntentMessage, IntentRegistry, Shell } from '@astrale-os/shell'
1
+ import type { IntentMessage, IntentRegistry } from '@astrale-os/shell'
2
2
 
3
3
  /**
4
4
  * useSendIntent (API §11) — a stable typed sender. Each call attaches a
@@ -13,6 +13,7 @@ import type { IntentPayload, IntentReply } from './envelope.js'
13
13
 
14
14
  import { useShell } from '../session/shell.hook.js'
15
15
  import { INTENT_REPLY, NOOP, intentMessage } from './envelope.js'
16
+ import { resolveIntentTarget } from './resolve-intent-target.js'
16
17
 
17
18
  /** The typed sender returned by {@link useSendIntent}. */
18
19
  export interface SendIntent {
@@ -24,25 +25,6 @@ export interface SendIntent {
24
25
  ): Promise<IntentReply<K> | void>
25
26
  }
26
27
 
27
- type Target =
28
- | { readonly kind: 'parent' }
29
- | { readonly kind: 'child'; readonly windowId: string }
30
- | { readonly kind: 'invalid'; readonly reason: string }
31
-
32
- function resolveTarget(shell: Shell, to: string | undefined): Target {
33
- if (to === undefined) {
34
- return shell.parent !== null
35
- ? { kind: 'parent' }
36
- : { kind: 'invalid', reason: 'no target — pass opts.to (a child windowId) from a root shell' }
37
- }
38
- if (to === 'parent') {
39
- return shell.parent !== null
40
- ? { kind: 'parent' }
41
- : { kind: 'invalid', reason: "opts.to 'parent' but this shell has no parent channel" }
42
- }
43
- return { kind: 'child', windowId: to }
44
- }
45
-
46
28
  export function useSendIntent(): SendIntent {
47
29
  const shell = useShell()
48
30
  // Unmount settlers for in-flight sends, so a pending reply resolves on teardown.
@@ -61,7 +43,7 @@ export function useSendIntent(): SendIntent {
61
43
  payload: IntentPayload<K>,
62
44
  opts?: { to?: 'parent' | string },
63
45
  ): Promise<IntentReply<K> | void> => {
64
- const target = resolveTarget(shell, opts?.to)
46
+ const target = resolveIntentTarget(shell, opts?.to)
65
47
  if (target.kind === 'invalid') {
66
48
  return Promise.reject(new Error(`useSendIntent: ${target.reason}`))
67
49
  }
@@ -4,7 +4,7 @@
4
4
  * `createSchemaBoundView` per (session, schema) so a binding — and the BoundNodes
5
5
  * it mints — keep referential identity across renders. Each binding is wrapped so
6
6
  * every BoundNode it hands back is schema-tagged (the traversal hooks read it
7
- * back) and its `id tree path` recorded (the write capability's index).
7
+ * back) and its raw node recorded (the write capability's index).
8
8
  */
9
9
  import type { FnMap, NodeRefInput, SchemaBoundView } from '@astrale-os/kernel-client'
10
10
  import type { GraphMemory } from '@astrale-os/kernel-client/store'
@@ -16,7 +16,7 @@ import { createSchemaBoundView } from '@astrale-os/kernel-client'
16
16
  import type { SessionView } from '../auth/act-as.context.js'
17
17
 
18
18
  import { tagSchema } from './tag.js'
19
- import { recordPath, writableFor } from './write.js'
19
+ import { recordNode, writableFor } from './write.js'
20
20
 
21
21
  /** One tagged binding per (kernel, schema) — shared across the whole tree. */
22
22
  const bindingCache = new WeakMap<object, WeakMap<Schema, unknown>>()
@@ -40,28 +40,37 @@ export function bindingFor<S extends Schema>(
40
40
  const cached = bySchema.get(schema)
41
41
  if (cached !== undefined) return cached as SchemaBoundView<S, FnMap>
42
42
 
43
- const { writable, pathIndex } = writableFor(kernel, memory)
44
- const binding = tagBinding(createSchemaBoundView(writable, schema), schema, pathIndex)
43
+ const { writable, nodeIndex } = writableFor(kernel, memory)
44
+ const binding = tagBinding(createSchemaBoundView(writable, schema), schema, nodeIndex)
45
45
  bySchema.set(schema, binding)
46
46
  return binding
47
47
  }
48
48
 
49
49
  /**
50
50
  * Wrap a binding so `bind`/`bindAny`/`as` outputs carry a schema tag, and every
51
- * bound wire node's `id tree path` is recorded (the write cap's index); the
51
+ * bound wire node is recorded with its class and path (the write cap's index); the
52
52
  * class handles and every other member pass straight through.
53
53
  */
54
54
  function tagBinding<S extends Schema>(
55
55
  binding: SchemaBoundView<S, FnMap>,
56
56
  schema: S,
57
- pathIndex: Map<string, string>,
57
+ nodeIndex: Map<string, Node>,
58
58
  ): SchemaBoundView<S, FnMap> {
59
+ let taggedClasses: unknown
59
60
  const handler: ProxyHandler<SchemaBoundView<S, FnMap>> = {
60
61
  get: (target, prop, receiver) => {
61
62
  const value = Reflect.get(target, prop, receiver) as unknown
63
+ if (prop === 'node' || prop === 'create') {
64
+ return async (...args: unknown[]) =>
65
+ tagBoundResult(
66
+ await (value as (...callArgs: unknown[]) => Promise<unknown>)(...args),
67
+ schema,
68
+ nodeIndex,
69
+ )
70
+ }
62
71
  if (prop === 'bind') {
63
72
  return (type: string, node: Node, opts?: unknown) => {
64
- recordPath(pathIndex, node)
73
+ recordNode(nodeIndex, node)
65
74
  return tagSchema(
66
75
  (value as (t: string, n: Node, o?: unknown) => unknown)(type, node, opts),
67
76
  schema,
@@ -70,14 +79,22 @@ function tagBinding<S extends Schema>(
70
79
  }
71
80
  if (prop === 'bindAny') {
72
81
  return (node: Node) => {
73
- recordPath(pathIndex, node)
82
+ recordNode(nodeIndex, node)
74
83
  return tagSchema((value as (n: Node) => unknown)(node), schema)
75
84
  }
76
85
  }
86
+ if (prop === 'classes') {
87
+ taggedClasses ??= tagClassHandles(value as object, schema, nodeIndex)
88
+ return taggedClasses
89
+ }
77
90
  if (prop === 'as') {
78
91
  const as = value as SchemaBoundView<S, FnMap>['as']
79
92
  return (credential: Parameters<typeof as>[0]) =>
80
- tagBinding(as(credential), schema, pathIndex)
93
+ tagBinding(as(credential), schema, nodeIndex)
94
+ }
95
+ if (prop === 'withSchema') {
96
+ return <S2 extends Schema>(next: S2) =>
97
+ tagBinding((value as SchemaBoundView<S, FnMap>['withSchema'])(next), next, nodeIndex)
81
98
  }
82
99
  return value
83
100
  },
@@ -85,4 +102,45 @@ function tagBinding<S extends Schema>(
85
102
  return new Proxy(binding, handler)
86
103
  }
87
104
 
105
+ function tagClassHandles<S extends Schema>(
106
+ classes: object,
107
+ schema: S,
108
+ nodeIndex: Map<string, Node>,
109
+ ): object {
110
+ const handles = new Map<PropertyKey, unknown>()
111
+ return new Proxy(classes, {
112
+ get(target, prop, receiver) {
113
+ const cached = handles.get(prop)
114
+ if (cached !== undefined) return cached
115
+ const handle = Reflect.get(target, prop, receiver) as unknown
116
+ if (typeof handle !== 'function') return handle
117
+ const tagged = new Proxy(handle, {
118
+ get(handleTarget, member, handleReceiver) {
119
+ const value = Reflect.get(handleTarget, member, handleReceiver) as unknown
120
+ if (member !== 'get') return value
121
+ return async (...args: unknown[]) =>
122
+ tagBoundResult(
123
+ await (value as (...callArgs: unknown[]) => Promise<unknown>)(...args),
124
+ schema,
125
+ nodeIndex,
126
+ )
127
+ },
128
+ })
129
+ handles.set(prop, tagged)
130
+ return tagged
131
+ },
132
+ })
133
+ }
134
+
135
+ function tagBoundResult<R>(result: R, schema: Schema, nodeIndex: Map<string, Node>): R {
136
+ const nodes = Array.isArray(result) ? result : [result]
137
+ for (const node of nodes) {
138
+ if (node !== null && typeof node === 'object') {
139
+ recordNode(nodeIndex, node)
140
+ tagSchema(node, schema)
141
+ }
142
+ }
143
+ return result
144
+ }
145
+
88
146
  export type { NodeRefInput }
@@ -9,6 +9,7 @@
9
9
  import type { CredentialInput } from '@astrale-os/kernel-client'
10
10
  import type { PathLike, Properties } from '@astrale-os/kernel-client/graph'
11
11
  import type { GraphMemory } from '@astrale-os/kernel-client/store'
12
+ import type { Node } from '@astrale-os/kernel-core'
12
13
 
13
14
  import type { SessionView } from '../auth/act-as.context.js'
14
15
 
@@ -19,15 +20,15 @@ import type { SessionView } from '../auth/act-as.context.js'
19
20
  */
20
21
  export interface WritableKernel {
21
22
  readonly writable: SessionView
22
- readonly pathIndex: Map<string, string>
23
+ readonly nodeIndex: Map<string, Node>
23
24
  }
24
25
  const writableCache = new WeakMap<object, WritableKernel>()
25
26
 
26
27
  export function writableFor(kernel: SessionView, memory: GraphMemory): WritableKernel {
27
28
  let entry = writableCache.get(kernel)
28
29
  if (entry === undefined) {
29
- const pathIndex = new Map<string, string>()
30
- entry = { writable: withMemoryWrites(kernel, memory, pathIndex), pathIndex }
30
+ const nodeIndex = new Map<string, Node>()
31
+ entry = { writable: withMemoryWrites(kernel, memory, nodeIndex), nodeIndex }
31
32
  writableCache.set(kernel, entry)
32
33
  }
33
34
  return entry
@@ -36,15 +37,15 @@ export function writableFor(kernel: SessionView, memory: GraphMemory): WritableK
36
37
  /**
37
38
  * Wrap a session view so `updateNode`/`createNode` route through the memory's
38
39
  * optimistic write shorthands. `BoundNode.update` addresses a node by `@<id>`,
39
- * but the memory indexes entities by tree path — so an `@<id>` ref is translated
40
- * through `pathIndex` (populated at bind time). A ref that is NOT indexed (a node
41
- * that was never read through a hook) falls back to the raw kernel-client floor:
40
+ * but the memory needs the node's class and tree path — so an `@<id>` ref is translated
41
+ * through `nodeIndex` (populated at bind time). A ref that is NOT indexed (a node
42
+ * that was never bound) falls back to the raw kernel-client floor:
42
43
  * it still dispatches, just without the overlay — matching the imperative floor.
43
44
  */
44
45
  function withMemoryWrites(
45
46
  kernel: SessionView,
46
47
  memory: GraphMemory,
47
- pathIndex: Map<string, string>,
48
+ nodeIndex: Map<string, Node>,
48
49
  ): SessionView {
49
50
  const base = kernel as unknown as {
50
51
  updateNode(cls: PathLike, ref: PathLike, props: Properties): Promise<void>
@@ -54,8 +55,8 @@ function withMemoryWrites(
54
55
  const rawUpdate = base.updateNode.bind(base)
55
56
  return extendCapabilities(kernel, {
56
57
  updateNode: (cls: PathLike, ref: PathLike, props: Properties): Promise<void> => {
57
- const path = treePathFor(pathIndex, ref)
58
- return path !== null ? memory.update(path, props) : rawUpdate(cls, ref, props)
58
+ const node = indexedNode(nodeIndex, ref)
59
+ return node !== null ? memory.update(node, props) : rawUpdate(cls, ref, props)
59
60
  },
60
61
  createNode: (cls: PathLike, at: PathLike, props?: Properties): Promise<string> =>
61
62
  memory.create(cls, at, props),
@@ -64,27 +65,32 @@ function withMemoryWrites(
64
65
  })
65
66
  }
66
67
 
67
- /** Clone the complete session capability object before overlaying memory writes. */
68
+ /** Overlay memory writes without materializing a possibly virtual session surface. */
68
69
  function extendCapabilities<Base extends object, Extension extends object>(
69
70
  base: Base,
70
71
  extension: Extension,
71
72
  ): Base & Extension {
72
- const result = Object.create(Object.getPrototypeOf(base)) as Base & Extension
73
- Object.defineProperties(result, Object.getOwnPropertyDescriptors(base))
74
- Object.defineProperties(result, Object.getOwnPropertyDescriptors(extension))
75
- return result
73
+ return new Proxy(base as Base & Extension, {
74
+ get(target, prop, receiver) {
75
+ return Object.prototype.hasOwnProperty.call(extension, prop)
76
+ ? Reflect.get(extension, prop, extension)
77
+ : Reflect.get(target, prop, receiver)
78
+ },
79
+ })
76
80
  }
77
81
 
78
82
  /** The tree path an `@<id>` write ref maps to, via the bind-time index; else null. */
79
- function treePathFor(pathIndex: Map<string, string>, ref: PathLike): string | null {
83
+ function indexedNode(nodeIndex: Map<string, Node>, ref: PathLike): Node | null {
80
84
  if (typeof ref !== 'string' || !ref.startsWith('@')) return null
81
- return pathIndex.get(ref.slice(1)) ?? null
85
+ return nodeIndex.get(ref.slice(1)) ?? null
82
86
  }
83
87
 
84
- /** Record a bound node's `id tree path` so the write cap can address it. */
85
- export function recordPath(pathIndex: Map<string, string>, node: unknown): void {
86
- const n = node as { id?: unknown; path?: { raw?: unknown } } | null
88
+ /** Record a bound node's raw value so the write cap retains its class and tree path. */
89
+ export function recordNode(nodeIndex: Map<string, Node>, node: unknown): void {
90
+ const wrapped = node as { raw?: unknown } | null
91
+ const candidate = wrapped?.raw !== null && typeof wrapped?.raw === 'object' ? wrapped.raw : node
92
+ const n = candidate as { id?: unknown; path?: { raw?: unknown } } | null
87
93
  if (n !== null && typeof n.id === 'string' && typeof n.path?.raw === 'string') {
88
- pathIndex.set(n.id, n.path.raw)
94
+ nodeIndex.set(n.id, candidate as Node)
89
95
  }
90
96
  }