@procedures/splay 0.0.0-stage → 0.2.0

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +21 -0
  3. package/README.md +408 -2
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/components/define.d.ts +134 -0
  6. package/dist/components/define.d.ts.map +1 -0
  7. package/dist/components/define.js +486 -0
  8. package/dist/components/define.js.map +1 -0
  9. package/dist/components/index.d.ts +39 -0
  10. package/dist/components/index.d.ts.map +1 -0
  11. package/dist/components/index.js +38 -0
  12. package/dist/components/index.js.map +1 -0
  13. package/dist/components/types.d.ts +264 -0
  14. package/dist/components/types.d.ts.map +1 -0
  15. package/dist/components/types.js +46 -0
  16. package/dist/components/types.js.map +1 -0
  17. package/dist/hydrate.d.ts +126 -0
  18. package/dist/hydrate.d.ts.map +1 -0
  19. package/dist/hydrate.js +226 -0
  20. package/dist/hydrate.js.map +1 -0
  21. package/dist/index.d.ts +40 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +53 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/module.d.ts +24 -0
  26. package/dist/module.d.ts.map +1 -0
  27. package/dist/module.js +68 -0
  28. package/dist/module.js.map +1 -0
  29. package/dist/register.d.ts +7 -0
  30. package/dist/register.d.ts.map +1 -0
  31. package/dist/register.js +9 -0
  32. package/dist/register.js.map +1 -0
  33. package/dist/registry.d.ts +60 -0
  34. package/dist/registry.d.ts.map +1 -0
  35. package/dist/registry.js +191 -0
  36. package/dist/registry.js.map +1 -0
  37. package/dist/streaming.d.ts +94 -0
  38. package/dist/streaming.d.ts.map +1 -0
  39. package/dist/streaming.js +422 -0
  40. package/dist/streaming.js.map +1 -0
  41. package/dist/types.d.ts +85 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +7 -0
  44. package/dist/types.js.map +1 -0
  45. package/package.json +76 -3
  46. package/src/components/define.ts +600 -0
  47. package/src/components/index.ts +78 -0
  48. package/src/components/types.ts +357 -0
  49. package/src/hydrate.ts +355 -0
  50. package/src/index.ts +110 -0
  51. package/src/module.ts +93 -0
  52. package/src/register.ts +11 -0
  53. package/src/registry.ts +257 -0
  54. package/src/streaming.ts +554 -0
  55. package/src/types.ts +114 -0
package/src/index.ts ADDED
@@ -0,0 +1,110 @@
1
+ /**
2
+ * client-splay
3
+ *
4
+ * Bridge between splay's rendering model and client's procedure system.
5
+ *
6
+ * This package enables:
7
+ * - Using client procedures as splay renderers
8
+ * - Streaming component updates via procedures
9
+ * - Hydrating serializable descriptors to framework components
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { call } from "client";
14
+ * import {
15
+ * createClientRegistry,
16
+ * createReactHydrate,
17
+ * } from "@procedures/splay";
18
+ *
19
+ * // Create a registry backed by client procedures
20
+ * const registry = createClientRegistry(call, {
21
+ * namespace: "ui",
22
+ * });
23
+ *
24
+ * // Hydrate descriptors to React components
25
+ * const hydrate = createReactHydrate(React, {
26
+ * components: { "user-card": UserCard },
27
+ * });
28
+ *
29
+ * // Render a component
30
+ * const descriptor = await registry.get("user-card")!(ctx);
31
+ * const element = hydrate(descriptor);
32
+ * ```
33
+ */
34
+
35
+ export * from "./module.js";
36
+
37
+ // =============================================================================
38
+ // Types
39
+ // =============================================================================
40
+
41
+ export type {
42
+ // Render context
43
+ RenderContext,
44
+
45
+ // Registry interfaces
46
+ ComponentRenderer,
47
+ StreamingComponentRenderer,
48
+ Registry,
49
+ StreamingRegistry,
50
+
51
+ // Registry options
52
+ ClientRegistryOptions,
53
+ StreamingRegistryOptions,
54
+
55
+ // Hydration
56
+ ComponentMap,
57
+ HydrateOptions,
58
+ } from "./types.js";
59
+
60
+ // =============================================================================
61
+ // Registry
62
+ // =============================================================================
63
+
64
+ export {
65
+ createClientRegistry,
66
+ createCheckedRegistry,
67
+ createRenderContext,
68
+ type ProcedureCaller,
69
+ } from "./registry.js";
70
+
71
+ // =============================================================================
72
+ // Streaming
73
+ // =============================================================================
74
+
75
+ export {
76
+ createStreamingRegistry,
77
+ createDualRegistry,
78
+ mergeStreams,
79
+ throttleStream,
80
+ debounceStream,
81
+ type StreamingProcedureCaller,
82
+ type DualRegistry,
83
+ } from "./streaming.js";
84
+
85
+ // =============================================================================
86
+ // Hydration
87
+ // =============================================================================
88
+
89
+ export {
90
+ createHydrate,
91
+ createReactHydrate,
92
+ walkDescriptor,
93
+ transformDescriptor,
94
+ findInDescriptor,
95
+ getUsedTypes,
96
+ validateDescriptor,
97
+ serializeDescriptor,
98
+ parseDescriptor,
99
+ compactDescriptor,
100
+ type CreateElement,
101
+ type ReactHydrateOptions,
102
+ } from "./hydrate.js";
103
+
104
+ // =============================================================================
105
+ // Components
106
+ // =============================================================================
107
+
108
+ // The components: serializable UI descriptors made by procedures. They moved here from
109
+ // "@procedures/client/components" (roadmap 3.4: this is the UI package of the ecosystem).
110
+ export * from "./components/index.js";
package/src/module.ts ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Module (no side effects): the bridge procedures as data, for `createRegistry()`.
3
+ *
4
+ * Procedure Registration
5
+ *
6
+ * Registers client-splay bridge procedures with the client system.
7
+ * This file is referenced by package.json's client.procedures field.
8
+ */
9
+
10
+ import { createProcedure, outputSchema, defineModule, PURE_EFFECTS, type AnyProcedure, type ProcedureModule, PROCEDURE_REGISTRY, registerModules, type ProcedureRegistry } from "@procedures/client";
11
+
12
+ // =============================================================================
13
+ // Types
14
+ // =============================================================================
15
+
16
+ interface BridgeInfo {
17
+ name: string;
18
+ version: string;
19
+ description: string;
20
+ }
21
+
22
+ interface HealthCheck {
23
+ status: string;
24
+ timestamp: string;
25
+ }
26
+
27
+ // =============================================================================
28
+ // Schemas
29
+ // =============================================================================
30
+
31
+ const voidSchema = outputSchema<void>();
32
+ const bridgeInfoSchema = outputSchema<BridgeInfo>();
33
+ const healthCheckSchema = outputSchema<HealthCheck>();
34
+
35
+ // =============================================================================
36
+ // Bridge Procedures
37
+ // =============================================================================
38
+
39
+ /**
40
+ * Get package info.
41
+ */
42
+ const infoProcedure = createProcedure()
43
+ .path(["splay", "bridge", "info"])
44
+ .input(voidSchema)
45
+ .output(bridgeInfoSchema)
46
+ .meta({ description: "Get client-splay bridge information", effects: PURE_EFFECTS })
47
+ .handler(() => ({
48
+ name: "@procedures/splay",
49
+ version: "1.0.0",
50
+ description: "Bridge between splay and client",
51
+ }))
52
+ .build();
53
+
54
+ /**
55
+ * Health check for the bridge.
56
+ */
57
+ const healthProcedure = createProcedure()
58
+ .path(["splay", "bridge", "health"])
59
+ .input(voidSchema)
60
+ .output(healthCheckSchema)
61
+ .meta({ description: "Health check for client-splay bridge", effects: PURE_EFFECTS })
62
+ .handler(() => ({
63
+ status: "healthy",
64
+ timestamp: new Date().toISOString(),
65
+ }))
66
+ .build();
67
+
68
+ // =============================================================================
69
+ // Registration
70
+ // =============================================================================
71
+
72
+ /**
73
+ * The module of this package, for `createRegistry()`: its procedures as data. Importing this
74
+ * file registers nothing. (`register.ts` registers the same procedures in the global registry.)
75
+ */
76
+ export const splayModule: ProcedureModule = defineModule("@procedures/splay", [
77
+ infoProcedure as AnyProcedure,
78
+ healthProcedure as AnyProcedure,
79
+ ]);
80
+
81
+ export default splayModule;
82
+
83
+ /**
84
+ * This function registers the procedures of @procedures/splay, and of the modules that it requires, in
85
+ * a registry. The default is the global registry. Importing this file registers nothing: the
86
+ * `./register` entry starts this function (ARCHITECTURE-PROPOSALS-2026-10, P9).
87
+ */
88
+ export function registerSplayProcedures(registry: ProcedureRegistry = PROCEDURE_REGISTRY): void {
89
+ registerModules(registry, [splayModule], { override: true });
90
+ }
91
+
92
+ /** @deprecated Use `registerSplayProcedures`. The old name stays for one version. */
93
+ export const registerBridge: (registry?: ProcedureRegistry) => void = registerSplayProcedures;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Importing this file registers the procedures of @procedures/splay, and of the modules that it
3
+ * requires, in the global registry: `import "@procedures/splay/register"`. The package root and the
4
+ * `./module` entry register nothing (ARCHITECTURE-PROPOSALS-2026-10, P9).
5
+ */
6
+
7
+ import { registerSplayProcedures } from "./module.js";
8
+
9
+ export * from "./module.js";
10
+
11
+ registerSplayProcedures();
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Client Registry
3
+ *
4
+ * Creates a splay-compatible Registry that delegates to client procedures.
5
+ * This enables splay's rendering engine to use client's procedure system
6
+ * for component resolution and execution.
7
+ */
8
+
9
+ import type { ComponentOutput, Size } from "./components/index.js";
10
+ import type {
11
+ Registry,
12
+ ComponentRenderer,
13
+ RenderContext,
14
+ ClientRegistryOptions,
15
+ } from "./types.js";
16
+
17
+ // =============================================================================
18
+ // Client Procedure Caller Type
19
+ // =============================================================================
20
+
21
+ /**
22
+ * Type for calling client procedures.
23
+ * This matches the shape of client's call function.
24
+ */
25
+ export interface ProcedureCaller {
26
+ <TResult>(path: string[], input: unknown): Promise<TResult>;
27
+ }
28
+
29
+ // =============================================================================
30
+ // Client Registry Implementation
31
+ // =============================================================================
32
+
33
+ /**
34
+ * Create a splay-compatible registry backed by client procedures.
35
+ *
36
+ * @param call - Client's procedure call function
37
+ * @param options - Registry options
38
+ * @returns Registry instance
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * import { call } from "client";
43
+ * import { createClientRegistry } from "@procedures/splay";
44
+ *
45
+ * const registry = createClientRegistry(call, {
46
+ * namespace: "ui",
47
+ * defaultSize: { width: 800, height: 600 },
48
+ * });
49
+ *
50
+ * // Now pass to splay's renderer
51
+ * const renderer = createRenderer({ registry });
52
+ * ```
53
+ */
54
+ export function createClientRegistry(
55
+ call: ProcedureCaller,
56
+ options: ClientRegistryOptions = {}
57
+ ): Registry {
58
+ const { namespace } = options;
59
+
60
+ /**
61
+ * Build the procedure path for a component type.
62
+ */
63
+ function buildPath(type: string): string[] {
64
+ const basePath = ["components"];
65
+ if (namespace) {
66
+ basePath.push(namespace);
67
+ }
68
+ basePath.push(type);
69
+ return basePath;
70
+ }
71
+
72
+ /**
73
+ * Create a renderer function for a component type.
74
+ */
75
+ function createRenderer(type: string): ComponentRenderer {
76
+ const procedurePath = buildPath(type);
77
+
78
+ return async (ctx: RenderContext): Promise<ComponentOutput> => {
79
+ // Build the input for the component procedure
80
+ const input = {
81
+ data: ctx.data,
82
+ size: ctx.size,
83
+ path: ctx.path,
84
+ depth: ctx.depth,
85
+ };
86
+
87
+ // Call the procedure
88
+ const result = await call<ComponentOutput>(procedurePath, input);
89
+ return result;
90
+ };
91
+ }
92
+
93
+ // Cache of created renderers
94
+ const rendererCache = new Map<string, ComponentRenderer>();
95
+
96
+ return {
97
+ /**
98
+ * Get a renderer for a component type.
99
+ * Creates and caches the renderer on first access.
100
+ */
101
+ get(type: string): ComponentRenderer | undefined {
102
+ // Check cache first
103
+ let renderer = rendererCache.get(type);
104
+ if (renderer) {
105
+ return renderer;
106
+ }
107
+
108
+ // Create and cache renderer
109
+ // Note: We always create a renderer - the procedure may or may not exist
110
+ // Errors will be thrown at call time if the procedure doesn't exist
111
+ renderer = createRenderer(type);
112
+ rendererCache.set(type, renderer);
113
+ return renderer;
114
+ },
115
+
116
+ /**
117
+ * Check if a renderer exists.
118
+ * Note: This always returns true since we lazily create renderers.
119
+ * Actual existence is checked at call time.
120
+ */
121
+ has(_type: string): boolean {
122
+ // We optimistically return true - actual check happens at call time
123
+ // This matches splay's expectation that has() is a quick check
124
+ return true;
125
+ },
126
+
127
+ /**
128
+ * Register a renderer (not supported - use client procedures).
129
+ */
130
+ register(type: string, _renderer: ComponentRenderer): void {
131
+ throw new Error(
132
+ `Cannot register renderers directly on ClientRegistry. ` +
133
+ `Define a component procedure at path: ${buildPath(type).join(".")}`
134
+ );
135
+ },
136
+ };
137
+ }
138
+
139
+ // =============================================================================
140
+ // Registry with Existence Check
141
+ // =============================================================================
142
+
143
+ /**
144
+ * Create a registry that can check procedure existence.
145
+ * Requires an additional hasProcedure function from client.
146
+ *
147
+ * @param call - Client's procedure call function
148
+ * @param hasProcedure - Function to check if a procedure exists
149
+ * @param options - Registry options
150
+ * @returns Registry with accurate has() checks
151
+ */
152
+ export function createCheckedRegistry(
153
+ call: ProcedureCaller,
154
+ hasProcedure: (path: string[]) => boolean,
155
+ options: ClientRegistryOptions = {}
156
+ ): Registry {
157
+ const baseRegistry = createClientRegistry(call, options);
158
+ const { namespace } = options;
159
+
160
+ function buildPath(type: string): string[] {
161
+ const basePath = ["components"];
162
+ if (namespace) {
163
+ basePath.push(namespace);
164
+ }
165
+ basePath.push(type);
166
+ return basePath;
167
+ }
168
+
169
+ return {
170
+ ...baseRegistry,
171
+
172
+ /**
173
+ * Check if a procedure exists for this component type.
174
+ */
175
+ has(type: string): boolean {
176
+ return hasProcedure(buildPath(type));
177
+ },
178
+ };
179
+ }
180
+
181
+ // =============================================================================
182
+ // Render Context Factory
183
+ // =============================================================================
184
+
185
+ /**
186
+ * Create a render context for calling component procedures.
187
+ *
188
+ * @param registry - The registry to use for child renders
189
+ * @param data - Data to render
190
+ * @param size - Available size
191
+ * @param path - Path in render tree
192
+ * @param depth - Current depth
193
+ * @returns RenderContext for component procedures
194
+ */
195
+ export function createRenderContext<TData>(
196
+ registry: Registry,
197
+ data: TData,
198
+ size: Size,
199
+ path: string = "root",
200
+ depth: number = 0
201
+ ): RenderContext<TData> {
202
+ return {
203
+ data,
204
+ size,
205
+ path,
206
+ depth,
207
+ render: async (childData: unknown, childSize: Size, childPath: string) => {
208
+ // Determine component type from data
209
+ const type = getComponentType(childData);
210
+ if (!type) {
211
+ throw new Error(`Cannot determine component type for data at ${childPath}`);
212
+ }
213
+
214
+ const renderer = registry.get(type);
215
+ if (!renderer) {
216
+ throw new Error(`No renderer found for component type: ${type}`);
217
+ }
218
+
219
+ // Create child context
220
+ const childCtx = createRenderContext(
221
+ registry,
222
+ childData,
223
+ childSize,
224
+ childPath,
225
+ depth + 1
226
+ );
227
+
228
+ return renderer(childCtx);
229
+ },
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Extract component type from data.
235
+ * Looks for a `type` or `__type__` property.
236
+ */
237
+ function getComponentType(data: unknown): string | null {
238
+ if (data === null || typeof data !== "object") {
239
+ return null;
240
+ }
241
+
242
+ const obj = data as Record<string, unknown>;
243
+
244
+ // Check for type property
245
+ const typeValue = obj["type"];
246
+ if (typeof typeValue === "string") {
247
+ return typeValue;
248
+ }
249
+
250
+ // Check for __type__ property (alternative convention)
251
+ const altTypeValue = obj["__type__"];
252
+ if (typeof altTypeValue === "string") {
253
+ return altTypeValue;
254
+ }
255
+
256
+ return null;
257
+ }