Packages are independently versioned, independently published, and never share source at runtime. "Interfacing" therefore always means one of three narrow channels — never a direct property or method call across a package boundary.
| Channel | Direction | Used for |
|---|---|---|
| Slot | Parent → Child | Structural composition (Declaration) |
| Event | Child → Owner | Asking the owner to do something ("ask your parent") |
| Library | Any → Library | Shared services (file system, settings, logging) |
Covered in Declarative Structure. A parent decides what to render inside a child's named slot; neither reaches into the other's shadow tree.
A child never reaches up to its owner; it dispatches a bubbling, composed custom event and
whoever owns it decides. @tce-v3/desktop-protocol is the single definition of these events:
for each one a name constant, a zod schema for the detail, the type derived from the schema, and
a request_* helper that dispatches it correctly. Elements that take part in the desktop
depend on this SDK package; elements that don't, don't load it — that is why it is not part of
@tce-v3/common (Package boundaries).
Requests (child → owner, dispatched with the helper):
| Event | Helper | Detail | Handled by |
|---|---|---|---|
tce.window.request |
request_window_action |
{ action: fullscreen \| maximize \| restore \| minimize \| close \| resize \| retitle, width?, height?, title? } |
the owning window |
tce.context-menu.request |
request_context_menu |
{ x, y, items: [{ label, action, disabled? }] } |
the surface owner (desktop) |
tce.desktop.open-request |
request_open |
{ identifier, attributes?, title?, width?, height? } |
the nearest desktop |
tce.desktop.create-icon-request |
request_create_icon |
{ identifier, label, icon_src?, attributes?, title?, width?, height? } |
the nearest desktop |
tce.desktop.layout-request |
request_layout |
{ on_layout } — a query, answered through the callback (below) |
the nearest desktop |
tce.desktop.composition-request |
request_composition |
{ on_composition } — a query: what fills each switchable slot, with attributes |
the nearest desktop |
tce.desktop.configure-request |
request_configure |
{ target: icon \| component, enabled?, attributes? } — change an icon's boot attributes, or switch a component on/off and set its attributes |
the nearest desktop |
Notifications (dispatched by an implementation so an owner can react without a build-time dependency on that implementation):
| Event | Dispatched by | Meaning |
|---|---|---|
tce.window.closed / .minimized / .restored / .focused / .retitled |
a window | window lifecycle, for taskbars |
tce.icon.select / tce.icon.activate |
an icon | selection, activation (double-click/Enter) |
tce.desktop.icon-removed |
the desktop | an icon was removed for good |
request_open is how one element opens another: the desktop loads the package if needed,
creates the element, sets the given attributes, and wraps it in a window. Attribute names are
validated — lower-case, never on* — so a request cannot install inline event handlers.
request_create_icon with attributes asks for a shortcut: an icon that, when activated, opens
the package with exactly those attributes set (and the given title, width, height), the way
request_open would. The desktop validates the attributes with the same rule. This is how a file
in the file system gets an icon: the explorer and the code editor request a shortcut to the file's
handler. An icon requested without attributes stands for the package itself; that is what the
package manager's "Add to Desktop" asks for.
The desktop remembers every requested icon, shortcut or not, across reloads, and offers to remove
it in its context menu ("Remove shortcut", or "Remove from desktop" for a package icon). The
icons the page itself declares (data-apps) are not remembered: the page sets them on every load.
Because the events are composed and bubble, a request from inside nested shells (a desktop in a
window of another desktop) passes every owner on its way out. The first owner that handles a
request calls event.stopPropagation(), so exactly one owner acts; outer owners never see it.
An owner reads a detail with event_detail(event, schema) and ignores the event when it does
not match the schema. Nobody casts event as CustomEvent<Detail>; the lint rules forbid it
(Coding Style).
A request is fire-and-forget: the sender gets no acknowledgment. Two requests are different in
kind — queries: tce.desktop.layout-request (request_layout) and
tce.desktop.composition-request (request_composition). A query's detail carries a function
(on_layout, on_composition), and the owner that claims the query calls it synchronously,
exactly once, inside its handler. The layout query answers with a TceDesktopLayout:
the viewport rectangles of the desktop surface, the taskbar, every visible (not minimized)
window with its title, stacking order and an opaque stable id, and every icon with its label,
package identifier and id. When request_layout returns, the sender either has the answer or
knows that no desktop owns it, and behaves as if the desktop were empty:
const answer: { layout: TceDesktopLayout | null } = { layout: null };
request_layout(this.host, {
on_layout: (raw) => { answer.layout = parse_or_null(desktop_layout_schema, raw, 'desktop layout'); },
});
The answer is external input to the sender, so it is parsed like any other. The rectangles are
public geometry (getBoundingClientRect()) and data- state of elements the desktop composed
itself; nothing in the answer comes from inside another element's shadow tree, and the answer
never hands out element references. A query describes; whoever wants to change something
still sends a request (request_open for the icon it sits on, for instance). This is how a
desktop effect — a mascot in the desktop's effects slot (Declaration)
— knows where to walk without reaching into anything: it asks, every frame, and the desktop
describes.
The composition query answers with a TceDesktopComposition — for each switchable slot
(decoration, effects, task-bar-icons) the package names composed into it and the
attributes their elements are created with — and request_configure is the request that
changes it: switch a package on or off in a slot, set the attributes it is created with, or set
the boot attributes an icon opens its package with. The properties dialog (tce-inspector) is
built on nothing else: candidates come from the loader's bindings (meta.slots,
meta.attributes — Declaration: Declared Attributes), the
current state from the query, every change through the request.
A package that needs a shared capability loads a library package through the loader and calls
its functions only through a contract assertion, never a bare load():
const file_system = await TceRoot.package_loader().load(file_system_identifier);
const read = assert_contract<NotepadFileSystem['read']>(file_system, file_system_contracts.read, 'dll-tce-file-system');
const content = await read('/documents/todo.txt');
A whole-library version is too coarse: two consumers use different subsets of a library's functions, so pinning compatibility per library rejects good combinations and reports failures as "library X is wrong" instead of "the one function you use changed". Compatibility is checked per function:
interface TceFunctionContract {
name: string; // as published through `functions`/`load()`
version: TcePackageVersion; // this function's own version, independent of the package's
params?: readonly string[]; // documentation only
}
A library lists its exposed names in functions, publishes one contract per function through
contracts(), and exports the contract constants and the function types so a consumer can
vendor them:
export const dll_tce_settings_contracts = {
register_group: { name: 'register_group', version: { major: 1, minor: 0, patch: 0 } },
list_groups: { name: 'list_groups', version: { major: 1, minor: 0, patch: 0 } },
} satisfies Record<string, TceFunctionContract>;
export class DllTceSettings extends TcePackage {
public functions: Array<keyof this> = ['register_group', 'list_groups'];
public contract_list: TceFunctionContract[] = Object.values(dll_tce_settings_contracts);
public version(): TcePackageVersion { return { major: 0, minor: 1, patch: 0 }; }
public register_group(group: TceSettingsGroup): void {
group = parse_or_throw(settings_group_schema, group, 'settings group passed to register_group()');
// ...
}
}
Arguments to a library's public functions are external input: each one is parsed at the top of the function.
A consumer vendors the contracts it was built against — copies the constants it uses into
src/contracts/<library>.contract.ts together with the library's identifier — the way a
lockfile pins a dependency, instead of reading them live off whatever is installed:
// src/contracts/dll-tce-file-system.contract.ts
import { TceFunctionContract } from '@tce-v3/common';
import type { DllTceFileSystem } from 'dll-tce-file-system'; // types only: never bundled
export const file_system_identifier = { namespace: 'tobidot', name: 'dll-tce-file-system' };
export const file_system_contracts = {
exists: { name: 'exists', version: { major: 1, minor: 0, patch: 0 } },
read: { name: 'read', version: { major: 1, minor: 0, patch: 0 } },
} satisfies Record<string, TceFunctionContract>;
export interface NotepadFileSystem {
exists: DllTceFileSystem['exists'];
read: DllTceFileSystem['read'];
}
At boot — inside init(), never lazily on first call (Lifecycle) —
the consumer asserts every vendored contract against the installed package. assert_contract
looks up the name in the installed package's contracts(), checks the installed version
against the vendored one with caret-range matching,
and returns the live function typed as requested — or throws a precise error:
dll-tce-file-system: 'read' contract mismatch — need >=1.2.0, installed package provides 1.1.3
A failure happens at boot with this message, never as undefined is not a function deep inside
application logic later.
The contract file is also the type source: the library's own types are imported as types only
(import type), so the library is never bundled into the consumer, and the generic parameter of
assert_contract<Fn> is supplied from that same file. The developer gets full type-checking on
the asserted function from the exact file that drives the runtime check — one artifact, nothing
to drift.
A library may be optional for a consumer (the notepad works on localStorage without the file
system). The consumer then checks has_binding() — after DOMContentLoaded when the document
is still loading, since a later <script> tag may still bind it — and degrades explicitly,
logging why. A library that is required and never binds fails init() through the loader's
bounded with_binding timeout, with the message naming the missing identifier; an element that
cannot work without it renders that failure in its own shadow DOM instead of staying blank.
The loader memoizes one instance per resolved package, so two elements loading
dll-tce-file-system share one running copy. Per-function contracts make the outcomes precise:
read at 1.2.0, B asserts write at 2.0.0. Both succeed
independently.read at major 1, the installed package
provides major 2. A's assertion fails naming exactly read and both versions; B is unaffected.dll-tce-file-system keeps a registry of which element opens which file types. An element
registers itself when its package boots, so "Open with" knows it before any window was opened:
file_system.register_file_handler({
element: 'tce-notepad',
namespace: 'tobidot', // the handler's own publisher — required, never defaulted
title: 'Notepad',
extensions: ['.txt', '.md'],
attribute: 'init-file-path', // receives the absolute path
width: 480,
height: 360,
});
The opener (tce-explorer) looks up handlers with get_file_handlers(extension) and asks its
desktop with request_open, passing the path through the registered attribute. The opened
element only reacts to that attribute; it never receives a function call from the opener.
request_* helpers and parse them with
event_detail.event.stopPropagation() once you have handled a request as its owner.assert_contract at boot, against a
vendored contract.import type only.init(), not connectedCallback.request_layout.load(name) and cast the result; assert its contract.CustomEvent for window control; extend window_action_schema.