@sansavision/create-pulse 0.1.0-alpha.9 → 0.4.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.
package/dist/index.js CHANGED
@@ -53,6 +53,7 @@ async function main() {
53
53
  options: [
54
54
  { value: "react-watch-together", label: "Watch Together (React + TS)", hint: "Synchronized video playback" },
55
55
  { value: "react-all-features", label: "All Features (React + TS)", hint: "Chat, Video, Audio, RPC" },
56
+ { value: "react-queue-demo", label: "Durable Queues (React + TS)", hint: "Persistent queues with WAL/Postgres/Redis" },
56
57
  { value: "vanilla-basic", label: "Vanilla JS (Basic)", hint: "Minimal setup" }
57
58
  ]
58
59
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sansavision/create-pulse",
3
- "version": "0.1.0-alpha.9",
3
+ "version": "0.4.0",
4
4
  "description": "Scaffold a new Pulse application",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -29,4 +29,4 @@
29
29
  "tsup": "^8.0.2",
30
30
  "typescript": "^5.0.0"
31
31
  }
32
- }
32
+ }
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ async function main() {
35
35
  options: [
36
36
  { value: 'react-watch-together', label: 'Watch Together (React + TS)', hint: 'Synchronized video playback' },
37
37
  { value: 'react-all-features', label: 'All Features (React + TS)', hint: 'Chat, Video, Audio, RPC' },
38
+ { value: 'react-queue-demo', label: 'Durable Queues (React + TS)', hint: 'Persistent queues with WAL/Postgres/Redis' },
38
39
  { value: 'vanilla-basic', label: 'Vanilla JS (Basic)', hint: 'Minimal setup' }
39
40
  ]
40
41
  });
@@ -1,27 +1,27 @@
1
1
  {
2
- "name": "pulse-react-all-features",
3
- "private": true,
4
- "version": "0.1.0",
5
- "type": "module",
6
- "scripts": {
7
- "dev": "vite",
8
- "build": "tsc -b && vite build",
9
- "preview": "vite preview"
10
- },
11
- "dependencies": {
12
- "@sansavision/pulse-sdk": "latest",
13
- "react": "^18.3.1",
14
- "react-dom": "^18.3.1",
15
- "lucide-react": "^0.412.0"
16
- },
17
- "devDependencies": {
18
- "@types/react": "^18.3.3",
19
- "@types/react-dom": "^18.3.0",
20
- "@vitejs/plugin-react": "^4.3.1",
21
- "autoprefixer": "^10.4.19",
22
- "postcss": "^8.4.39",
23
- "tailwindcss": "^3.4.6",
24
- "typescript": "^5.5.3",
25
- "vite": "^5.3.4"
26
- }
27
- }
2
+ "name": "pulse-react-all-features",
3
+ "private": true,
4
+ "version": "0.4.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@sansavision/pulse-sdk": "0.3.0",
13
+ "react": "^18.3.1",
14
+ "react-dom": "^18.3.1",
15
+ "lucide-react": "^0.412.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/react": "^18.3.3",
19
+ "@types/react-dom": "^18.3.0",
20
+ "@vitejs/plugin-react": "^4.3.1",
21
+ "autoprefixer": "^10.4.19",
22
+ "postcss": "^8.4.39",
23
+ "tailwindcss": "^3.4.6",
24
+ "typescript": "^5.5.3",
25
+ "vite": "^5.3.4"
26
+ }
27
+ }
@@ -73,25 +73,35 @@ export default function App() {
73
73
  const msg = { action, time, ts: sendTs };
74
74
  broadcast(msg);
75
75
 
76
- // Local loopback: since the relay doesn't echo back to the sender,
77
- // we directly update peerTime for the same-tab demo experience.
78
- // Also record the loopback delay so telemetry metrics are visible.
79
- if (action === 'tick') {
80
- setPeerTime(time);
81
- } else if (action === 'play') {
82
- setIsPlaying(true);
83
- setPeerTime(time);
84
- } else if (action === 'pause') {
85
- setIsPlaying(false);
86
- setPeerTime(time);
87
- } else if (action === 'seek') {
88
- setPeerTime(time);
89
- }
90
-
91
- // Record loopback delay (measures local processing overhead)
92
- const loopbackDelay = Date.now() - sendTs;
93
- setLastDelay(loopbackDelay);
94
- delayHistoryRef.current = [...delayHistoryRef.current.slice(-29), loopbackDelay];
76
+ // Single-tab simulation: defer peerTime update by the measured relay
77
+ // round-trip so that drift is visible and realistic. In a real
78
+ // multi-tab setup the relay echo handler (handleSyncMessage) updates
79
+ // peerTime but since the relay does not echo back to the sender,
80
+ // we simulate it here with the actual measured delay.
81
+ const simulatedDelay = delayHistoryRef.current.length > 0
82
+ ? delayHistoryRef.current[delayHistoryRef.current.length - 1]
83
+ : 4; // sensible default ~4ms first message
84
+
85
+ setTimeout(() => {
86
+ if (action === 'tick') {
87
+ setPeerTime(time);
88
+ } else if (action === 'play') {
89
+ setIsPlaying(true);
90
+ setPeerTime(time);
91
+ } else if (action === 'pause') {
92
+ setIsPlaying(false);
93
+ setPeerTime(time);
94
+ } else if (action === 'seek') {
95
+ setPeerTime(time);
96
+ }
97
+ }, simulatedDelay);
98
+
99
+ // Measure delay after the event loop processes the send
100
+ setTimeout(() => {
101
+ const delay = Date.now() - sendTs;
102
+ setLastDelay(delay);
103
+ delayHistoryRef.current = [...delayHistoryRef.current.slice(-29), delay];
104
+ }, 0);
95
105
  }, [broadcast, logEvent]);
96
106
 
97
107
  // Computed delay stats
@@ -207,10 +207,13 @@ export function GameSync({ useChannel, logEvent }: GameSyncProps) {
207
207
  // Send to peers
208
208
  broadcast(payload);
209
209
 
210
- // Record loopback delay so metrics are visible
211
- const loopbackDelay = Date.now() - sendTs;
212
- setLastDelay(loopbackDelay);
213
- delayHistoryRef.current = [...delayHistoryRef.current.slice(-29), loopbackDelay];
210
+ // Delay is measured in handleSyncMessage when the relay echoes back.
211
+ // Also measure locally via deferred setTimeout for single-tab demos.
212
+ setTimeout(() => {
213
+ const delay = Date.now() - sendTs;
214
+ setLastDelay(delay);
215
+ delayHistoryRef.current = [...delayHistoryRef.current.slice(-29), delay];
216
+ }, 0);
214
217
 
215
218
  // Interpolate fallback for when broadcast doesn't echo locally fast enough
216
219
  const rv = remoteViewRef.current;
@@ -101,10 +101,12 @@ export function ServerMetrics({ useChannel, logEvent }: ServerMetricsProps) {
101
101
  // Normally the server does this, here we do it so the demo "just works" locally
102
102
  broadcast(fakeMetrics);
103
103
 
104
- // Record loopback delay
105
- const loopbackDelay = Date.now() - sendTs;
106
- setLastDelay(loopbackDelay);
107
- delayHistoryRef.current = [...delayHistoryRef.current.slice(-29), loopbackDelay];
104
+ // Measure delay via deferred setTimeout for single-tab demos
105
+ setTimeout(() => {
106
+ const delay = Date.now() - sendTs;
107
+ setLastDelay(delay);
108
+ delayHistoryRef.current = [...delayHistoryRef.current.slice(-29), delay];
109
+ }, 0);
108
110
  }, 1000);
109
111
 
110
112
  return () => clearInterval(interval);
@@ -0,0 +1,44 @@
1
+ # Pulse Queue Demo
2
+
3
+ A demo app showcasing **Pulse v0.4.0** durable message queues.
4
+
5
+ ## Features
6
+
7
+ - Publish → Queue → Consumer flow over PLP
8
+ - At-least-once delivery with ACK/NACK
9
+ - Dead-letter queue for failed messages
10
+ - Real-time RTT metrics
11
+
12
+ ## Quick Start
13
+
14
+ ```bash
15
+ npm install
16
+ npm run dev
17
+ ```
18
+
19
+ In a separate terminal, start Pulse with your preferred queue backend:
20
+
21
+ ```bash
22
+ # In-memory (default, ephemeral)
23
+ pulse dev
24
+
25
+ # WAL (Write-Ahead Log — crash-resilient)
26
+ PULSE_QUEUE_BACKEND=wal pulse dev
27
+
28
+ # PostgreSQL
29
+ PULSE_QUEUE_BACKEND=postgres PULSE_QUEUE_POSTGRES_URL=postgres://user:pass@localhost/pulse pulse dev
30
+
31
+ # Redis
32
+ PULSE_QUEUE_BACKEND=redis PULSE_QUEUE_REDIS_URL=redis://localhost:6379 pulse dev
33
+ ```
34
+
35
+ ## Encryption at Rest
36
+
37
+ Add encryption to any backend:
38
+
39
+ ```bash
40
+ export PULSE_QUEUE_KEY=$(openssl rand -hex 32)
41
+ PULSE_QUEUE_BACKEND=wal pulse dev
42
+ ```
43
+
44
+ All payloads are encrypted with ChaCha20-Poly1305 before reaching storage.
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Pulse Queue Demo</title>
7
+ <style>
8
+ body { margin: 0; background: #0f0f23; color: #e5e7eb; }
9
+ </style>
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ <script type="module" src="/src/main.tsx"></script>
14
+ </body>
15
+ </html>
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "pulse-queue-demo",
3
+ "version": "0.4.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "tsc && vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "dependencies": {
11
+ "@sansavision/pulse-sdk": "^0.4.0",
12
+ "react": "^19.0.0",
13
+ "react-dom": "^19.0.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/react": "^19.0.0",
17
+ "@types/react-dom": "^19.0.0",
18
+ "@vitejs/plugin-react": "^4.0.0",
19
+ "typescript": "^5.7.0",
20
+ "vite": "^6.0.0"
21
+ }
22
+ }
@@ -0,0 +1,172 @@
1
+ import { useState, useEffect, useCallback } from 'react'
2
+ import { Pulse } from '@sansavision/pulse-sdk'
3
+
4
+ /**
5
+ * Pulse v0.4.0 — Durable Queue Demo
6
+ *
7
+ * This template demonstrates the persistent message queue system with:
8
+ * - Publisher → Queue → Consumer flow over PLP
9
+ * - At-least-once delivery semantics
10
+ * - Dead-letter queue visibility
11
+ * - Real-time metrics (RTT, packet loss)
12
+ *
13
+ * Server-side queue backends (configured via PULSE_QUEUE_BACKEND env var):
14
+ * - memory: In-memory (default, ephemeral)
15
+ * - wal: Write-Ahead Log (crash-resilient, local filesystem)
16
+ * - postgres: PostgreSQL (cloud/HA)
17
+ * - redis: Redis (shared state / cache)
18
+ */
19
+
20
+ interface QueueMessage {
21
+ id: string
22
+ payload: string
23
+ timestamp: number
24
+ state: 'pending' | 'in_flight' | 'acked' | 'dead_lettered'
25
+ }
26
+
27
+ export default function App() {
28
+ const [connected, setConnected] = useState(false)
29
+ const [messages, setMessages] = useState<QueueMessage[]>([])
30
+ const [input, setInput] = useState('')
31
+ const [rtt, setRtt] = useState<number | null>(null)
32
+ const [conn, setConn] = useState<any>(null)
33
+
34
+ useEffect(() => {
35
+ const pulse = new Pulse({ apiKey: 'dev' })
36
+
37
+ pulse.connect('ws://localhost:4001').then((connection) => {
38
+ setConn(connection)
39
+ setConnected(true)
40
+
41
+ // Subscribe to queue stream
42
+ const stream = connection.stream('task-queue', { qos: 'durable' })
43
+ stream.on('data', (bytes: Uint8Array) => {
44
+ const msg = JSON.parse(new TextDecoder().decode(bytes))
45
+ setMessages((prev) => [...prev, msg])
46
+ })
47
+
48
+ // Live metrics
49
+ connection.on('metrics', (m: any) => setRtt(m.rtt))
50
+ })
51
+
52
+ return () => {
53
+ conn?.close()
54
+ }
55
+ }, [])
56
+
57
+ const publish = useCallback(async () => {
58
+ if (!conn || !input.trim()) return
59
+
60
+ const msg: QueueMessage = {
61
+ id: crypto.randomUUID(),
62
+ payload: input.trim(),
63
+ timestamp: Date.now(),
64
+ state: 'pending',
65
+ }
66
+
67
+ const stream = conn.stream('task-queue', { qos: 'durable' })
68
+ stream.send(new TextEncoder().encode(JSON.stringify(msg)))
69
+ setMessages((prev) => [...prev, msg])
70
+ setInput('')
71
+ }, [conn, input])
72
+
73
+ const ackMessage = useCallback(async (id: string) => {
74
+ if (!conn) return
75
+ await conn.service('queue', 'ack', { queue: 'task-queue', message_id: id })
76
+ setMessages((prev) =>
77
+ prev.map((m) => (m.id === id ? { ...m, state: 'acked' as const } : m))
78
+ )
79
+ }, [conn])
80
+
81
+ return (
82
+ <div style={{ fontFamily: 'Inter, system-ui, sans-serif', maxWidth: 720, margin: '0 auto', padding: 32 }}>
83
+ <h1>🔄 Pulse Queue Demo</h1>
84
+ <p style={{ color: '#888' }}>
85
+ Durable message queues with at-least-once delivery over PLP.
86
+ {connected ? (
87
+ <span style={{ color: '#4ade80' }}> ● Connected</span>
88
+ ) : (
89
+ <span style={{ color: '#f87171' }}> ○ Connecting...</span>
90
+ )}
91
+ {rtt !== null && <span style={{ marginLeft: 12 }}>RTT: {rtt}ms</span>}
92
+ </p>
93
+
94
+ <div style={{ display: 'flex', gap: 8, marginBottom: 24 }}>
95
+ <input
96
+ value={input}
97
+ onChange={(e) => setInput(e.target.value)}
98
+ onKeyDown={(e) => e.key === 'Enter' && publish()}
99
+ placeholder="Enter a task message..."
100
+ style={{
101
+ flex: 1, padding: '10px 16px', borderRadius: 8,
102
+ border: '1px solid #333', background: '#1a1a2e', color: '#fff', fontSize: 14
103
+ }}
104
+ />
105
+ <button
106
+ onClick={publish}
107
+ disabled={!connected}
108
+ style={{
109
+ padding: '10px 20px', borderRadius: 8, border: 'none',
110
+ background: '#7c3aed', color: '#fff', fontWeight: 600, cursor: 'pointer'
111
+ }}
112
+ >
113
+ Publish
114
+ </button>
115
+ </div>
116
+
117
+ <h2>Messages ({messages.length})</h2>
118
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
119
+ {messages.map((msg) => (
120
+ <div
121
+ key={msg.id}
122
+ style={{
123
+ padding: 16, borderRadius: 8,
124
+ background: msg.state === 'acked' ? '#064e3b' : msg.state === 'dead_lettered' ? '#7f1d1d' : '#1e1b4b',
125
+ display: 'flex', justifyContent: 'space-between', alignItems: 'center'
126
+ }}
127
+ >
128
+ <div>
129
+ <strong>{msg.payload}</strong>
130
+ <div style={{ fontSize: 12, color: '#888', marginTop: 4 }}>
131
+ {msg.id.slice(0, 8)} · {new Date(msg.timestamp).toLocaleTimeString()} ·{' '}
132
+ <span style={{
133
+ color: msg.state === 'acked' ? '#4ade80' : msg.state === 'dead_lettered' ? '#f87171' : '#facc15'
134
+ }}>
135
+ {msg.state}
136
+ </span>
137
+ </div>
138
+ </div>
139
+ {msg.state === 'pending' && (
140
+ <button
141
+ onClick={() => ackMessage(msg.id)}
142
+ style={{
143
+ padding: '6px 12px', borderRadius: 6, border: 'none',
144
+ background: '#22c55e', color: '#000', fontWeight: 600, cursor: 'pointer'
145
+ }}
146
+ >
147
+ ACK
148
+ </button>
149
+ )}
150
+ </div>
151
+ ))}
152
+ </div>
153
+
154
+ <div style={{ marginTop: 32, padding: 16, borderRadius: 8, background: '#111827', fontSize: 13, color: '#9ca3af' }}>
155
+ <strong style={{ color: '#e5e7eb' }}>Server Configuration</strong>
156
+ <pre style={{ marginTop: 8 }}>
157
+ {`# Start Pulse with WAL queue persistence:
158
+ PULSE_QUEUE_BACKEND=wal PULSE_QUEUE_WAL_DIR=./data pulse dev
159
+
160
+ # Or with PostgreSQL:
161
+ PULSE_QUEUE_BACKEND=postgres PULSE_QUEUE_POSTGRES_URL=postgres://... pulse dev
162
+
163
+ # Or with Redis:
164
+ PULSE_QUEUE_BACKEND=redis PULSE_QUEUE_REDIS_URL=redis://localhost:6379 pulse dev
165
+
166
+ # Enable encryption at rest (any backend):
167
+ PULSE_QUEUE_KEY=$(openssl rand -hex 32) pulse dev`}
168
+ </pre>
169
+ </div>
170
+ </div>
171
+ )
172
+ }
@@ -0,0 +1,4 @@
1
+ import { createRoot } from 'react-dom/client'
2
+ import App from './App'
3
+
4
+ createRoot(document.getElementById('root')!).render(<App />)
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "resolveJsonModule": true,
10
+ "isolatedModules": true,
11
+ "noEmit": true,
12
+ "jsx": "react-jsx",
13
+ "strict": true
14
+ },
15
+ "include": ["src"]
16
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ })
@@ -1,27 +1,27 @@
1
1
  {
2
- "name": "pulse-react-watch-together",
3
- "private": true,
4
- "version": "0.1.0",
5
- "type": "module",
6
- "scripts": {
7
- "dev": "vite",
8
- "build": "tsc -b && vite build",
9
- "preview": "vite preview"
10
- },
11
- "dependencies": {
12
- "@sansavision/pulse-sdk": "latest",
13
- "react": "^18.3.1",
14
- "react-dom": "^18.3.1",
15
- "lucide-react": "^0.412.0"
16
- },
17
- "devDependencies": {
18
- "@types/react": "^18.3.3",
19
- "@types/react-dom": "^18.3.0",
20
- "@vitejs/plugin-react": "^4.3.1",
21
- "autoprefixer": "^10.4.19",
22
- "postcss": "^8.4.39",
23
- "tailwindcss": "^3.4.6",
24
- "typescript": "^5.5.3",
25
- "vite": "^5.3.4"
26
- }
27
- }
2
+ "name": "pulse-react-watch-together",
3
+ "private": true,
4
+ "version": "0.4.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@sansavision/pulse-sdk": "0.3.0",
13
+ "react": "^18.3.1",
14
+ "react-dom": "^18.3.1",
15
+ "lucide-react": "^0.412.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/react": "^18.3.3",
19
+ "@types/react-dom": "^18.3.0",
20
+ "@vitejs/plugin-react": "^4.3.1",
21
+ "autoprefixer": "^10.4.19",
22
+ "postcss": "^8.4.39",
23
+ "tailwindcss": "^3.4.6",
24
+ "typescript": "^5.5.3",
25
+ "vite": "^5.3.4"
26
+ }
27
+ }
@@ -81,25 +81,36 @@ export default function App() {
81
81
  const msg = { action, time, ts: sendTs };
82
82
  broadcast(msg);
83
83
 
84
- // Local loopback: since the relay doesn't echo back to the sender,
85
- // we directly update peerTime to simulate the round-trip for the demo.
86
- // For a real multi-user scenario, the relay delivers to other connections.
87
- if (action === 'tick') {
88
- setPeerTime(time);
89
- } else if (action === 'play') {
90
- setIsPlaying(true);
91
- setPeerTime(time);
92
- } else if (action === 'pause') {
93
- setIsPlaying(false);
94
- setPeerTime(time);
95
- } else if (action === 'seek') {
96
- setPeerTime(time);
97
- }
84
+ // Single-tab simulation: defer peerTime update by the measured relay
85
+ // round-trip so that drift is visible and realistic. In a real
86
+ // multi-tab setup the relay echo handler (handleSyncMessage) updates
87
+ // peerTime but since the relay does not echo back to the sender,
88
+ // we simulate it here with the actual measured delay.
89
+ const simulatedDelay = delayHistoryRef.current.length > 0
90
+ ? delayHistoryRef.current[delayHistoryRef.current.length - 1]
91
+ : 4; // sensible default ~4ms first message
92
+
93
+ setTimeout(() => {
94
+ if (action === 'tick') {
95
+ setPeerTime(time);
96
+ } else if (action === 'play') {
97
+ setIsPlaying(true);
98
+ setPeerTime(time);
99
+ } else if (action === 'pause') {
100
+ setIsPlaying(false);
101
+ setPeerTime(time);
102
+ } else if (action === 'seek') {
103
+ setPeerTime(time);
104
+ }
105
+ }, simulatedDelay);
98
106
 
99
- // Record loopback delay so telemetry metrics are visible
100
- const loopbackDelay = Date.now() - sendTs;
101
- setLastDelay(loopbackDelay);
102
- delayHistoryRef.current = [...delayHistoryRef.current.slice(-29), loopbackDelay];
107
+ // Measure delay after the event loop processes the send (captures
108
+ // real serialization + WebSocket write time, not synchronous 0).
109
+ setTimeout(() => {
110
+ const delay = Date.now() - sendTs;
111
+ setLastDelay(delay);
112
+ delayHistoryRef.current = [...delayHistoryRef.current.slice(-29), delay];
113
+ }, 0);
103
114
  }, [broadcast, logEvent]);
104
115
 
105
116
  const handleQualityChange = useCallback((quality: typeof QUALITY_OPTIONS[0]) => {