@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.
- package/README.md +27 -3
- package/dist/index.js +3 -1
- package/package.json +1 -1
- package/templates/nextjs-auth-demo/.env.example +6 -0
- package/templates/nextjs-auth-demo/README.md +125 -0
- package/templates/nextjs-auth-demo/_gitignore +33 -0
- package/templates/nextjs-auth-demo/drizzle.config.ts +10 -0
- package/templates/nextjs-auth-demo/eslint.config.mjs +18 -0
- package/templates/nextjs-auth-demo/next-env.d.ts +6 -0
- package/templates/nextjs-auth-demo/next.config.ts +7 -0
- package/templates/nextjs-auth-demo/package.json +36 -0
- package/templates/nextjs-auth-demo/postcss.config.mjs +7 -0
- package/templates/nextjs-auth-demo/public/file.svg +1 -0
- package/templates/nextjs-auth-demo/public/globe.svg +1 -0
- package/templates/nextjs-auth-demo/public/next.svg +1 -0
- package/templates/nextjs-auth-demo/public/vercel.svg +1 -0
- package/templates/nextjs-auth-demo/public/window.svg +1 -0
- package/templates/nextjs-auth-demo/src/app/api/auth/[...all]/route.ts +4 -0
- package/templates/nextjs-auth-demo/src/app/api/pulse/verify/route.ts +54 -0
- package/templates/nextjs-auth-demo/src/app/auth/sign-in/page.tsx +131 -0
- package/templates/nextjs-auth-demo/src/app/auth/sign-up/page.tsx +153 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +248 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +198 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +192 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/queues/page.tsx +297 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +258 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/layout.tsx +109 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/page.tsx +147 -0
- package/templates/nextjs-auth-demo/src/app/favicon.ico +0 -0
- package/templates/nextjs-auth-demo/src/app/globals.css +96 -0
- package/templates/nextjs-auth-demo/src/app/layout.tsx +27 -0
- package/templates/nextjs-auth-demo/src/app/page.tsx +254 -0
- package/templates/nextjs-auth-demo/src/lib/auth-client.ts +15 -0
- package/templates/nextjs-auth-demo/src/lib/auth.ts +14 -0
- package/templates/nextjs-auth-demo/src/lib/db.ts +6 -0
- package/templates/nextjs-auth-demo/src/lib/pulse.ts +45 -0
- package/templates/nextjs-auth-demo/src/lib/schema.ts +107 -0
- package/templates/nextjs-auth-demo/tsconfig.json +34 -0
- package/templates/react-queue-demo/README.md +6 -7
- package/templates/react-queue-demo/src/App.tsx +34 -13
- 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,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.
|
|
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
|
|
23
|
+
pulse serve
|
|
24
24
|
|
|
25
25
|
# WAL (Write-Ahead Log — crash-resilient)
|
|
26
|
-
|
|
26
|
+
pulse serve --queue-storage wal
|
|
27
27
|
|
|
28
28
|
# PostgreSQL
|
|
29
|
-
|
|
29
|
+
pulse serve --queue-storage postgres --database-url postgres://user:pass@localhost/pulse
|
|
30
30
|
|
|
31
31
|
# Redis
|
|
32
|
-
|
|
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
|
-
|
|
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 [
|
|
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
|
-
|
|
76
|
+
setConnectionState('connected')
|
|
59
77
|
|
|
60
78
|
// Live metrics
|
|
61
79
|
connection.on('metrics', (m: ConnectionMetrics) => setRtt(m.rtt))
|
|
62
80
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
//
|
|
70
|
-
await
|
|
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
|
)}
|