@sansavision/create-pulse 0.4.1 → 0.4.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.
Files changed (41) hide show
  1. package/README.md +27 -3
  2. package/dist/index.js +3 -1
  3. package/package.json +1 -1
  4. package/templates/nextjs-auth-demo/.env.example +6 -0
  5. package/templates/nextjs-auth-demo/README.md +125 -0
  6. package/templates/nextjs-auth-demo/_gitignore +33 -0
  7. package/templates/nextjs-auth-demo/drizzle.config.ts +10 -0
  8. package/templates/nextjs-auth-demo/eslint.config.mjs +18 -0
  9. package/templates/nextjs-auth-demo/next-env.d.ts +6 -0
  10. package/templates/nextjs-auth-demo/next.config.ts +7 -0
  11. package/templates/nextjs-auth-demo/package.json +36 -0
  12. package/templates/nextjs-auth-demo/postcss.config.mjs +7 -0
  13. package/templates/nextjs-auth-demo/public/file.svg +1 -0
  14. package/templates/nextjs-auth-demo/public/globe.svg +1 -0
  15. package/templates/nextjs-auth-demo/public/next.svg +1 -0
  16. package/templates/nextjs-auth-demo/public/vercel.svg +1 -0
  17. package/templates/nextjs-auth-demo/public/window.svg +1 -0
  18. package/templates/nextjs-auth-demo/src/app/api/auth/[...all]/route.ts +4 -0
  19. package/templates/nextjs-auth-demo/src/app/api/pulse/verify/route.ts +54 -0
  20. package/templates/nextjs-auth-demo/src/app/auth/sign-in/page.tsx +131 -0
  21. package/templates/nextjs-auth-demo/src/app/auth/sign-up/page.tsx +153 -0
  22. package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +248 -0
  23. package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +198 -0
  24. package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +192 -0
  25. package/templates/nextjs-auth-demo/src/app/dashboard/demos/queues/page.tsx +297 -0
  26. package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +258 -0
  27. package/templates/nextjs-auth-demo/src/app/dashboard/layout.tsx +109 -0
  28. package/templates/nextjs-auth-demo/src/app/dashboard/page.tsx +147 -0
  29. package/templates/nextjs-auth-demo/src/app/favicon.ico +0 -0
  30. package/templates/nextjs-auth-demo/src/app/globals.css +96 -0
  31. package/templates/nextjs-auth-demo/src/app/layout.tsx +27 -0
  32. package/templates/nextjs-auth-demo/src/app/page.tsx +254 -0
  33. package/templates/nextjs-auth-demo/src/lib/auth-client.ts +15 -0
  34. package/templates/nextjs-auth-demo/src/lib/auth.ts +14 -0
  35. package/templates/nextjs-auth-demo/src/lib/db.ts +6 -0
  36. package/templates/nextjs-auth-demo/src/lib/pulse.ts +45 -0
  37. package/templates/nextjs-auth-demo/src/lib/schema.ts +107 -0
  38. package/templates/nextjs-auth-demo/tsconfig.json +34 -0
  39. package/templates/react-queue-demo/README.md +6 -7
  40. package/templates/react-queue-demo/src/App.tsx +34 -13
  41. package/src/index.ts +0 -115
@@ -0,0 +1,254 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import {
5
+ Zap,
6
+ Shield,
7
+ Wifi,
8
+ MessageSquare,
9
+ Video,
10
+ Database,
11
+ Gamepad2,
12
+ Lock,
13
+ ArrowRight,
14
+ Globe,
15
+ Clock,
16
+ Activity,
17
+ } from "lucide-react";
18
+
19
+ export default function LandingPage() {
20
+ return (
21
+ <div className="min-h-screen">
22
+ {/* Nav */}
23
+ <nav className="fixed top-0 left-0 right-0 z-50 glass">
24
+ <div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
25
+ <div className="flex items-center gap-2">
26
+ <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-purple-500 to-cyan-500 flex items-center justify-center">
27
+ <Zap className="w-5 h-5 text-white" />
28
+ </div>
29
+ <span className="text-xl font-bold">Pulse</span>
30
+ </div>
31
+ <div className="flex items-center gap-4">
32
+ <Link
33
+ href="/auth/sign-in"
34
+ className="px-4 py-2 text-sm text-slate-300 hover:text-white transition-colors"
35
+ >
36
+ Sign In
37
+ </Link>
38
+ <Link
39
+ href="/auth/sign-up"
40
+ className="px-5 py-2 text-sm bg-purple-600 hover:bg-purple-500 rounded-lg font-medium transition-all hover:shadow-lg hover:shadow-purple-500/25"
41
+ >
42
+ Get Started
43
+ </Link>
44
+ </div>
45
+ </div>
46
+ </nav>
47
+
48
+ {/* Hero */}
49
+ <section className="relative pt-32 pb-20 px-6 overflow-hidden">
50
+ {/* Background gradient orbs */}
51
+ <div className="absolute top-20 left-1/4 w-96 h-96 bg-purple-500/20 rounded-full blur-[128px]" />
52
+ <div className="absolute top-40 right-1/4 w-96 h-96 bg-cyan-500/20 rounded-full blur-[128px]" />
53
+
54
+ <div className="max-w-5xl mx-auto text-center relative z-10">
55
+ <div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full border border-purple-500/30 bg-purple-500/10 text-purple-300 text-sm mb-8">
56
+ <Activity className="w-3.5 h-3.5" />
57
+ <span>PLP — Pulse Line Protocol v1</span>
58
+ </div>
59
+
60
+ <h1 className="text-5xl md:text-7xl font-extrabold tracking-tight leading-tight mb-6">
61
+ <span className="gradient-text">The Real-Time Protocol</span>
62
+ <br />
63
+ <span className="text-slate-200">for Modern Applications</span>
64
+ </h1>
65
+
66
+ <p className="text-xl text-slate-400 max-w-2xl mx-auto mb-10 leading-relaxed">
67
+ Sub-millisecond latency, end-to-end encryption, durable queues, and
68
+ external auth — all in one binary. Built with Rust, powered by QUIC.
69
+ </p>
70
+
71
+ <div className="flex items-center justify-center gap-4">
72
+ <Link
73
+ href="/auth/sign-up"
74
+ className="inline-flex items-center gap-2 px-8 py-3.5 bg-gradient-to-r from-purple-600 to-purple-500 hover:from-purple-500 hover:to-purple-400 rounded-xl font-semibold text-lg transition-all hover:shadow-xl hover:shadow-purple-500/25 pulse-glow"
75
+ >
76
+ Try the Demo
77
+ <ArrowRight className="w-5 h-5" />
78
+ </Link>
79
+ <a
80
+ href="https://github.com/Sansa-Organisation/pulse"
81
+ target="_blank"
82
+ rel="noopener"
83
+ className="px-8 py-3.5 rounded-xl font-semibold text-lg border border-slate-700 hover:border-slate-500 hover:bg-slate-800/50 transition-all"
84
+ >
85
+ GitHub
86
+ </a>
87
+ </div>
88
+
89
+ {/* Stats */}
90
+ <div className="grid grid-cols-3 gap-8 max-w-xl mx-auto mt-16">
91
+ {[
92
+ { label: "Latency", value: "<1ms", icon: Clock },
93
+ { label: "Connections", value: "100K+", icon: Globe },
94
+ { label: "Uptime", value: "99.99%", icon: Activity },
95
+ ].map((stat) => (
96
+ <div key={stat.label} className="text-center">
97
+ <stat.icon className="w-5 h-5 text-purple-400 mx-auto mb-2" />
98
+ <div className="text-2xl font-bold">{stat.value}</div>
99
+ <div className="text-sm text-slate-500">{stat.label}</div>
100
+ </div>
101
+ ))}
102
+ </div>
103
+ </div>
104
+ </section>
105
+
106
+ {/* Features */}
107
+ <section className="py-20 px-6">
108
+ <div className="max-w-6xl mx-auto">
109
+ <div className="text-center mb-16">
110
+ <h2 className="text-3xl md:text-4xl font-bold mb-4">
111
+ Everything you need, out of the box
112
+ </h2>
113
+ <p className="text-slate-400 text-lg max-w-xl mx-auto">
114
+ One protocol, five powerful modes. Each demo below is fully functional
115
+ and uses real PLP connections.
116
+ </p>
117
+ </div>
118
+
119
+ <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
120
+ {[
121
+ {
122
+ icon: MessageSquare,
123
+ title: "Real-time Chat",
124
+ desc: "Multi-user rooms with typing indicators, presence, and message history.",
125
+ color: "from-blue-500 to-blue-600",
126
+ },
127
+ {
128
+ icon: Video,
129
+ title: "Watch Together",
130
+ desc: "Synchronized video playback — play, pause, seek in perfect sync.",
131
+ color: "from-pink-500 to-rose-600",
132
+ },
133
+ {
134
+ icon: Database,
135
+ title: "Durable Queues",
136
+ desc: "Persist messages, simulate offline, come back online — nothing lost.",
137
+ color: "from-amber-500 to-orange-600",
138
+ },
139
+ {
140
+ icon: Gamepad2,
141
+ title: "Game State Sync",
142
+ desc: "Real-time shared state with conflict-free resolution.",
143
+ color: "from-green-500 to-emerald-600",
144
+ },
145
+ {
146
+ icon: Lock,
147
+ title: "E2E Encrypted Chat",
148
+ desc: "End-to-end encryption with relay-blind transport.",
149
+ color: "from-purple-500 to-violet-600",
150
+ },
151
+ {
152
+ icon: Shield,
153
+ title: "Auth Context",
154
+ desc: "External auth integration — Better Auth, Clerk, Auth0, and more.",
155
+ color: "from-cyan-500 to-teal-600",
156
+ },
157
+ ].map((feature) => (
158
+ <div
159
+ key={feature.title}
160
+ className="demo-card glass rounded-2xl p-6"
161
+ >
162
+ <div
163
+ className={`w-12 h-12 rounded-xl bg-gradient-to-br ${feature.color} flex items-center justify-center mb-4`}
164
+ >
165
+ <feature.icon className="w-6 h-6 text-white" />
166
+ </div>
167
+ <h3 className="text-lg font-semibold mb-2">{feature.title}</h3>
168
+ <p className="text-slate-400 text-sm leading-relaxed">
169
+ {feature.desc}
170
+ </p>
171
+ </div>
172
+ ))}
173
+ </div>
174
+ </div>
175
+ </section>
176
+
177
+ {/* Architecture */}
178
+ <section className="py-20 px-6">
179
+ <div className="max-w-4xl mx-auto">
180
+ <div className="glass rounded-2xl p-8 md:p-12">
181
+ <h2 className="text-2xl font-bold mb-6 text-center">
182
+ How Auth Works
183
+ </h2>
184
+ <div className="flex flex-col md:flex-row items-center gap-6">
185
+ {[
186
+ {
187
+ step: 1,
188
+ icon: Shield,
189
+ title: "Sign In",
190
+ desc: "User authenticates via Better Auth (email/password)",
191
+ },
192
+ {
193
+ step: 2,
194
+ icon: Wifi,
195
+ title: "Connect",
196
+ desc: "SDK sends bearer token in PLP CONNECT handshake",
197
+ },
198
+ {
199
+ step: 3,
200
+ icon: Zap,
201
+ title: "Verify",
202
+ desc: "Relay calls webhook → Better Auth validates session",
203
+ },
204
+ ].map((item, i) => (
205
+ <div key={item.step} className="flex-1 text-center">
206
+ <div className="w-10 h-10 rounded-full bg-purple-600 text-white font-bold flex items-center justify-center mx-auto mb-3">
207
+ {item.step}
208
+ </div>
209
+ <item.icon className="w-6 h-6 text-purple-400 mx-auto mb-2" />
210
+ <h4 className="font-semibold mb-1">{item.title}</h4>
211
+ <p className="text-sm text-slate-400">{item.desc}</p>
212
+ {i < 2 && (
213
+ <ArrowRight className="w-5 h-5 text-slate-600 mx-auto mt-4 hidden md:block rotate-0" />
214
+ )}
215
+ </div>
216
+ ))}
217
+ </div>
218
+ </div>
219
+ </div>
220
+ </section>
221
+
222
+ {/* CTA */}
223
+ <section className="py-20 px-6">
224
+ <div className="max-w-3xl mx-auto text-center">
225
+ <h2 className="text-3xl md:text-4xl font-bold mb-4">
226
+ Ready to experience it?
227
+ </h2>
228
+ <p className="text-slate-400 text-lg mb-8">
229
+ Create an account and explore every feature live. Open two browser
230
+ tabs to see multi-user sync in action.
231
+ </p>
232
+ <Link
233
+ href="/auth/sign-up"
234
+ className="inline-flex items-center gap-2 px-10 py-4 bg-gradient-to-r from-purple-600 to-cyan-600 hover:from-purple-500 hover:to-cyan-500 rounded-xl font-semibold text-lg transition-all hover:shadow-xl hover:shadow-purple-500/25"
235
+ >
236
+ Create Account
237
+ <ArrowRight className="w-5 h-5" />
238
+ </Link>
239
+ </div>
240
+ </section>
241
+
242
+ {/* Footer */}
243
+ <footer className="border-t border-slate-800 py-8 px-6">
244
+ <div className="max-w-6xl mx-auto flex items-center justify-between text-sm text-slate-500">
245
+ <span>© 2026 Sansa Vision. Pulse Protocol.</span>
246
+ <div className="flex items-center gap-1">
247
+ <div className="status-online" />
248
+ <span>Relay Live</span>
249
+ </div>
250
+ </div>
251
+ </footer>
252
+ </div>
253
+ );
254
+ }
@@ -0,0 +1,15 @@
1
+ import { createAuthClient } from "better-auth/react";
2
+
3
+ export const authClient = createAuthClient({
4
+ baseURL: typeof window !== "undefined" ? window.location.origin : "http://localhost:3000",
5
+ fetchOptions: {
6
+ onSuccess: (ctx) => {
7
+ const authToken = ctx.response.headers.get("set-auth-token");
8
+ if (authToken && typeof window !== "undefined") {
9
+ localStorage.setItem("pulse_bearer_token", authToken);
10
+ }
11
+ },
12
+ },
13
+ });
14
+
15
+ export const { signIn, signUp, signOut, useSession } = authClient;
@@ -0,0 +1,14 @@
1
+ import { betterAuth } from "better-auth";
2
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
3
+ import { bearer } from "better-auth/plugins";
4
+ import { db } from "./db";
5
+ import * as schema from "./schema";
6
+
7
+ export const auth = betterAuth({
8
+ database: drizzleAdapter(db, { provider: "sqlite", schema }),
9
+ emailAndPassword: {
10
+ enabled: true,
11
+ },
12
+ plugins: [bearer()],
13
+ trustedOrigins: [process.env.BETTER_AUTH_URL || "http://localhost:3000"],
14
+ });
@@ -0,0 +1,6 @@
1
+ import Database from "better-sqlite3";
2
+ import { drizzle } from "drizzle-orm/better-sqlite3";
3
+ import * as schema from "./schema";
4
+
5
+ const sqlite = new Database("./sqlite.db");
6
+ export const db = drizzle(sqlite, { schema });
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import { Pulse } from "@sansavision/pulse-sdk";
4
+
5
+ let pulseInstance: Pulse | null = null;
6
+
7
+ export function getPulse(): Pulse {
8
+ if (!pulseInstance) {
9
+ pulseInstance = new Pulse({ apiKey: "demo" });
10
+ }
11
+ return pulseInstance;
12
+ }
13
+
14
+ /**
15
+ * Get the bearer token stored by Better Auth for Pulse auth.
16
+ */
17
+ export function getPulseToken(): string | null {
18
+ if (typeof window === "undefined") return null;
19
+ return localStorage.getItem("pulse_bearer_token");
20
+ }
21
+
22
+ /**
23
+ * Connect to the Pulse relay with authentication.
24
+ * Uses the Better Auth bearer token for the PLP CONNECT handshake.
25
+ */
26
+ export async function connectWithAuth() {
27
+ const pulse = getPulse();
28
+ const token = getPulseToken();
29
+ const url = process.env.NEXT_PUBLIC_PULSE_URL || "ws://localhost:4001";
30
+
31
+ const conn = await pulse.connect(url, {
32
+ encoding: "json",
33
+ autoReconnect: true,
34
+ ...(token
35
+ ? {
36
+ auth: {
37
+ token,
38
+ provider: "better-auth",
39
+ },
40
+ }
41
+ : {}),
42
+ });
43
+
44
+ return conn;
45
+ }
@@ -0,0 +1,107 @@
1
+ import { relations, sql } from "drizzle-orm";
2
+ import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
3
+
4
+ export const user = sqliteTable("user", {
5
+ id: text("id").primaryKey(),
6
+ name: text("name").notNull(),
7
+ email: text("email").notNull().unique(),
8
+ emailVerified: integer("email_verified", { mode: "boolean" })
9
+ .default(false)
10
+ .notNull(),
11
+ image: text("image"),
12
+ createdAt: integer("created_at", { mode: "timestamp_ms" })
13
+ .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
14
+ .notNull(),
15
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" })
16
+ .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
17
+ .$onUpdate(() => /* @__PURE__ */ new Date())
18
+ .notNull(),
19
+ });
20
+
21
+ export const session = sqliteTable(
22
+ "session",
23
+ {
24
+ id: text("id").primaryKey(),
25
+ expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
26
+ token: text("token").notNull().unique(),
27
+ createdAt: integer("created_at", { mode: "timestamp_ms" })
28
+ .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
29
+ .notNull(),
30
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" })
31
+ .$onUpdate(() => /* @__PURE__ */ new Date())
32
+ .notNull(),
33
+ ipAddress: text("ip_address"),
34
+ userAgent: text("user_agent"),
35
+ userId: text("user_id")
36
+ .notNull()
37
+ .references(() => user.id, { onDelete: "cascade" }),
38
+ },
39
+ (table) => [index("session_userId_idx").on(table.userId)],
40
+ );
41
+
42
+ export const account = sqliteTable(
43
+ "account",
44
+ {
45
+ id: text("id").primaryKey(),
46
+ accountId: text("account_id").notNull(),
47
+ providerId: text("provider_id").notNull(),
48
+ userId: text("user_id")
49
+ .notNull()
50
+ .references(() => user.id, { onDelete: "cascade" }),
51
+ accessToken: text("access_token"),
52
+ refreshToken: text("refresh_token"),
53
+ idToken: text("id_token"),
54
+ accessTokenExpiresAt: integer("access_token_expires_at", {
55
+ mode: "timestamp_ms",
56
+ }),
57
+ refreshTokenExpiresAt: integer("refresh_token_expires_at", {
58
+ mode: "timestamp_ms",
59
+ }),
60
+ scope: text("scope"),
61
+ password: text("password"),
62
+ createdAt: integer("created_at", { mode: "timestamp_ms" })
63
+ .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
64
+ .notNull(),
65
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" })
66
+ .$onUpdate(() => /* @__PURE__ */ new Date())
67
+ .notNull(),
68
+ },
69
+ (table) => [index("account_userId_idx").on(table.userId)],
70
+ );
71
+
72
+ export const verification = sqliteTable(
73
+ "verification",
74
+ {
75
+ id: text("id").primaryKey(),
76
+ identifier: text("identifier").notNull(),
77
+ value: text("value").notNull(),
78
+ expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
79
+ createdAt: integer("created_at", { mode: "timestamp_ms" })
80
+ .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
81
+ .notNull(),
82
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" })
83
+ .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
84
+ .$onUpdate(() => /* @__PURE__ */ new Date())
85
+ .notNull(),
86
+ },
87
+ (table) => [index("verification_identifier_idx").on(table.identifier)],
88
+ );
89
+
90
+ export const userRelations = relations(user, ({ many }) => ({
91
+ sessions: many(session),
92
+ accounts: many(account),
93
+ }));
94
+
95
+ export const sessionRelations = relations(session, ({ one }) => ({
96
+ user: one(user, {
97
+ fields: [session.userId],
98
+ references: [user.id],
99
+ }),
100
+ }));
101
+
102
+ export const accountRelations = relations(account, ({ one }) => ({
103
+ user: one(user, {
104
+ fields: [account.userId],
105
+ references: [user.id],
106
+ }),
107
+ }));
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }
@@ -1,6 +1,6 @@
1
1
  # Pulse Queue Demo
2
2
 
3
- A demo app showcasing **Pulse v0.4.0** durable message queues.
3
+ A demo app showcasing **Pulse v0.4.5** durable message queues.
4
4
 
5
5
  ## Features
6
6
 
@@ -20,16 +20,16 @@ In a separate terminal, start Pulse with your preferred queue backend:
20
20
 
21
21
  ```bash
22
22
  # In-memory (default, ephemeral)
23
- pulse dev
23
+ pulse serve
24
24
 
25
25
  # WAL (Write-Ahead Log — crash-resilient)
26
- PULSE_QUEUE_BACKEND=wal pulse dev
26
+ pulse serve --queue-storage wal
27
27
 
28
28
  # PostgreSQL
29
- PULSE_QUEUE_BACKEND=postgres PULSE_QUEUE_POSTGRES_URL=postgres://user:pass@localhost/pulse pulse dev
29
+ pulse serve --queue-storage postgres --database-url postgres://user:pass@localhost/pulse
30
30
 
31
31
  # Redis
32
- PULSE_QUEUE_BACKEND=redis PULSE_QUEUE_REDIS_URL=redis://localhost:6379 pulse dev
32
+ pulse serve --queue-storage redis --redis-url redis://localhost:6379
33
33
  ```
34
34
 
35
35
  ## Encryption at Rest
@@ -37,8 +37,7 @@ PULSE_QUEUE_BACKEND=redis PULSE_QUEUE_REDIS_URL=redis://localhost:6379 pulse dev
37
37
  Add encryption to any backend:
38
38
 
39
39
  ```bash
40
- export PULSE_QUEUE_KEY=$(openssl rand -hex 32)
41
- PULSE_QUEUE_BACKEND=wal pulse dev
40
+ pulse serve --queue-storage wal --queue-encryption-key $(openssl rand -hex 32)
42
41
  ```
43
42
 
44
43
  All payloads are encrypted with ChaCha20-Poly1305 before reaching storage.
@@ -36,7 +36,7 @@ const TTL_OPTIONS = [
36
36
  ]
37
37
 
38
38
  export default function App() {
39
- const [connected, setConnected] = useState(false)
39
+ const [connectionState, setConnectionState] = useState<'connecting' | 'connected' | 'reconnecting' | 'disconnected'>('connecting')
40
40
  const [messages, setMessages] = useState<DisplayMessage[]>([])
41
41
  const [input, setInput] = useState('')
42
42
  const [rtt, setRtt] = useState<number | null>(null)
@@ -47,35 +47,54 @@ export default function App() {
47
47
  const connRef = useRef<PulseConnection | null>(null)
48
48
  const queueRef = useRef<PulseQueue | null>(null)
49
49
 
50
+ const connected = connectionState === 'connected'
51
+
52
+ // Helper: drain persisted messages and refresh depth
53
+ const recoverMessages = useCallback(async (queue: PulseQueue) => {
54
+ try {
55
+ const recovered = await queue.drain()
56
+ if (recovered.length > 0) {
57
+ setMessages(recovered.map((m) => ({ ...m, state: 'pending' as const })))
58
+ }
59
+ await refreshDepth(queue)
60
+ } catch {
61
+ // Queue may not exist yet — that's fine, it'll be auto-created
62
+ }
63
+ }, [])
64
+
50
65
  // Connect + auto-fetch existing messages on mount
66
+ // Auto-reconnect is enabled by default in the SDK — the protocol "just works"
51
67
  useEffect(() => {
52
68
  const pulse = new Pulse({ apiKey: 'dev' })
53
69
 
70
+ setConnectionState('connecting')
71
+
54
72
  pulse.connect('ws://localhost:4001').then(async (connection) => {
55
73
  connRef.current = connection
56
74
  const queue = new PulseQueue(connection, QUEUE_NAME, 'demo-ui')
57
75
  queueRef.current = queue
58
- setConnected(true)
76
+ setConnectionState('connected')
59
77
 
60
78
  // Live metrics
61
79
  connection.on('metrics', (m: ConnectionMetrics) => setRtt(m.rtt))
62
80
 
63
- // Drain any persisted messages from a previous session
64
- const recovered = await queue.drain()
65
- if (recovered.length > 0) {
66
- setMessages(recovered.map((m) => ({ ...m, state: 'pending' as const })))
67
- }
81
+ // Connection lifecycle events
82
+ connection.on('disconnect', () => setConnectionState('reconnecting'))
83
+ connection.on('reconnecting', () => setConnectionState('reconnecting'))
84
+ connection.on('reconnected', async () => {
85
+ setConnectionState('connected')
86
+ // Re-drain persisted messages after reconnect
87
+ await recoverMessages(queue)
88
+ })
68
89
 
69
- // Refresh depth
70
- await refreshDepth(queue)
71
- }).catch((err: Error) => {
72
- console.error('Failed to connect:', err)
90
+ // Drain any persisted messages from a previous session
91
+ await recoverMessages(queue)
73
92
  })
74
93
 
75
94
  return () => {
76
95
  connRef.current?.disconnect()
77
96
  }
78
- }, [])
97
+ }, [recoverMessages])
79
98
 
80
99
  const refreshDepth = async (queue?: PulseQueue) => {
81
100
  const q = queue || queueRef.current
@@ -162,8 +181,10 @@ export default function App() {
162
181
  <h1>🔄 Pulse Queue Demo</h1>
163
182
  <p style={{ color: '#888' }}>
164
183
  Durable message queues with at-least-once delivery over PLP.
165
- {connected ? (
184
+ {connectionState === 'connected' ? (
166
185
  <span style={{ color: '#4ade80' }}> ● Connected</span>
186
+ ) : connectionState === 'reconnecting' ? (
187
+ <span style={{ color: '#fbbf24' }}> ◌ Reconnecting...</span>
167
188
  ) : (
168
189
  <span style={{ color: '#f87171' }}> ○ Connecting...</span>
169
190
  )}