@supalive/core 0.1.3 → 0.1.5

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 (212) hide show
  1. package/README.md +308 -1
  2. package/dist/helper-CiacMqje.d.ts +14 -0
  3. package/dist/helper-CiacMqje.d.ts.map +1 -0
  4. package/dist/helper-zdJT5FUc.js +58 -0
  5. package/dist/helper-zdJT5FUc.js.map +1 -0
  6. package/dist/index-CTyXKcq_.d.ts +1736 -0
  7. package/dist/index-CTyXKcq_.d.ts.map +1 -0
  8. package/dist/mysql-C8SN443m.d.ts +104 -0
  9. package/dist/mysql-C8SN443m.d.ts.map +1 -0
  10. package/dist/mysql-DELSRrpY.js +569 -0
  11. package/dist/mysql-DELSRrpY.js.map +1 -0
  12. package/dist/overlap-checker-CVkbbuEQ.js +187 -0
  13. package/dist/overlap-checker-CVkbbuEQ.js.map +1 -0
  14. package/dist/postgres-DE-XyNAQ.js +823 -0
  15. package/dist/postgres-DE-XyNAQ.js.map +1 -0
  16. package/dist/postgres-D_gwCHx-.d.ts +108 -0
  17. package/dist/postgres-D_gwCHx-.d.ts.map +1 -0
  18. package/dist/query-CbQLjeME.js +713 -0
  19. package/dist/query-CbQLjeME.js.map +1 -0
  20. package/dist/realtime_db-D9p2X2PH.js +246 -0
  21. package/dist/realtime_db-D9p2X2PH.js.map +1 -0
  22. package/dist/router-DlTYWpop.js +81 -0
  23. package/dist/router-DlTYWpop.js.map +1 -0
  24. package/dist/schema-SKcIWHfo.js +167 -0
  25. package/dist/schema-SKcIWHfo.js.map +1 -0
  26. package/dist/src/client/index.d.ts +2 -17
  27. package/dist/src/client/index.js +1151 -18
  28. package/dist/src/client/index.js.map +1 -1
  29. package/dist/src/exports/mysql.d.ts +2 -2
  30. package/dist/src/exports/mysql.js +2 -2
  31. package/dist/src/exports/postgres.d.ts +2 -2
  32. package/dist/src/exports/postgres.js +2 -2
  33. package/dist/src/exports/procedure.d.ts +3 -5
  34. package/dist/src/exports/procedure.js +5 -5
  35. package/dist/src/exports/server.d.ts +368 -2
  36. package/dist/src/exports/server.d.ts.map +1 -1
  37. package/dist/src/exports/server.js +1346 -2
  38. package/dist/src/exports/server.js.map +1 -1
  39. package/dist/src/exports/subscription-manager-worker-entry.d.ts +1 -0
  40. package/dist/src/exports/subscription-manager-worker-entry.js +115 -0
  41. package/dist/src/exports/subscription-manager-worker-entry.js.map +1 -0
  42. package/dist/src/exports/types.d.ts +4 -6
  43. package/dist/src/exports/types.js +5 -6
  44. package/dist/subscription-worker-BurdPO8d.js +511 -0
  45. package/dist/subscription-worker-BurdPO8d.js.map +1 -0
  46. package/dist/types_client_rpc-CIsjQQGG.js +105 -0
  47. package/dist/types_client_rpc-CIsjQQGG.js.map +1 -0
  48. package/dist/types_db-Ctf-ksoy.js +168 -0
  49. package/dist/types_db-Ctf-ksoy.js.map +1 -0
  50. package/dist/types_server-Ch56y8L3.d.ts +216 -0
  51. package/dist/types_server-Ch56y8L3.d.ts.map +1 -0
  52. package/dist/types_server-Dk6o_B7N.js +24 -0
  53. package/dist/types_server-Dk6o_B7N.js.map +1 -0
  54. package/package.json +3 -3
  55. package/dist/src/client/caller_client.d.ts +0 -42
  56. package/dist/src/client/caller_client.d.ts.map +0 -1
  57. package/dist/src/client/caller_client.js +0 -77
  58. package/dist/src/client/caller_client.js.map +0 -1
  59. package/dist/src/client/client.d.ts +0 -149
  60. package/dist/src/client/client.d.ts.map +0 -1
  61. package/dist/src/client/client.js +0 -308
  62. package/dist/src/client/client.js.map +0 -1
  63. package/dist/src/client/index.d.ts.map +0 -1
  64. package/dist/src/client/sub_key.d.ts +0 -20
  65. package/dist/src/client/sub_key.d.ts.map +0 -1
  66. package/dist/src/client/sub_key.js +0 -37
  67. package/dist/src/client/sub_key.js.map +0 -1
  68. package/dist/src/client/ws_client_manager.d.ts +0 -268
  69. package/dist/src/client/ws_client_manager.d.ts.map +0 -1
  70. package/dist/src/client/ws_client_manager.js +0 -1013
  71. package/dist/src/client/ws_client_manager.js.map +0 -1
  72. package/dist/src/config.d.ts +0 -117
  73. package/dist/src/config.d.ts.map +0 -1
  74. package/dist/src/config.js +0 -2
  75. package/dist/src/config.js.map +0 -1
  76. package/dist/src/db/bytes.d.ts +0 -4
  77. package/dist/src/db/bytes.d.ts.map +0 -1
  78. package/dist/src/db/bytes.js +0 -24
  79. package/dist/src/db/bytes.js.map +0 -1
  80. package/dist/src/db/cache.d.ts +0 -30
  81. package/dist/src/db/cache.d.ts.map +0 -1
  82. package/dist/src/db/cache.js +0 -153
  83. package/dist/src/db/cache.js.map +0 -1
  84. package/dist/src/db/codec.d.ts +0 -10
  85. package/dist/src/db/codec.d.ts.map +0 -1
  86. package/dist/src/db/codec.js +0 -73
  87. package/dist/src/db/codec.js.map +0 -1
  88. package/dist/src/db/context.d.ts +0 -28
  89. package/dist/src/db/context.d.ts.map +0 -1
  90. package/dist/src/db/context.js +0 -147
  91. package/dist/src/db/context.js.map +0 -1
  92. package/dist/src/db/db.d.ts +0 -154
  93. package/dist/src/db/db.d.ts.map +0 -1
  94. package/dist/src/db/db.js +0 -23
  95. package/dist/src/db/db.js.map +0 -1
  96. package/dist/src/db/mysql.d.ts +0 -120
  97. package/dist/src/db/mysql.d.ts.map +0 -1
  98. package/dist/src/db/mysql.js +0 -708
  99. package/dist/src/db/mysql.js.map +0 -1
  100. package/dist/src/db/postgres.d.ts +0 -116
  101. package/dist/src/db/postgres.d.ts.map +0 -1
  102. package/dist/src/db/postgres.js +0 -998
  103. package/dist/src/db/postgres.js.map +0 -1
  104. package/dist/src/db/query.d.ts +0 -174
  105. package/dist/src/db/query.d.ts.map +0 -1
  106. package/dist/src/db/query.js +0 -790
  107. package/dist/src/db/query.js.map +0 -1
  108. package/dist/src/db/query_definition.d.ts +0 -54
  109. package/dist/src/db/query_definition.d.ts.map +0 -1
  110. package/dist/src/db/query_definition.js +0 -47
  111. package/dist/src/db/query_definition.js.map +0 -1
  112. package/dist/src/db/realtime_db.d.ts +0 -19
  113. package/dist/src/db/realtime_db.d.ts.map +0 -1
  114. package/dist/src/db/realtime_db.js +0 -98
  115. package/dist/src/db/realtime_db.js.map +0 -1
  116. package/dist/src/db/schema.d.ts +0 -110
  117. package/dist/src/db/schema.d.ts.map +0 -1
  118. package/dist/src/db/schema.js +0 -47
  119. package/dist/src/db/schema.js.map +0 -1
  120. package/dist/src/db/types_client_rpc.d.ts +0 -157
  121. package/dist/src/db/types_client_rpc.d.ts.map +0 -1
  122. package/dist/src/db/types_client_rpc.js +0 -106
  123. package/dist/src/db/types_client_rpc.js.map +0 -1
  124. package/dist/src/db/types_db.d.ts +0 -236
  125. package/dist/src/db/types_db.d.ts.map +0 -1
  126. package/dist/src/db/types_db.js +0 -188
  127. package/dist/src/db/types_db.js.map +0 -1
  128. package/dist/src/exports/mysql.d.ts.map +0 -1
  129. package/dist/src/exports/mysql.js.map +0 -1
  130. package/dist/src/exports/postgres.d.ts.map +0 -1
  131. package/dist/src/exports/postgres.js.map +0 -1
  132. package/dist/src/exports/procedure.d.ts.map +0 -1
  133. package/dist/src/exports/procedure.js.map +0 -1
  134. package/dist/src/exports/types.d.ts.map +0 -1
  135. package/dist/src/exports/types.js.map +0 -1
  136. package/dist/src/helper.d.ts +0 -11
  137. package/dist/src/helper.d.ts.map +0 -1
  138. package/dist/src/helper.js +0 -45
  139. package/dist/src/helper.js.map +0 -1
  140. package/dist/src/logger.d.ts +0 -7
  141. package/dist/src/logger.d.ts.map +0 -1
  142. package/dist/src/logger.js +0 -45
  143. package/dist/src/logger.js.map +0 -1
  144. package/dist/src/router/index.d.ts +0 -4
  145. package/dist/src/router/index.d.ts.map +0 -1
  146. package/dist/src/router/index.js +0 -4
  147. package/dist/src/router/index.js.map +0 -1
  148. package/dist/src/router/procedure.d.ts +0 -204
  149. package/dist/src/router/procedure.d.ts.map +0 -1
  150. package/dist/src/router/procedure.js +0 -122
  151. package/dist/src/router/procedure.js.map +0 -1
  152. package/dist/src/router/router.d.ts +0 -88
  153. package/dist/src/router/router.d.ts.map +0 -1
  154. package/dist/src/router/router.js +0 -88
  155. package/dist/src/router/router.js.map +0 -1
  156. package/dist/src/server/hash.d.ts +0 -12
  157. package/dist/src/server/hash.d.ts.map +0 -1
  158. package/dist/src/server/hash.js +0 -20
  159. package/dist/src/server/hash.js.map +0 -1
  160. package/dist/src/server/hash.test.d.ts +0 -2
  161. package/dist/src/server/hash.test.d.ts.map +0 -1
  162. package/dist/src/server/hash.test.js +0 -43
  163. package/dist/src/server/hash.test.js.map +0 -1
  164. package/dist/src/server/index.d.ts +0 -8
  165. package/dist/src/server/index.d.ts.map +0 -1
  166. package/dist/src/server/index.js +0 -7
  167. package/dist/src/server/index.js.map +0 -1
  168. package/dist/src/server/overlap-checker.d.ts +0 -18
  169. package/dist/src/server/overlap-checker.d.ts.map +0 -1
  170. package/dist/src/server/overlap-checker.js +0 -247
  171. package/dist/src/server/overlap-checker.js.map +0 -1
  172. package/dist/src/server/sub_hash.d.ts +0 -23
  173. package/dist/src/server/sub_hash.d.ts.map +0 -1
  174. package/dist/src/server/sub_hash.js +0 -38
  175. package/dist/src/server/sub_hash.js.map +0 -1
  176. package/dist/src/server/subscription-manager-client.d.ts +0 -120
  177. package/dist/src/server/subscription-manager-client.d.ts.map +0 -1
  178. package/dist/src/server/subscription-manager-client.js +0 -207
  179. package/dist/src/server/subscription-manager-client.js.map +0 -1
  180. package/dist/src/server/subscription-manager-types.d.ts +0 -168
  181. package/dist/src/server/subscription-manager-types.d.ts.map +0 -1
  182. package/dist/src/server/subscription-manager-types.js +0 -94
  183. package/dist/src/server/subscription-manager-types.js.map +0 -1
  184. package/dist/src/server/subscription-manager-worker-entry.d.ts +0 -2
  185. package/dist/src/server/subscription-manager-worker-entry.d.ts.map +0 -1
  186. package/dist/src/server/subscription-manager-worker-entry.js +0 -127
  187. package/dist/src/server/subscription-manager-worker-entry.js.map +0 -1
  188. package/dist/src/server/subscription-manager-worker-protocol.d.ts +0 -40
  189. package/dist/src/server/subscription-manager-worker-protocol.d.ts.map +0 -1
  190. package/dist/src/server/subscription-manager-worker-protocol.js +0 -20
  191. package/dist/src/server/subscription-manager-worker-protocol.js.map +0 -1
  192. package/dist/src/server/subscription-manager.d.ts +0 -32
  193. package/dist/src/server/subscription-manager.d.ts.map +0 -1
  194. package/dist/src/server/subscription-manager.js +0 -249
  195. package/dist/src/server/subscription-manager.js.map +0 -1
  196. package/dist/src/server/subscription-manager.test.d.ts +0 -2
  197. package/dist/src/server/subscription-manager.test.d.ts.map +0 -1
  198. package/dist/src/server/subscription-manager.test.js +0 -610
  199. package/dist/src/server/subscription-manager.test.js.map +0 -1
  200. package/dist/src/server/subscription-worker.d.ts +0 -32
  201. package/dist/src/server/subscription-worker.d.ts.map +0 -1
  202. package/dist/src/server/subscription-worker.js +0 -190
  203. package/dist/src/server/subscription-worker.js.map +0 -1
  204. package/dist/src/server/supalive-server.d.ts +0 -158
  205. package/dist/src/server/supalive-server.d.ts.map +0 -1
  206. package/dist/src/server/supalive-server.js +0 -1048
  207. package/dist/src/server/supalive-server.js.map +0 -1
  208. package/dist/src/server/types_server.d.ts +0 -96
  209. package/dist/src/server/types_server.d.ts.map +0 -1
  210. package/dist/src/server/types_server.js +0 -36
  211. package/dist/src/server/types_server.js.map +0 -1
  212. package/dist/tsconfig.tsbuildinfo +0 -1
package/README.md CHANGED
@@ -1,7 +1,22 @@
1
+ <div align="center">
2
+ <img src="../../supalive.png" alt="Supalive" width="420" />
3
+
1
4
  # @supalive/core
2
5
 
3
6
  Core package for supalive — a reactive database framework.
4
7
 
8
+ </div>
9
+
10
+ **Reactive queries for any Postgres/MySQL.**
11
+
12
+ Write a typed query once. The client re-renders the moment a mutation touches a row that would change the result — no triggers, no `LISTEN/NOTIFY` wiring, no manual cache invalidation.
13
+
14
+ [Live demo](#try-the-demo-locally) · [How it works](#how-it-works) · [Architecture](./examples/basic/README.md)
15
+
16
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](#license)
17
+ [![node](https://img.shields.io/badge/node-%3E%3D24-339933.svg)](package.json)
18
+ [![status](https://img.shields.io/badge/status-alpha-orange.svg)](#status)
19
+
5
20
  ## Installation
6
21
 
7
22
  ```bash
@@ -17,6 +32,298 @@ npm install @supalive/core
17
32
  - `@supalive/core/server` - Server implementation
18
33
  - `@supalive/core/client` - Client implementation
19
34
 
35
+
36
+
37
+
38
+ ---
39
+
40
+ ## What it is
41
+
42
+ Supalive sits between your app and your database and turns any query into a live query.
43
+
44
+ You define procedures the same way you'd define a tRPC router. Mark one as a `query` and any client that calls `useLiveQuery` on it gets a stream of updates. When a `mutation` commits and its writes overlap that query's read set, Supalive recomputes and pushes the new result. Everything is typed end-to-end from the database column to the React hook.
45
+
46
+ It does **not** replace your database. It runs on top of plain Postgres or MySQL — including Supabase Postgres, Neon, RDS, PlanetScale — without schema changes.
47
+
48
+ ## The 30-second version
49
+
50
+ **1. Define a query and a mutation.**
51
+
52
+ ```ts
53
+ // procedures.ts
54
+ import { z } from "zod";
55
+ import { createQueryBuilder, createMutationBuilder, defineSchema, router } from "@supalive/core/procedure";
56
+
57
+ const query = createQueryBuilder<ServerContext>();
58
+ const mutation = createMutationBuilder<ServerContext>();
59
+
60
+ const ItemSchema = defineSchema({
61
+ name: "items",
62
+ schema: {
63
+ id: z.string(),
64
+ title: z.string(),
65
+ status: z.enum(["todo", "in_progress", "done"]),
66
+ ownerId: z.string(),
67
+ },
68
+ columns: { ownerId: "owner_id" },
69
+ });
70
+
71
+ export const listItems = query({
72
+ args: z.object({ status: z.enum(["todo", "in_progress", "done"]).optional() }),
73
+ handler: async (ctx, { status }) => {
74
+ let q = ctx.db.query(ItemSchema).select()
75
+ .where((f) => f.eq("ownerId", ctx.serverCtx.user.userId));
76
+ if (status) q = q.where((f) => f.eq("status", status));
77
+ return q.orderBy("createdAt", "desc").limit(50).get();
78
+ },
79
+ });
80
+
81
+ export const createItem = mutation({
82
+ args: z.object({ id: z.string(), title: z.string(), status: z.enum(["todo", "in_progress", "done"]) }),
83
+ handler: async (ctx, args) => {
84
+ await ctx.db.insert(ItemSchema, args.id, {
85
+ ...args,
86
+ ownerId: ctx.serverCtx.user.userId,
87
+ createdAt: new Date().toISOString(),
88
+ updatedAt: new Date().toISOString(),
89
+ });
90
+ },
91
+ });
92
+
93
+ export const appRouter = router({ procedures: { listItems, createItem } });
94
+ ```
95
+
96
+ **2. Start the server.**
97
+
98
+ ```ts
99
+ // server.ts
100
+ import { SupaliveWebSocketServer } from "@supalive/core/server";
101
+ import { appRouter } from "./procedures";
102
+
103
+ const { cacheLayer, redisSubClient } = initCacheLayer({
104
+ upstash: {
105
+ url: `https://${process.env.UPSTASH_REDIS_HOST}`,
106
+ token: process.env.UPSTASH_REDIS_TOKEN ?? "",
107
+ redisUrl: process.env.UPSTASH_REDIS_URL ?? ""
108
+ },
109
+ cacheTtlSeconds: 24 * 3600,
110
+ });
111
+ const db = await initDatabase({
112
+ DbConfig: { type: "postgres", connectionString: process.env.DATABASE_URL || '', },
113
+ CacheLayer: cacheLayer,
114
+ forceBootstrapReload: false,
115
+ });
116
+
117
+ const server = new SupaliveWebSocketServer({
118
+ port: 3000,
119
+ cacheLayer: cacheLayer,
120
+ redisSubClient: redisSubClient,
121
+ database: db,
122
+ subManagerUrl: process.env.SUB_MANAGER_URL!,
123
+ verifyAuth: async (data) => verifyJwt(data.token),
124
+ });
125
+ server.registerRouter(appRouter);
126
+ await server.start();
127
+ ```
128
+
129
+ **3. Use it on the client.**
130
+
131
+ ```tsx
132
+ // App.tsx
133
+ import { useLiveQuery, useMutation } from "./supalive";
134
+
135
+ function Todos() {
136
+ const { data, status } = useLiveQuery((c) => c.listItems, { status: "todo" });
137
+ const [createItem] = useMutation((c) => c.createItem);
138
+
139
+ return (
140
+ <>
141
+ {data?.map((item) => <li key={item.id}>{item.title}</li>)}
142
+ <button onClick={() => createItem({ id: crypto.randomUUID(), title: "new", status: "todo" })}>
143
+ add
144
+ </button>
145
+ </>
146
+ );
147
+ }
148
+ ```
149
+
150
+ Open this page in two tabs. Click **add** in one. Watch the list grow in both, automatically.
151
+
152
+ ## Why this exists
153
+
154
+ Postgres `LISTEN/NOTIFY` is row-level — you get a notification, you still have to figure out which queries it affects. Supabase Realtime is great for broadcast, presence, and a row-level changefeed, but it doesn't answer the question every reactive app actually has: *"re-run this exact query when its result would change."*
155
+
156
+ The frameworks that *do* answer that question — Convex, Zero, Replicache, PowerSync — each ship their own database, sync engine, or client-side store. That's a lot to adopt when all you wanted was reactivity on top of the Postgres you already have.
157
+
158
+ Supalive is the smallest possible "reactive query" layer that works on top of your existing database. It's the Convex DX without the Convex lock-in.
159
+
160
+ ## How it works
161
+
162
+ ```
163
+ client supalive server sub-manager (1 process) postgres
164
+ ────── ──────────────── ───────────────────────── ────────
165
+ ┌── worker 0 ──┐
166
+ │ subs slice 0 │
167
+ ├── worker 1 ──┤ ← FNV-1a(subId) % N
168
+ │ subs slice 1 │ routes ops here
169
+ ├── … ────┤
170
+ │ worker N-1 │
171
+ └──────────────┘
172
+
173
+ subscribe ────────▶ register subId ───────────▶ slot in worker; if metadata
174
+ is still valid vs commit log,
175
+ skip recompute
176
+ liveQuery ────────────────────────────────────────────▶ SELECT
177
+ cache { data, metadata }
178
+ sub:update ◀──────── push result
179
+
180
+ call(mutation) ────▶ run handler (DbWriter)
181
+ capture readSet + writeSet (+ prevData)
182
+ OCC commit:
183
+ beginTs = latest snapshot
184
+ getNextTimestamp() ────────────────────────────────▶ commit_ts
185
+ CAS apply + readSet validate ─────────────────────▶ BEGIN…COMMIT
186
+ retries on conflict (cockatiel, max 15)
187
+ invalidateWriteset ────────▶ every worker scans its
188
+ slice; per-sub same-table
189
+ filter + exact predicate
190
+ eval on prev+post row
191
+ ◀── affected subs
192
+ recompute locally (if subs are here)
193
+ OR publish recompute-race
194
+ on sub:instance:<id> ─┐
195
+ SETNX lock:recompute: │ (cross-instance fanout
196
+ <subId>:<commitTs> │ via Redis pub/sub)
197
+ winner runs query, losers │
198
+ wait for update broadcast ─┘
199
+ sub:update ◀───────── { data, dataHash }
200
+ ```
201
+
202
+ Four things make this fast and correct:
203
+
204
+ - **ReadSet + WriteSet capture.** Every query records exactly which point reads and range predicates (with column lists, order, and limit) it touched. Every mutation records the rows it wrote plus their previous values — both sides are needed so the invalidator can detect rows entering, leaving, or changing within a result set.
205
+ - **Sharded scan, not row-by-row recheck.** A single sub-manager process owns all subscription state, partitioned across N logical workers by `FNV-1a(subId) % N` (configurable via `SUPALIVE_SUB_MANAGER_WORKERS`). On commit, each worker scans only its slice; per-sub the check short-circuits on table-id mismatch, then runs an exact 3-valued predicate evaluation on the row's pre- and post-images. New subscribers also benefit: if the worker still holds metadata for a `cacheKey`, it replays `commit_logs` between the cached `snapshotTs` and the caller's `snapshotTs` to decide whether a fresh `liveQuery` is needed at all.
206
+ - **OCC with retries.** Mutations execute against a snapshot, allocate `commitTs` from a DB sequence at apply time, and commit via compare-and-swap with a readSet validator (per-row fast path; commit-log scan when range reads are present). Conflicts retry with exponential backoff (`cockatiel`, up to 15 attempts). The Σ-balance invariant in the money-transfer demo holds under burst contention because of this.
207
+ - **Cross-instance fanout with a deterministic recompute winner.** Multiple Supalive servers behind a load balancer share one sub-manager and one Redis. When a write on instance A affects a subscription whose session lives on instance B, A publishes a `recompute-race` on `sub:instance:<B>`; B and any other holders try to `SETNX lock:recompute:<subId>:<commitTs>` — the winner re-runs the query and broadcasts `sub:update` on `sub:instance:<…>`; losers no-op. A periodic prune trims `commit_logs` below the oldest active subscription's snapshot.
208
+
209
+ Full architecture write-up — including the cache layout, schema-reload broadcast, and sub-manager recovery protocol — lives in [examples/basic/README.md](./examples/basic/README.md).
210
+
211
+ ## Try the demo locally
212
+
213
+ The `examples/basic` workspace ships a 5-tab React demo that shows everything Supalive can do. Each tab is a different stress test of the system.
214
+
215
+ | Tab | What it shows |
216
+ |---|---|
217
+ | **Todos · list** | Live query + create. Toggle the subscription to prove updates only flow when subscribed. |
218
+ | **Todos · cards** | Filter + inline edit. Changing `status` moves a row in or out of the visible set — predicate index handles it. |
219
+ | **Mouse tracking** | High-frequency writes (cursor positions @ ~60ms). Stress test for fanout. |
220
+ | **Money transfer** | OCC under contention. Σ-balance on screen must never drift, no matter how hard you spam transfers. |
221
+ | **Parallel lanes** | Disjoint concurrent transfers. Non-overlapping writers don't block each other. |
222
+
223
+ ```bash
224
+ git clone https://github.com/rebaz94/supalive
225
+ cd supalive
226
+ npm install
227
+
228
+ cp examples/basic/.env.example examples/basic/.env
229
+ # fill in DATABASE_URL, REDIS_URL, UPSTASH_REDIS_TOKEN, JWT_SECRET
230
+
231
+ # terminal 1 — sub-manager
232
+ cd examples/basic && npm run dev:submanager
233
+
234
+ # terminal 2 — websocket server
235
+ cd examples/basic && npm run dev
236
+
237
+ # terminal 3 — web demo
238
+ cd examples/basic && npm run dev:demo
239
+ ```
240
+
241
+ Open the demo URL in two tabs and click around. The whole point is that **nothing in your app code wires the updates** — Supalive does.
242
+
243
+ ## API surface
244
+
245
+ ### Server
246
+
247
+ ```ts
248
+ import { SupaliveWebSocketServer, SubscriptionManager } from "@supalive/core/server";
249
+ import {
250
+ createQueryBuilder, createMutationBuilder, createActionBuilder,
251
+ defineSchema, router,
252
+ } from "@supalive/core/procedure";
253
+ ```
254
+
255
+ - `query` — read-only, subscribable. ReadSet captured automatically.
256
+ - `mutation` — read + write, transactional, OCC-committed.
257
+ - `action` — arbitrary async work (HTTP, queue calls). Can compose queries and mutations via `ctx.db`.
258
+
259
+ ### Client (React)
260
+
261
+ ```ts
262
+ import {
263
+ createSupaliveContext, createSupaliveProvider, createUseSupalive,
264
+ createUseConnectionState, createLiveQuery, createQuery,
265
+ createMutation, createAction,
266
+ } from "@supalive/react";
267
+ ```
268
+
269
+ - `useLiveQuery(c => c.listItems, args)` — subscribe; re-renders on push.
270
+ - `useQuery(c => c.getItem, args)` — one-shot.
271
+ - `useMutation(c => c.createItem)` — returns `[mutate, { status }]`.
272
+ - `useAction(c => c.bulkUpdate)` — returns `[run, { status }]`.
273
+ - `useSupaliveConnectionState()` — `connecting | connected | reconnecting | closed`.
274
+
275
+ The hooks are factory-built so you get fully-typed autocomplete on `c.<procedure>` from your own router type. See [examples/basic/src/web/supalive.tsx](./examples/basic/src/web/supalive.tsx) for the wiring pattern.
276
+
277
+ ## Where it sits
278
+
279
+ | | Supalive | Supabase Realtime | ElectricSQL | PowerSync | Zero | Replicache | Convex |
280
+ |---|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
281
+ | Live queries (re-runs on relevant writes) | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ |
282
+ | Works on your own Postgres / MySQL | ✓ | ✓ | ✓ | ✓ | — | — | — |
283
+ | Server-authoritative auth + validation | ✓ | ✓ | partial | partial | ✓ | partial | ✓ |
284
+ | OCC mutations w/ automatic retry | ✓ | — | — | — | partial | — | ✓ |
285
+ | Typed end-to-end (TS, no codegen) | ✓ | — | — | — | ✓ | ✓ | ✓ |
286
+ | No client-side SQLite required | ✓ | ✓ | — | — | ✓ | ✓ | ✓ |
287
+ | Horizontally scalable out of the box | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
288
+ | MIT, self-hostable | ✓ | ✓ | ✓ | partial | ✓ | — | — |
289
+
290
+ (Categories vary in nuance; this is the executive-summary view. PRs welcome to refine.)
291
+
292
+ ## Production
293
+
294
+ The repo already includes Fly configs for a three-process deployment:
295
+
296
+ - `examples/basic/config/fly.server.toml` — `SupaliveWebSocketServer` (handles client websockets)
297
+ - `examples/basic/config/fly.submanager.toml` — `SubscriptionManager` (predicate index, overlap checks)
298
+ - `examples/basic/config/fly.web.toml` — the demo frontend
299
+
300
+ Multiple `SupaliveWebSocketServer` instances behind a load balancer share state via:
301
+
302
+ - The sub-manager (single source of truth for active subscriptions)
303
+ - Redis pub/sub (cross-instance recompute + cache-invalidation messages)
304
+ - An Upstash Redis cache layer for query results
305
+
306
+ ### Environment variables
307
+
308
+ | Variable | Required | Description |
309
+ |---|---|---|
310
+ | `DATABASE_URL` | yes | Postgres or MySQL connection string |
311
+ | `REDIS_URL` / `UPSTASH_REDIS_URL` | yes | Cache + pub/sub |
312
+ | `UPSTASH_REDIS_TOKEN` | yes | If using Upstash HTTP transport |
313
+ | `SUB_MANAGER_URL` | yes | `ws://…:3003` — where the sub-manager listens |
314
+ | `JWT_SECRET` | yes (example) | The example verifies JWTs with `jose` |
315
+ | `SUPALIVE_DB_QUERY_TIMEOUT_MS` | no | Per-statement timeout, default 30s. `0` to disable |
316
+ | `SUPALIVE_HEARTBEAT_INTERVAL_MS` | no | WebSocket heartbeat, default 15s |
317
+ | `SUPALIVE_RECOMPUTE_LOCK_TTL_SECONDS` | no | Redis recompute lock TTL, default 10s |
318
+ | `SUPALIVE_SUB_MANAGER_WORKERS` | no | Logical workers inside the sub-manager process, default 1. Subscriptions are hash-routed across them |
319
+ | `SUPALIVE_SCHEMA_VERSION` | no | Cache-busting key for column-type bootstrap |
320
+
321
+ ## Status
322
+
323
+ **Alpha.** APIs may change. The implementation is real — there's a working test suite (`packages/test`, `packages/core/src/server/*.test.ts`), the OCC and overlap-detection layers carry production-shaped concerns (statement timeouts, prepared statement cache, schema-reload broadcast, sub-manager recovery), and the demo is end-to-end. But: no semver guarantees yet, no published npm packages, no formal docs site.
324
+
325
+ If you're evaluating Supalive for production, open an issue first — I'd love to know what you're building.
326
+
20
327
  ## License
21
328
 
22
- MIT
329
+ MIT © Rebaz Raouf
@@ -0,0 +1,14 @@
1
+ //#region src/helper.d.ts
2
+ /**
3
+ * Synchronous deterministic JSON stringification.
4
+ * Keys are sorted; `undefined` values are dropped.
5
+ *
6
+ * Suitable for React `useEffect` dependency keys — calling this on the same
7
+ * logical input always produces the same string, regardless of property order.
8
+ */
9
+ declare function stableStringify(input: unknown): string;
10
+ declare function supaliveStringify(input: unknown): string;
11
+ declare function groupByToMap<T, K>(items: Iterable<T>, getKey: (item: T) => K): Map<K, T[]>;
12
+ //#endregion
13
+ export { stableStringify as n, supaliveStringify as r, groupByToMap as t };
14
+ //# sourceMappingURL=helper-CiacMqje.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helper-CiacMqje.d.ts","names":[],"sources":["../src/helper.ts"],"mappings":";;AASA;;;;AAA8C;AAS9C;iBATgB,eAAA,CAAgB,KAAc;AAAA,iBAS9B,iBAAA,CAAkB,KAAc;AAAA,iBAchC,YAAA,OACd,KAAA,EAAO,QAAA,CAAS,CAAA,GAChB,MAAA,GAAS,IAAA,EAAM,CAAA,KAAM,CAAA,GACpB,GAAA,CAAI,CAAA,EAAG,CAAA"}
@@ -0,0 +1,58 @@
1
+ import stableStringifyFn from "json-stable-stringify";
2
+ //#region src/db/bytes.ts
3
+ function bytesEqual(a, b) {
4
+ if (a === b) return true;
5
+ if (a.byteLength !== b.byteLength) return false;
6
+ for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;
7
+ return true;
8
+ }
9
+ const _enc = new TextEncoder();
10
+ const _dec = new TextDecoder("utf-8");
11
+ function utf8Encode(s) {
12
+ return _enc.encode(s);
13
+ }
14
+ function utf8Decode(b) {
15
+ return _dec.decode(b);
16
+ }
17
+ //#endregion
18
+ //#region src/helper.ts
19
+ /**
20
+ * Synchronous deterministic JSON stringification.
21
+ * Keys are sorted; `undefined` values are dropped.
22
+ *
23
+ * Suitable for React `useEffect` dependency keys — calling this on the same
24
+ * logical input always produces the same string, regardless of property order.
25
+ */
26
+ function stableStringify(input) {
27
+ return stableStringifyFn(input, { replacer: (_, v) => {
28
+ if (typeof v === "bigint") return v.toString();
29
+ return v ?? void 0;
30
+ } }) ?? "";
31
+ }
32
+ function supaliveStringify(input) {
33
+ return JSON.stringify(input, (_, v) => {
34
+ if (typeof v === "bigint") return v.toString();
35
+ if (v instanceof Uint8Array) return {
36
+ type: "Buffer",
37
+ data: Array.from(v)
38
+ };
39
+ return v ?? void 0;
40
+ });
41
+ }
42
+ function groupByToMap(items, getKey) {
43
+ const map = /* @__PURE__ */ new Map();
44
+ for (const item of items) {
45
+ const key = getKey(item);
46
+ let group = map.get(key);
47
+ if (!group) {
48
+ group = [];
49
+ map.set(key, group);
50
+ }
51
+ group.push(item);
52
+ }
53
+ return map;
54
+ }
55
+ //#endregion
56
+ export { utf8Decode as a, bytesEqual as i, stableStringify as n, utf8Encode as o, supaliveStringify as r, groupByToMap as t };
57
+
58
+ //# sourceMappingURL=helper-zdJT5FUc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helper-zdJT5FUc.js","names":[],"sources":["../src/db/bytes.ts","../src/helper.ts"],"sourcesContent":["// Small helpers for byte arrays. We standardize on Uint8Array internally so\n// values survive structured clone (worker_threads postMessage) and stay\n// browser-safe. Buffer is a Uint8Array subclass, so values from pg/mysql\n// drivers satisfy Uint8Array fields automatically.\n\nexport function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a === b) return true;\n if (a.byteLength !== b.byteLength) return false;\n for (let i = 0; i < a.byteLength; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\nconst _enc = new TextEncoder();\nconst _dec = new TextDecoder(\"utf-8\");\n\nexport function utf8Encode(s: string): Uint8Array {\n return _enc.encode(s);\n}\n\nexport function utf8Decode(b: Uint8Array): string {\n return _dec.decode(b);\n}\n","import stableStringifyFn from \"json-stable-stringify\";\n\n/**\n * Synchronous deterministic JSON stringification.\n * Keys are sorted; `undefined` values are dropped.\n *\n * Suitable for React `useEffect` dependency keys — calling this on the same\n * logical input always produces the same string, regardless of property order.\n */\nexport function stableStringify(input: unknown): string {\n return stableStringifyFn(input, {\n replacer: (_, v) => {\n if (typeof v === \"bigint\") return v.toString();\n return v ?? undefined;\n }\n }) ?? \"\";\n}\n\nexport function supaliveStringify(input: unknown): string {\n return JSON.stringify(input, (_, v) => {\n if (typeof v === \"bigint\") return v.toString();\n // Emit a stable wire shape for byte arrays regardless of whether the\n // producer holds a Buffer or a plain Uint8Array (the latter is what\n // crosses worker_threads postMessage). The matching decoder lives in\n // `bytesFromJson` in db/types_db.ts.\n if (v instanceof Uint8Array) {\n return { type: \"Buffer\", data: Array.from(v) };\n }\n return v ?? undefined;\n });\n}\n\nexport function groupByToMap<T, K>(\n items: Iterable<T>,\n getKey: (item: T) => K,\n): Map<K, T[]> {\n const map = new Map<K, T[]>();\n\n for (const item of items) {\n const key = getKey(item);\n\n let group = map.get(key);\n if (!group) {\n group = [];\n map.set(key, group);\n }\n\n group.push(item);\n }\n\n return map;\n}\n"],"mappings":";;AAKA,SAAgB,WAAW,GAAe,GAAwB;CAChE,IAAI,MAAM,GAAG,OAAO;CACpB,IAAI,EAAE,eAAe,EAAE,YAAY,OAAO;CAC1C,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE,YAAY,KAChC,IAAI,EAAE,OAAO,EAAE,IAAI,OAAO;CAE5B,OAAO;AACT;AAEA,MAAM,OAAO,IAAI,YAAY;AAC7B,MAAM,OAAO,IAAI,YAAY,OAAO;AAEpC,SAAgB,WAAW,GAAuB;CAChD,OAAO,KAAK,OAAO,CAAC;AACtB;AAEA,SAAgB,WAAW,GAAuB;CAChD,OAAO,KAAK,OAAO,CAAC;AACtB;;;;;;;;;;ACdA,SAAgB,gBAAgB,OAAwB;CACtD,OAAO,kBAAkB,OAAO,EAC9B,WAAW,GAAG,MAAM;EAClB,IAAI,OAAO,MAAM,UAAU,OAAO,EAAE,SAAS;EAC7C,OAAO,KAAK,KAAA;CACd,EACF,CAAC,KAAK;AACR;AAEA,SAAgB,kBAAkB,OAAwB;CACxD,OAAO,KAAK,UAAU,QAAQ,GAAG,MAAM;EACrC,IAAI,OAAO,MAAM,UAAU,OAAO,EAAE,SAAS;EAK7C,IAAI,aAAa,YACf,OAAO;GAAE,MAAM;GAAU,MAAM,MAAM,KAAK,CAAC;EAAE;EAE/C,OAAO,KAAK,KAAA;CACd,CAAC;AACH;AAEA,SAAgB,aACd,OACA,QACa;CACb,MAAM,sBAAM,IAAI,IAAY;CAE5B,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,MAAM,OAAO,IAAI;EAEvB,IAAI,QAAQ,IAAI,IAAI,GAAG;EACvB,IAAI,CAAC,OAAO;GACV,QAAQ,CAAC;GACT,IAAI,IAAI,KAAK,KAAK;EACpB;EAEA,MAAM,KAAK,IAAI;CACjB;CAEA,OAAO;AACT"}