@zap-js/client 0.2.1 → 0.2.3

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.
@@ -2,8 +2,9 @@ import { execSync } from 'child_process';
2
2
  import { join, resolve } from 'path';
3
3
  import { existsSync, mkdirSync, copyFileSync, readdirSync, statSync, rmSync, writeFileSync } from 'fs';
4
4
  import { cliLogger } from '../utils/logger.js';
5
- import { resolveBinary, getPlatformIdentifier } from '../utils/binary-resolver.js';
5
+ import { resolveBinary, getPlatformIdentifier, resolveSpliceBinary } from '../utils/binary-resolver.js';
6
6
  import { validateBuildStructure } from '../utils/build-validator.js';
7
+ import { buildUserServerRelease } from '../utils/user-server.js';
7
8
  /**
8
9
  * Build for production
9
10
  */
@@ -63,6 +64,8 @@ export async function buildCommand(options) {
63
64
  // This happens AFTER frontend build so Vite doesn't overwrite it
64
65
  mkdirSync(join(outputDir, 'bin'), { recursive: true });
65
66
  await buildRust(outputDir, options);
67
+ // Step 4.5: Build user server and copy Splice binary (if available)
68
+ await buildUserServerAndSplice(outputDir);
66
69
  // Step 5: Create production config
67
70
  await createProductionConfig(outputDir, staticDir);
68
71
  // Step 6: Create build manifest
@@ -421,3 +424,32 @@ function getBinarySize(path) {
421
424
  return 'unknown';
422
425
  }
423
426
  }
427
+ /**
428
+ * Build user's Rust server and copy Splice binary to dist/bin/
429
+ */
430
+ async function buildUserServerAndSplice(outputDir) {
431
+ const projectDir = process.cwd();
432
+ // 1. Try to resolve pre-built Splice binary
433
+ const spliceBinary = resolveSpliceBinary(projectDir);
434
+ if (spliceBinary && existsSync(spliceBinary)) {
435
+ cliLogger.spinner('splice', 'Copying Splice binary...');
436
+ try {
437
+ const destBinary = join(outputDir, 'bin', 'splice');
438
+ copyFileSync(spliceBinary, destBinary);
439
+ execSync(`chmod +x "${destBinary}"`, { stdio: 'pipe' });
440
+ cliLogger.succeedSpinner('splice', 'Splice binary copied');
441
+ }
442
+ catch (error) {
443
+ cliLogger.warn('Failed to copy Splice binary');
444
+ }
445
+ }
446
+ else {
447
+ cliLogger.info('Splice binary not found (skipping)');
448
+ }
449
+ // 2. Build user server if it exists
450
+ const success = await buildUserServerRelease(projectDir, outputDir);
451
+ if (!success) {
452
+ // Warning already logged by buildUserServerRelease
453
+ return;
454
+ }
455
+ }
@@ -6,6 +6,7 @@ import { pathToFileURL } from 'url';
6
6
  import { findAvailablePort } from '../utils/port-finder.js';
7
7
  import { IpcServer } from '../../runtime/index.js';
8
8
  import { cliLogger } from '../utils/logger.js';
9
+ import { SpliceManager } from '../../dev-server/splice-manager.js';
9
10
  // Register tsx loader for TypeScript imports
10
11
  let tsxRegistered = false;
11
12
  async function ensureTsxRegistered() {
@@ -107,6 +108,31 @@ async function runProductionServer(binPath, options, workDir, prodConfig) {
107
108
  }
108
109
  // Generate unique socket path
109
110
  const socketPath = join(tmpdir(), `zap-prod-${Date.now()}-${Math.random().toString(36).substring(7)}.sock`);
111
+ // Check for Splice and user server binaries
112
+ let spliceManager = null;
113
+ const spliceBinPath = join(workDir, 'bin', 'splice');
114
+ const userServerBinPath = join(workDir, 'bin', 'server');
115
+ if (existsSync(spliceBinPath) && existsSync(userServerBinPath)) {
116
+ cliLogger.spinner('splice-prod', 'Starting Splice...');
117
+ const spliceSocketPath = join(tmpdir(), `splice-prod-${Date.now()}.sock`);
118
+ spliceManager = new SpliceManager({
119
+ spliceBinaryPath: spliceBinPath,
120
+ workerBinaryPath: userServerBinPath,
121
+ socketPath: spliceSocketPath,
122
+ maxConcurrency: 1024,
123
+ timeout: 30,
124
+ });
125
+ try {
126
+ await spliceManager.start();
127
+ cliLogger.succeedSpinner('splice-prod', 'Splice started');
128
+ }
129
+ catch (err) {
130
+ cliLogger.failSpinner('splice-prod', 'Splice failed to start');
131
+ const message = err instanceof Error ? err.message : String(err);
132
+ cliLogger.warn(`Continuing without Splice: ${message}`);
133
+ spliceManager = null;
134
+ }
135
+ }
110
136
  // Start IPC server for TypeScript handlers
111
137
  cliLogger.spinner('ipc', 'Starting IPC server...');
112
138
  const ipcServer = new IpcServer(socketPath);
@@ -131,6 +157,10 @@ async function runProductionServer(binPath, options, workDir, prodConfig) {
131
157
  },
132
158
  health_check_path: '/health',
133
159
  };
160
+ // Add Splice socket if available
161
+ if (spliceManager && spliceManager.isRunning()) {
162
+ zapConfig.splice_socket_path = spliceManager.getSocketPath();
163
+ }
134
164
  // Also check for static directory in workDir
135
165
  const staticDir = join(workDir, 'static');
136
166
  if (existsSync(staticDir) && zapConfig.static_files.length === 0) {
@@ -173,7 +203,7 @@ async function runProductionServer(binPath, options, workDir, prodConfig) {
173
203
  cliLogger.failSpinner('rpc', 'Failed to connect RPC client');
174
204
  const message = err instanceof Error ? err.message : String(err);
175
205
  cliLogger.error(message);
176
- cleanup(ipcServer, tempConfigPath);
206
+ cleanup(ipcServer, tempConfigPath, spliceManager);
177
207
  if (!rustProcess.killed) {
178
208
  rustProcess.kill();
179
209
  }
@@ -198,7 +228,7 @@ async function runProductionServer(binPath, options, workDir, prodConfig) {
198
228
  if (output.includes('error') || output.includes('Error')) {
199
229
  cliLogger.failSpinner('rust', 'Server failed to start');
200
230
  cliLogger.error(output);
201
- cleanup(ipcServer, tempConfigPath);
231
+ cleanup(ipcServer, tempConfigPath, spliceManager);
202
232
  process.exit(1);
203
233
  }
204
234
  }
@@ -206,13 +236,13 @@ async function runProductionServer(binPath, options, workDir, prodConfig) {
206
236
  });
207
237
  rustProcess.on('error', (err) => {
208
238
  cliLogger.failSpinner('rust', `Failed to start: ${err.message}`);
209
- cleanup(ipcServer, tempConfigPath);
239
+ cleanup(ipcServer, tempConfigPath, spliceManager);
210
240
  process.exit(1);
211
241
  });
212
242
  rustProcess.on('exit', (code) => {
213
243
  if (code !== 0 && code !== null) {
214
244
  cliLogger.error(`Server exited with code ${code}`);
215
- cleanup(ipcServer, tempConfigPath);
245
+ cleanup(ipcServer, tempConfigPath, spliceManager);
216
246
  process.exit(code);
217
247
  }
218
248
  });
@@ -234,7 +264,7 @@ async function runProductionServer(binPath, options, workDir, prodConfig) {
234
264
  // Stop IPC server
235
265
  await ipcServer.stop();
236
266
  // Cleanup temp config
237
- cleanup(null, tempConfigPath);
267
+ cleanup(null, tempConfigPath, spliceManager);
238
268
  // Force kill after timeout
239
269
  setTimeout(() => {
240
270
  if (!rustProcess.killed) {
@@ -379,7 +409,15 @@ function formatHandlerResponse(result) {
379
409
  /**
380
410
  * Cleanup temp files
381
411
  */
382
- function cleanup(ipcServer, configPath) {
412
+ function cleanup(ipcServer, configPath, spliceManager) {
413
+ if (spliceManager) {
414
+ try {
415
+ spliceManager.stop();
416
+ }
417
+ catch {
418
+ // Ignore cleanup errors
419
+ }
420
+ }
383
421
  if (ipcServer) {
384
422
  try {
385
423
  ipcServer.stop();
@@ -4,7 +4,7 @@
4
4
  * 2. Local bin/ directory in user's project (for development/custom builds)
5
5
  * 3. Returns null to trigger cargo build fallback
6
6
  */
7
- export declare function resolveBinary(binaryName: 'zap' | 'zap-codegen', projectDir?: string): string | null;
7
+ export declare function resolveBinary(binaryName: 'zap' | 'zap-codegen' | 'splice', projectDir?: string): string | null;
8
8
  /**
9
9
  * Detects both zap and zap-codegen binaries
10
10
  */
@@ -20,3 +20,12 @@ export declare function getPlatformIdentifier(): string;
20
20
  * Checks if a platform-specific package is installed
21
21
  */
22
22
  export declare function isPlatformPackageInstalled(): boolean;
23
+ /**
24
+ * Resolves the Splice binary using the same resolution strategy
25
+ *
26
+ * Resolution order:
27
+ * 1. Platform-specific npm package (@zap-js/darwin-arm64)
28
+ * 2. Local bin/ directory (user's project)
29
+ * 3. null (triggers cargo build fallback)
30
+ */
31
+ export declare function resolveSpliceBinary(projectDir?: string): string | null;
@@ -69,3 +69,14 @@ export function isPlatformPackageInstalled() {
69
69
  return false;
70
70
  }
71
71
  }
72
+ /**
73
+ * Resolves the Splice binary using the same resolution strategy
74
+ *
75
+ * Resolution order:
76
+ * 1. Platform-specific npm package (@zap-js/darwin-arm64)
77
+ * 2. Local bin/ directory (user's project)
78
+ * 3. null (triggers cargo build fallback)
79
+ */
80
+ export function resolveSpliceBinary(projectDir) {
81
+ return resolveBinary('splice', projectDir);
82
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Check if project has a server/Cargo.toml (user's Rust server)
3
+ */
4
+ export declare function hasUserServer(projectDir: string): boolean;
5
+ /**
6
+ * Build user's Rust server in development mode
7
+ * Returns path to built binary or null on failure
8
+ */
9
+ export declare function buildUserServer(projectDir: string, binaryName?: string): Promise<string | null>;
10
+ /**
11
+ * Build user's Rust server in release mode for production
12
+ */
13
+ export declare function buildUserServerRelease(projectDir: string, outputDir: string, binaryName?: string): Promise<boolean>;
@@ -0,0 +1,79 @@
1
+ import { existsSync, copyFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { execSync } from 'child_process';
4
+ import { cliLogger } from './logger.js';
5
+ /**
6
+ * Check if project has a server/Cargo.toml (user's Rust server)
7
+ */
8
+ export function hasUserServer(projectDir) {
9
+ const serverCargoToml = join(projectDir, 'server', 'Cargo.toml');
10
+ return existsSync(serverCargoToml);
11
+ }
12
+ /**
13
+ * Build user's Rust server in development mode
14
+ * Returns path to built binary or null on failure
15
+ */
16
+ export async function buildUserServer(projectDir, binaryName = 'server') {
17
+ const serverDir = join(projectDir, 'server');
18
+ if (!existsSync(join(serverDir, 'Cargo.toml'))) {
19
+ return null;
20
+ }
21
+ try {
22
+ cliLogger.spinner('user-server', 'Building Rust server...');
23
+ execSync('cargo build --bin ' + binaryName, {
24
+ cwd: serverDir,
25
+ stdio: 'pipe',
26
+ });
27
+ // Find the built binary
28
+ const targetDir = join(serverDir, 'target', 'debug');
29
+ const binaryPath = join(targetDir, binaryName);
30
+ if (existsSync(binaryPath)) {
31
+ cliLogger.succeedSpinner('user-server', 'User server built');
32
+ return binaryPath;
33
+ }
34
+ cliLogger.failSpinner('user-server', 'Binary not found after build');
35
+ return null;
36
+ }
37
+ catch (error) {
38
+ cliLogger.failSpinner('user-server', 'User server build failed');
39
+ if (error instanceof Error) {
40
+ cliLogger.error(error.message);
41
+ }
42
+ return null;
43
+ }
44
+ }
45
+ /**
46
+ * Build user's Rust server in release mode for production
47
+ */
48
+ export async function buildUserServerRelease(projectDir, outputDir, binaryName = 'server') {
49
+ const serverDir = join(projectDir, 'server');
50
+ if (!existsSync(join(serverDir, 'Cargo.toml'))) {
51
+ cliLogger.info('No user server found (skipping)');
52
+ return true; // Not an error
53
+ }
54
+ try {
55
+ cliLogger.spinner('user-server-release', 'Building user server (release mode)...');
56
+ execSync('cargo build --release --bin ' + binaryName, {
57
+ cwd: serverDir,
58
+ stdio: 'pipe',
59
+ });
60
+ // Copy binary to dist/bin/
61
+ const srcBinary = join(serverDir, 'target', 'release', binaryName);
62
+ const destBinary = join(outputDir, 'bin', binaryName);
63
+ if (existsSync(srcBinary)) {
64
+ copyFileSync(srcBinary, destBinary);
65
+ execSync(`chmod +x "${destBinary}"`, { stdio: 'pipe' });
66
+ cliLogger.succeedSpinner('user-server-release', 'User server built (release)');
67
+ return true;
68
+ }
69
+ cliLogger.failSpinner('user-server-release', 'Binary not found');
70
+ return false;
71
+ }
72
+ catch (error) {
73
+ cliLogger.failSpinner('user-server-release', 'Build failed');
74
+ if (error instanceof Error) {
75
+ cliLogger.error(error.message);
76
+ }
77
+ return false;
78
+ }
79
+ }
@@ -12,6 +12,8 @@ export interface DevServerConfig {
12
12
  binaryPath?: string;
13
13
  /** Path to pre-built zap-codegen binary */
14
14
  codegenBinaryPath?: string;
15
+ /** Path to pre-built splice binary */
16
+ spliceBinaryPath?: string;
15
17
  }
16
18
  interface ServerState {
17
19
  phase: 'starting' | 'building' | 'ready' | 'rebuilding' | 'error' | 'stopped';
@@ -49,6 +51,9 @@ export declare class DevServer extends EventEmitter {
49
51
  private socketPath;
50
52
  private currentRouteTree;
51
53
  private registeredHandlers;
54
+ private spliceManager;
55
+ private splicePath;
56
+ private userServerBinaryPath;
52
57
  private startTime;
53
58
  constructor(config: DevServerConfig);
54
59
  /**
@@ -99,6 +104,10 @@ export declare class DevServer extends EventEmitter {
99
104
  * Build Rust server configuration from routes
100
105
  */
101
106
  private buildRustConfig;
107
+ /**
108
+ * Start Splice supervisor for distributed Rust functions
109
+ */
110
+ private startSplice;
102
111
  /**
103
112
  * Restart Rust server (called when routes change)
104
113
  */
@@ -131,6 +140,22 @@ export declare class DevServer extends EventEmitter {
131
140
  * Handle config file changes
132
141
  */
133
142
  private handleConfigChange;
143
+ /**
144
+ * Handle user server (server/ Rust files) file changes
145
+ */
146
+ private handleUserServerChange;
147
+ /**
148
+ * Wait for Splice to detect binary change and reload
149
+ */
150
+ private waitForSpliceReload;
151
+ /**
152
+ * Run Splice codegen to generate TypeScript bindings
153
+ */
154
+ private runSpliceCodegen;
155
+ /**
156
+ * Find codegen binary in various locations
157
+ */
158
+ private findCodegenBinary;
134
159
  /**
135
160
  * Print the ready message
136
161
  */
@@ -3,6 +3,8 @@ import path from 'path';
3
3
  import { tmpdir } from 'os';
4
4
  import { pathToFileURL } from 'url';
5
5
  import { promises as fs } from 'fs';
6
+ import { spawn, execSync } from 'child_process';
7
+ import { existsSync } from 'fs';
6
8
  import { FileWatcher } from './watcher.js';
7
9
  import { RustBuilder } from './rust-builder.js';
8
10
  import { ViteProxy } from './vite-proxy.js';
@@ -12,6 +14,9 @@ import { RouteScannerRunner } from './route-scanner.js';
12
14
  import { ProcessManager, IpcServer } from '../runtime/index.js';
13
15
  import { initRpcClient } from '../runtime/rpc-client.js';
14
16
  import { cliLogger } from '../cli/utils/logger.js';
17
+ import { SpliceManager } from './splice-manager.js';
18
+ import { hasUserServer, buildUserServer } from '../cli/utils/user-server.js';
19
+ import { resolveSpliceBinary } from '../cli/utils/binary-resolver.js';
15
20
  // Register tsx loader for TypeScript imports
16
21
  // This must be called before any dynamic imports of .ts files
17
22
  let tsxRegistered = false;
@@ -52,6 +57,10 @@ export class DevServer extends EventEmitter {
52
57
  this.socketPath = '';
53
58
  this.currentRouteTree = null;
54
59
  this.registeredHandlers = new Map(); // handlerId -> filePath
60
+ // Splice components for distributed Rust functions
61
+ this.spliceManager = null;
62
+ this.splicePath = '';
63
+ this.userServerBinaryPath = null;
55
64
  // Timing
56
65
  this.startTime = 0;
57
66
  this.config = {
@@ -106,6 +115,7 @@ export class DevServer extends EventEmitter {
106
115
  setupEventHandlers() {
107
116
  // File watcher events
108
117
  this.watcher.on('rust', (event) => this.handleRustChange(event));
118
+ this.watcher.on('user-server', (event) => this.handleUserServerChange(event));
109
119
  this.watcher.on('typescript', (event) => this.handleTypeScriptChange(event));
110
120
  this.watcher.on('config', (event) => this.handleConfigChange(event));
111
121
  this.watcher.on('error', (err) => this.log('error', `Watcher error: ${err.message}`));
@@ -177,6 +187,11 @@ export class DevServer extends EventEmitter {
177
187
  // Phase 2.5: Scan routes
178
188
  const routeTree = await this.scanRoutes();
179
189
  this.currentRouteTree = routeTree;
190
+ // Phase 2.75: Check for user server and start Splice if available
191
+ const hasServer = hasUserServer(this.config.projectDir);
192
+ if (hasServer) {
193
+ await this.startSplice();
194
+ }
180
195
  // Phase 3: Start Rust HTTP server with IPC
181
196
  await this.startRustServer(routeTree);
182
197
  // Phase 4: Start other servers in parallel
@@ -210,7 +225,12 @@ export class DevServer extends EventEmitter {
210
225
  this.state.phase = 'stopped';
211
226
  cliLogger.newline();
212
227
  cliLogger.warn('Shutting down...');
213
- // Kill Rust server first (most important)
228
+ // Stop Splice first
229
+ if (this.spliceManager) {
230
+ await this.spliceManager.stop();
231
+ this.spliceManager = null;
232
+ }
233
+ // Kill Rust server (most important)
214
234
  if (this.processManager) {
215
235
  this.processManager.stop();
216
236
  this.processManager = null;
@@ -473,7 +493,7 @@ export class DevServer extends EventEmitter {
473
493
  * Build Rust server configuration from routes
474
494
  */
475
495
  buildRustConfig(routes) {
476
- return {
496
+ const config = {
477
497
  port: this.config.rustPort,
478
498
  hostname: '127.0.0.1',
479
499
  ipc_socket_path: this.socketPath,
@@ -487,6 +507,56 @@ export class DevServer extends EventEmitter {
487
507
  health_check_path: '/health',
488
508
  metrics_path: '/metrics',
489
509
  };
510
+ // Add Splice socket if available
511
+ if (this.splicePath && this.spliceManager?.isRunning()) {
512
+ config.splice_socket_path = this.splicePath;
513
+ }
514
+ return config;
515
+ }
516
+ /**
517
+ * Start Splice supervisor for distributed Rust functions
518
+ */
519
+ async startSplice() {
520
+ cliLogger.spinner('splice', 'Starting Splice supervisor...');
521
+ try {
522
+ // 1. Resolve Splice binary
523
+ const spliceBinaryPath = this.config.spliceBinaryPath ||
524
+ resolveSpliceBinary(this.config.projectDir);
525
+ if (!spliceBinaryPath) {
526
+ cliLogger.warn('Splice binary not found (skipping distributed functions)');
527
+ return;
528
+ }
529
+ // 2. Build user server
530
+ const userServerPath = await buildUserServer(this.config.projectDir);
531
+ if (!userServerPath) {
532
+ cliLogger.warn('User server build failed (skipping Splice)');
533
+ return;
534
+ }
535
+ this.userServerBinaryPath = userServerPath;
536
+ // 3. Generate socket path for Splice
537
+ this.splicePath = path.join(tmpdir(), `splice-dev-${Date.now()}-${Math.random().toString(36).substring(7)}.sock`);
538
+ // 4. Create and start Splice manager
539
+ this.spliceManager = new SpliceManager({
540
+ spliceBinaryPath,
541
+ workerBinaryPath: userServerPath,
542
+ socketPath: this.splicePath,
543
+ maxConcurrency: 1024,
544
+ timeout: 30,
545
+ watchPaths: [path.join(this.config.projectDir, 'server')],
546
+ });
547
+ await this.spliceManager.start();
548
+ cliLogger.succeedSpinner('splice', `Splice ready on ${this.splicePath}`);
549
+ // Generate initial TypeScript bindings from Splice exports
550
+ cliLogger.spinner('splice-codegen', 'Generating TypeScript bindings...');
551
+ await this.runSpliceCodegen();
552
+ cliLogger.succeedSpinner('splice-codegen', 'Splice bindings generated');
553
+ }
554
+ catch (err) {
555
+ cliLogger.failSpinner('splice', 'Failed to start Splice');
556
+ const message = err instanceof Error ? err.message : String(err);
557
+ this.log('error', `Splice error: ${message}`);
558
+ // Don't throw - continue without Splice
559
+ }
490
560
  }
491
561
  /**
492
562
  * Restart Rust server (called when routes change)
@@ -600,6 +670,126 @@ export class DevServer extends EventEmitter {
600
670
  await this.viteProxy.restart();
601
671
  }
602
672
  }
673
+ /**
674
+ * Handle user server (server/ Rust files) file changes
675
+ */
676
+ async handleUserServerChange(event) {
677
+ const relativePath = path.relative(this.config.projectDir, event.path);
678
+ cliLogger.newline();
679
+ cliLogger.info(`[user-server:${event.type}] ${relativePath}`);
680
+ // Only proceed if Splice is running
681
+ if (!this.spliceManager?.isRunning()) {
682
+ this.log('debug', 'Splice not running, skipping user server rebuild');
683
+ return;
684
+ }
685
+ cliLogger.spinner('user-server-rebuild', 'Rebuilding user server...');
686
+ const rebuildStart = Date.now();
687
+ try {
688
+ // Step 1: Rebuild user server binary
689
+ const newBinaryPath = await buildUserServer(this.config.projectDir);
690
+ if (!newBinaryPath) {
691
+ cliLogger.failSpinner('user-server-rebuild', 'User server build failed');
692
+ this.hotReloadServer.notifyError('User server compilation failed');
693
+ return;
694
+ }
695
+ this.userServerBinaryPath = newBinaryPath;
696
+ const duration = ((Date.now() - rebuildStart) / 1000).toFixed(2);
697
+ cliLogger.succeedSpinner('user-server-rebuild', `User server rebuilt (${duration}s)`);
698
+ // Step 2: Wait for Splice to detect binary change and reload worker
699
+ this.log('info', 'Waiting for Splice to reload worker...');
700
+ await this.waitForSpliceReload();
701
+ // Step 3: Regenerate TypeScript bindings from updated exports
702
+ if (this.splicePath) {
703
+ cliLogger.spinner('splice-codegen', 'Regenerating Splice bindings...');
704
+ await this.runSpliceCodegen();
705
+ cliLogger.succeedSpinner('splice-codegen', 'TypeScript bindings updated');
706
+ }
707
+ // Step 4: Signal browser to reload
708
+ this.hotReloadServer.reload('rust', [relativePath]);
709
+ this.state.phase = 'ready';
710
+ }
711
+ catch (err) {
712
+ cliLogger.failSpinner('user-server-rebuild', 'Rebuild failed');
713
+ const message = err instanceof Error ? err.message : String(err);
714
+ this.log('error', `User server rebuild error: ${message}`);
715
+ this.hotReloadServer.notifyError(`User server error: ${message}`);
716
+ this.state.phase = 'error';
717
+ }
718
+ }
719
+ /**
720
+ * Wait for Splice to detect binary change and reload
721
+ */
722
+ async waitForSpliceReload() {
723
+ // Splice ReloadManager detects binary hash change and auto-reloads
724
+ // Give it time to complete the reload cycle
725
+ await new Promise((resolve) => setTimeout(resolve, 1000));
726
+ }
727
+ /**
728
+ * Run Splice codegen to generate TypeScript bindings
729
+ */
730
+ async runSpliceCodegen() {
731
+ if (!this.splicePath || !this.spliceManager?.isRunning()) {
732
+ throw new Error('Splice not running');
733
+ }
734
+ const codegenBinary = await this.findCodegenBinary();
735
+ if (!codegenBinary) {
736
+ throw new Error('Codegen binary not found');
737
+ }
738
+ return new Promise((resolve, reject) => {
739
+ const args = ['--splice-socket', this.splicePath, '--output-dir', './src/api'];
740
+ this.log('debug', `Running codegen: ${codegenBinary} ${args.join(' ')}`);
741
+ const proc = spawn(codegenBinary, args, {
742
+ cwd: this.config.projectDir,
743
+ stdio: ['ignore', 'pipe', 'pipe'],
744
+ });
745
+ let stderr = '';
746
+ proc.stdout?.on('data', (data) => {
747
+ this.log('debug', `[codegen] ${data.toString().trim()}`);
748
+ });
749
+ proc.stderr?.on('data', (data) => {
750
+ stderr += data.toString();
751
+ this.log('warn', `[codegen] ${data.toString().trim()}`);
752
+ });
753
+ proc.on('close', (code) => {
754
+ if (code === 0) {
755
+ this.log('info', 'Splice codegen completed successfully');
756
+ resolve();
757
+ }
758
+ else {
759
+ reject(new Error(`Codegen exited with code ${code}: ${stderr}`));
760
+ }
761
+ });
762
+ proc.on('error', (err) => {
763
+ reject(new Error(`Codegen process error: ${err.message}`));
764
+ });
765
+ });
766
+ }
767
+ /**
768
+ * Find codegen binary in various locations
769
+ */
770
+ async findCodegenBinary() {
771
+ const candidates = [
772
+ this.config.codegenBinaryPath,
773
+ path.join(this.config.projectDir, 'bin/zap-codegen'),
774
+ path.join(__dirname, '../../bin/zap-codegen'),
775
+ path.join(this.config.projectDir, 'node_modules/@zap-js/client/bin/zap-codegen'),
776
+ path.join(this.config.projectDir, 'target/release/zap-codegen'),
777
+ path.join(this.config.projectDir, 'target/debug/zap-codegen'),
778
+ ].filter(Boolean);
779
+ for (const candidate of candidates) {
780
+ if (existsSync(candidate)) {
781
+ return candidate;
782
+ }
783
+ }
784
+ // Check PATH
785
+ try {
786
+ execSync('which zap-codegen', { stdio: 'ignore' });
787
+ return 'zap-codegen';
788
+ }
789
+ catch {
790
+ return null;
791
+ }
792
+ }
603
793
  /**
604
794
  * Print the ready message
605
795
  */
@@ -0,0 +1,52 @@
1
+ import { EventEmitter } from 'events';
2
+ export interface SpliceManagerConfig {
3
+ /** Path to Splice binary */
4
+ spliceBinaryPath: string;
5
+ /** Path to user's server binary */
6
+ workerBinaryPath: string;
7
+ /** Socket path for host connection */
8
+ socketPath: string;
9
+ /** Max concurrent requests */
10
+ maxConcurrency?: number;
11
+ /** Timeout in seconds */
12
+ timeout?: number;
13
+ /** Watch paths for hot reload */
14
+ watchPaths?: string[];
15
+ }
16
+ /**
17
+ * SpliceManager - DevServer component for Splice supervisor
18
+ *
19
+ * Manages the Splice process lifecycle:
20
+ * - Spawns splice binary with proper args
21
+ * - Forwards logs
22
+ * - Monitors health
23
+ * - Graceful shutdown
24
+ *
25
+ * Follows same pattern as ProcessManager but specialized for Splice
26
+ */
27
+ export declare class SpliceManager extends EventEmitter {
28
+ private config;
29
+ private process;
30
+ private running;
31
+ constructor(config: SpliceManagerConfig);
32
+ /**
33
+ * Start the Splice supervisor
34
+ */
35
+ start(): Promise<void>;
36
+ /**
37
+ * Wait for Splice to create its socket file
38
+ */
39
+ private waitForSocket;
40
+ /**
41
+ * Stop the Splice process
42
+ */
43
+ stop(): Promise<void>;
44
+ /**
45
+ * Check if Splice is running
46
+ */
47
+ isRunning(): boolean;
48
+ /**
49
+ * Get the socket path
50
+ */
51
+ getSocketPath(): string;
52
+ }
@@ -0,0 +1,146 @@
1
+ import { EventEmitter } from 'events';
2
+ import { spawn } from 'child_process';
3
+ import { existsSync, unlinkSync } from 'fs';
4
+ /**
5
+ * SpliceManager - DevServer component for Splice supervisor
6
+ *
7
+ * Manages the Splice process lifecycle:
8
+ * - Spawns splice binary with proper args
9
+ * - Forwards logs
10
+ * - Monitors health
11
+ * - Graceful shutdown
12
+ *
13
+ * Follows same pattern as ProcessManager but specialized for Splice
14
+ */
15
+ export class SpliceManager extends EventEmitter {
16
+ constructor(config) {
17
+ super();
18
+ this.process = null;
19
+ this.running = false;
20
+ this.config = config;
21
+ }
22
+ /**
23
+ * Start the Splice supervisor
24
+ */
25
+ async start() {
26
+ if (this.running) {
27
+ throw new Error('Splice already running');
28
+ }
29
+ const args = [
30
+ '--socket',
31
+ this.config.socketPath,
32
+ '--worker',
33
+ this.config.workerBinaryPath,
34
+ '--max-concurrency',
35
+ (this.config.maxConcurrency || 1024).toString(),
36
+ '--timeout',
37
+ (this.config.timeout || 30).toString(),
38
+ ];
39
+ if (this.config.watchPaths && this.config.watchPaths.length > 0) {
40
+ args.push('--watch', this.config.watchPaths.join(','));
41
+ }
42
+ console.log('[Splice] Starting supervisor...');
43
+ console.log('[Splice] Worker:', this.config.workerBinaryPath);
44
+ console.log('[Splice] Socket:', this.config.socketPath);
45
+ this.process = spawn(this.config.spliceBinaryPath, args, {
46
+ stdio: ['ignore', 'pipe', 'pipe'],
47
+ env: {
48
+ ...process.env,
49
+ RUST_LOG: process.env.RUST_LOG || 'info',
50
+ },
51
+ });
52
+ if (!this.process.stdout || !this.process.stderr) {
53
+ throw new Error('Failed to create Splice process streams');
54
+ }
55
+ // Forward stdout
56
+ this.process.stdout.on('data', (data) => {
57
+ const output = data.toString().trim();
58
+ if (output) {
59
+ console.log(`[Splice] ${output}`);
60
+ }
61
+ });
62
+ // Forward stderr
63
+ this.process.stderr.on('data', (data) => {
64
+ const output = data.toString().trim();
65
+ if (output) {
66
+ console.error(`[Splice] ${output}`);
67
+ }
68
+ });
69
+ // Handle exit
70
+ this.process.on('exit', (code, signal) => {
71
+ this.running = false;
72
+ if (code !== 0 && code !== null) {
73
+ console.error(`[Splice] Exited: code=${code}, signal=${signal}`);
74
+ this.emit('error', new Error(`Splice exited with code ${code}`));
75
+ }
76
+ });
77
+ // Handle errors
78
+ this.process.on('error', (err) => {
79
+ this.running = false;
80
+ console.error('[Splice] Process error:', err);
81
+ this.emit('error', err);
82
+ });
83
+ this.running = true;
84
+ this.emit('started');
85
+ // Wait for socket to be ready
86
+ await this.waitForSocket();
87
+ }
88
+ /**
89
+ * Wait for Splice to create its socket file
90
+ */
91
+ async waitForSocket() {
92
+ const maxWait = 5000; // 5 seconds
93
+ const checkInterval = 100;
94
+ const startTime = Date.now();
95
+ while (Date.now() - startTime < maxWait) {
96
+ if (existsSync(this.config.socketPath)) {
97
+ console.log('[Splice] Socket ready');
98
+ return;
99
+ }
100
+ await new Promise((resolve) => setTimeout(resolve, checkInterval));
101
+ }
102
+ throw new Error('Splice socket not ready within timeout');
103
+ }
104
+ /**
105
+ * Stop the Splice process
106
+ */
107
+ async stop() {
108
+ if (!this.process || !this.running) {
109
+ return;
110
+ }
111
+ console.log('[Splice] Stopping...');
112
+ if (!this.process.killed) {
113
+ this.process.kill('SIGTERM');
114
+ }
115
+ // Wait briefly for graceful shutdown
116
+ await new Promise((resolve) => setTimeout(resolve, 1000));
117
+ // Force kill if still running
118
+ if (!this.process.killed) {
119
+ this.process.kill('SIGKILL');
120
+ }
121
+ this.process = null;
122
+ this.running = false;
123
+ // Cleanup socket file
124
+ if (existsSync(this.config.socketPath)) {
125
+ try {
126
+ unlinkSync(this.config.socketPath);
127
+ }
128
+ catch {
129
+ // Ignore cleanup errors
130
+ }
131
+ }
132
+ this.emit('stopped');
133
+ }
134
+ /**
135
+ * Check if Splice is running
136
+ */
137
+ isRunning() {
138
+ return this.running && this.process !== null && !this.process.killed;
139
+ }
140
+ /**
141
+ * Get the socket path
142
+ */
143
+ getSocketPath() {
144
+ return this.config.socketPath;
145
+ }
146
+ }
@@ -3,7 +3,7 @@ export type WatchEventType = 'add' | 'change' | 'unlink';
3
3
  export interface WatchEvent {
4
4
  type: WatchEventType;
5
5
  path: string;
6
- category: 'rust' | 'typescript' | 'config' | 'unknown';
6
+ category: 'rust' | 'user-server' | 'typescript' | 'config' | 'unknown';
7
7
  }
8
8
  export interface WatcherConfig {
9
9
  rootDir: string;
@@ -38,7 +38,7 @@ export declare class FileWatcher extends EventEmitter {
38
38
  */
39
39
  private handleEvent;
40
40
  /**
41
- * Categorize a file based on its extension
41
+ * Categorize a file based on its extension and location
42
42
  */
43
43
  private categorizeFile;
44
44
  /**
@@ -98,12 +98,19 @@ export class FileWatcher extends EventEmitter {
98
98
  this.debounceTimers.set(filePath, timer);
99
99
  }
100
100
  /**
101
- * Categorize a file based on its extension
101
+ * Categorize a file based on its extension and location
102
102
  */
103
103
  categorizeFile(filePath) {
104
104
  const ext = path.extname(filePath).toLowerCase();
105
105
  const basename = path.basename(filePath);
106
- // Rust files
106
+ // Check if file is in server/ directory (user's Rust server)
107
+ const relativePath = path.relative(this.config.rootDir, filePath);
108
+ if (relativePath.startsWith('server' + path.sep) || relativePath === 'server') {
109
+ if (ext === '.rs' || basename === 'Cargo.toml') {
110
+ return 'user-server';
111
+ }
112
+ }
113
+ // Rust files in packages/server (Zap runtime)
107
114
  if (ext === '.rs' || basename === 'Cargo.toml') {
108
115
  return 'rust';
109
116
  }
@@ -360,6 +360,8 @@ export interface ZapConfig {
360
360
  security?: SecurityConfig;
361
361
  /** Observability configuration */
362
362
  observability?: ObservabilityConfig;
363
+ /** Splice socket path for distributed Rust functions (optional) */
364
+ splice_socket_path?: string;
363
365
  }
364
366
  /**
365
367
  * Type guard for InvokeHandlerMessage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zap-js/client",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "High-performance fullstack React framework - Client package",
5
5
  "homepage": "https://github.com/saint0x/zapjs",
6
6
  "repository": {
@@ -74,9 +74,9 @@
74
74
  "ws": "^8.16.0"
75
75
  },
76
76
  "optionalDependencies": {
77
- "@zap-js/darwin-arm64": "0.2.1",
78
- "@zap-js/darwin-x64": "0.2.1",
79
- "@zap-js/linux-x64": "0.2.1"
77
+ "@zap-js/darwin-arm64": "0.2.3",
78
+ "@zap-js/darwin-x64": "0.2.3",
79
+ "@zap-js/linux-x64": "0.2.3"
80
80
  },
81
81
  "peerDependencies": {
82
82
  "react": "^18.0.0",