@risingwave/wavelet-server 0.2.4 → 0.2.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 (72) hide show
  1. package/dist/__tests__/cursor-parsing.test.d.ts +2 -0
  2. package/dist/__tests__/cursor-parsing.test.d.ts.map +1 -0
  3. package/dist/__tests__/cursor-parsing.test.js +64 -0
  4. package/dist/__tests__/cursor-parsing.test.js.map +1 -0
  5. package/dist/__tests__/http-api.test.d.ts +2 -0
  6. package/dist/__tests__/http-api.test.d.ts.map +1 -0
  7. package/dist/__tests__/http-api.test.js +135 -0
  8. package/dist/__tests__/http-api.test.js.map +1 -0
  9. package/dist/__tests__/integration.test.d.ts +2 -0
  10. package/dist/__tests__/integration.test.d.ts.map +1 -0
  11. package/dist/__tests__/integration.test.js +229 -0
  12. package/dist/__tests__/integration.test.js.map +1 -0
  13. package/dist/__tests__/jwt.test.d.ts +2 -0
  14. package/dist/__tests__/jwt.test.d.ts.map +1 -0
  15. package/dist/__tests__/jwt.test.js +86 -0
  16. package/dist/__tests__/jwt.test.js.map +1 -0
  17. package/dist/__tests__/ws-fanout.test.d.ts +2 -0
  18. package/dist/__tests__/ws-fanout.test.d.ts.map +1 -0
  19. package/dist/__tests__/ws-fanout.test.js +127 -0
  20. package/dist/__tests__/ws-fanout.test.js.map +1 -0
  21. package/dist/config-loader.d.ts +3 -0
  22. package/dist/config-loader.d.ts.map +1 -0
  23. package/dist/config-loader.js +25 -0
  24. package/dist/config-loader.js.map +1 -0
  25. package/dist/cursor-manager.d.ts +54 -0
  26. package/dist/cursor-manager.d.ts.map +1 -0
  27. package/dist/cursor-manager.js +263 -0
  28. package/dist/cursor-manager.js.map +1 -0
  29. package/dist/ddl-manager.d.ts +33 -0
  30. package/dist/ddl-manager.d.ts.map +1 -0
  31. package/dist/ddl-manager.js +364 -0
  32. package/dist/ddl-manager.js.map +1 -0
  33. package/dist/http-api.d.ts +21 -0
  34. package/dist/http-api.d.ts.map +1 -0
  35. package/dist/http-api.js +242 -0
  36. package/dist/http-api.js.map +1 -0
  37. package/dist/index.d.ts +5 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +32 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/jwt.d.ts +16 -0
  42. package/dist/jwt.d.ts.map +1 -0
  43. package/dist/jwt.js +87 -0
  44. package/dist/jwt.js.map +1 -0
  45. package/dist/server.d.ts +24 -0
  46. package/dist/server.d.ts.map +1 -0
  47. package/dist/server.js +82 -0
  48. package/dist/server.js.map +1 -0
  49. package/dist/webhook.d.ts +9 -0
  50. package/dist/webhook.d.ts.map +1 -0
  51. package/dist/webhook.js +63 -0
  52. package/dist/webhook.js.map +1 -0
  53. package/dist/ws-fanout.d.ts +24 -0
  54. package/dist/ws-fanout.d.ts.map +1 -0
  55. package/dist/ws-fanout.js +198 -0
  56. package/dist/ws-fanout.js.map +1 -0
  57. package/package.json +7 -3
  58. package/src/__tests__/cursor-parsing.test.ts +0 -68
  59. package/src/__tests__/http-api.test.ts +0 -130
  60. package/src/__tests__/integration.test.ts +0 -249
  61. package/src/__tests__/jwt.test.ts +0 -62
  62. package/src/__tests__/ws-fanout.test.ts +0 -143
  63. package/src/config-loader.ts +0 -28
  64. package/src/cursor-manager.ts +0 -311
  65. package/src/ddl-manager.ts +0 -408
  66. package/src/http-api.ts +0 -278
  67. package/src/index.ts +0 -31
  68. package/src/jwt.ts +0 -56
  69. package/src/server.ts +0 -89
  70. package/src/webhook.ts +0 -67
  71. package/src/ws-fanout.ts +0 -245
  72. package/tsconfig.json +0 -8
@@ -1,28 +0,0 @@
1
- import { pathToFileURL } from 'node:url'
2
- import { resolve } from 'node:path'
3
- import type { WaveletConfig } from '@risingwave/wavelet'
4
-
5
- export async function loadConfig(configPath: string): Promise<WaveletConfig> {
6
- const abs = resolve(configPath)
7
- const mod = await import(pathToFileURL(abs).href)
8
-
9
- // Handle various module formats:
10
- // - ESM: mod.default is the config
11
- // - CJS interop: mod.default.default is the config
12
- // - Plain object: mod itself is the config
13
- let config = mod.default ?? mod
14
- if (config && typeof config === 'object' && 'default' in config) {
15
- config = config.default
16
- }
17
-
18
- if (!config || !config.database) {
19
- throw new Error(
20
- `wavelet.config.ts must export a config with a 'database' field.\n` +
21
- `Example:\n` +
22
- ` import { defineConfig } from '@risingwave/wavelet'\n` +
23
- ` export default defineConfig({ database: 'postgres://...' })`
24
- )
25
- }
26
-
27
- return config
28
- }
@@ -1,311 +0,0 @@
1
- import pg from 'pg'
2
- import type { SqlFragment, QueryDef } from '@risingwave/wavelet'
3
-
4
- const { Client } = pg
5
-
6
- export interface DiffRow {
7
- op: 'insert' | 'update_insert' | 'update_delete' | 'delete'
8
- row: Record<string, unknown>
9
- rw_timestamp: string
10
- }
11
-
12
- export interface ViewDiff {
13
- cursor: string
14
- inserted: Record<string, unknown>[]
15
- updated: Record<string, unknown>[]
16
- deleted: Record<string, unknown>[]
17
- }
18
-
19
- export interface BootstrapResult {
20
- snapshotRows: Record<string, unknown>[]
21
- diffs: ViewDiff[]
22
- lastCursor: string | null
23
- }
24
-
25
- type DiffCallback = (queryName: string, diff: ViewDiff) => void
26
-
27
- /**
28
- * Manages persistent subscription cursors against RisingWave.
29
- *
30
- * Each query gets its own dedicated pg connection and a persistent cursor.
31
- * Uses blocking FETCH (WITH timeout) so there is no polling interval -
32
- * diffs are dispatched as soon as RisingWave produces them.
33
- */
34
- export class CursorManager {
35
- // Shared connection for DDL (CREATE SUBSCRIPTION) and ad-hoc queries
36
- private client: InstanceType<typeof Client> | null = null
37
-
38
- // Per-query dedicated connections for blocking FETCH
39
- private queryConnections: Map<string, InstanceType<typeof Client>> = new Map()
40
- private cursorNames: Map<string, string> = new Map()
41
- private subscriptions: Map<string, string> = new Map()
42
- private running = false
43
-
44
- constructor(
45
- private connectionString: string,
46
- private queries: Record<string, QueryDef | SqlFragment>
47
- ) {}
48
-
49
- async initialize(): Promise<void> {
50
- this.client = new Client({ connectionString: this.connectionString })
51
- await this.client.connect()
52
- console.log('Connected to RisingWave')
53
-
54
- for (const [queryName] of Object.entries(this.queries)) {
55
- const subName = `wavelet_sub_${queryName}`
56
-
57
- // Create subscription if not exists (idempotent)
58
- try {
59
- await this.client.query(
60
- `CREATE SUBSCRIPTION ${subName} FROM ${queryName} WITH (retention = '24h')`
61
- )
62
- console.log(`Created subscription: ${subName}`)
63
- } catch (err: any) {
64
- if (err.message?.includes('exists')) {
65
- console.log(`Subscription exists: ${subName}`)
66
- } else {
67
- throw err
68
- }
69
- }
70
-
71
- this.subscriptions.set(queryName, subName)
72
-
73
- // Create dedicated connection and persistent cursor for this query
74
- const conn = new Client({ connectionString: this.connectionString })
75
- await conn.connect()
76
-
77
- const cursorName = `wavelet_cur_${queryName}`
78
- await conn.query(`DECLARE ${cursorName} SUBSCRIPTION CURSOR FOR ${subName}`)
79
- console.log(`Opened persistent cursor: ${cursorName}`)
80
-
81
- this.queryConnections.set(queryName, conn)
82
- this.cursorNames.set(queryName, cursorName)
83
- }
84
- }
85
-
86
- /**
87
- * Start listening for diffs on all queries.
88
- * Each query runs its own async loop with blocking FETCH.
89
- * No polling interval - FETCH blocks until data arrives or timeout.
90
- */
91
- startPolling(callback: DiffCallback): void {
92
- this.running = true
93
-
94
- for (const [queryName] of this.subscriptions.entries()) {
95
- this.listenLoop(queryName, callback)
96
- }
97
- }
98
-
99
- stopPolling(): void {
100
- this.running = false
101
- }
102
-
103
- private async listenLoop(queryName: string, callback: DiffCallback): Promise<void> {
104
- const conn = this.queryConnections.get(queryName)
105
- const cursorName = this.cursorNames.get(queryName)
106
- if (!conn || !cursorName) return
107
-
108
- while (this.running) {
109
- try {
110
- // Blocking FETCH: waits up to 5s for new data, returns immediately when data arrives
111
- const result = await conn.query(
112
- `FETCH NEXT FROM ${cursorName} WITH (timeout = '5s')`
113
- )
114
-
115
- if (result.rows.length === 0) continue
116
-
117
- // Got at least one row. Drain any remaining rows with timeout.
118
- const allRows = [...result.rows]
119
- let more = true
120
- while (more) {
121
- const batch = await conn.query(
122
- `FETCH 100 FROM ${cursorName} WITH (timeout = '1s')`
123
- )
124
- if (batch.rows.length > 0) {
125
- allRows.push(...batch.rows)
126
- } else {
127
- more = false
128
- }
129
- }
130
-
131
- const diffs = this.parseDiffBatches(allRows)
132
-
133
- for (const diff of diffs) {
134
- if (diff.inserted.length === 0 && diff.updated.length === 0 && diff.deleted.length === 0) {
135
- continue
136
- }
137
- callback(queryName, diff)
138
- }
139
- } catch (err: any) {
140
- if (!this.running) return
141
- console.error(`[cursor-manager] Error fetching ${queryName}:`, err.message)
142
- // Back off on error, then retry
143
- await new Promise(r => setTimeout(r, 1000))
144
- }
145
- }
146
- }
147
-
148
- parseDiffs(rows: any[]): ViewDiff {
149
- const diff: ViewDiff = {
150
- cursor: '',
151
- inserted: [],
152
- updated: [],
153
- deleted: [],
154
- }
155
-
156
- for (const row of rows) {
157
- const { op, rw_timestamp, ...data } = row
158
- diff.cursor = rw_timestamp ?? diff.cursor
159
-
160
- // RisingWave returns op as a string: "Insert", "Delete", "UpdateInsert", "UpdateDelete"
161
- const opStr = String(op)
162
- switch (opStr) {
163
- case 'Insert':
164
- case '1':
165
- diff.inserted.push(data)
166
- break
167
- case 'Delete':
168
- case '2':
169
- diff.deleted.push(data)
170
- break
171
- case 'UpdateDelete':
172
- case '3':
173
- diff.deleted.push(data)
174
- break
175
- case 'UpdateInsert':
176
- case '4':
177
- diff.updated.push(data)
178
- break
179
- }
180
- }
181
-
182
- return diff
183
- }
184
-
185
- parseDiffBatches(rows: any[]): ViewDiff[] {
186
- const diffs: ViewDiff[] = []
187
- let currentRows: any[] = []
188
- let currentCursor: string | null = null
189
-
190
- for (const row of rows) {
191
- const cursor = this.normalizeCursor(row.rw_timestamp)
192
- if (!cursor) continue
193
-
194
- if (currentCursor !== null && cursor !== currentCursor) {
195
- diffs.push(this.parseDiffs(currentRows))
196
- currentRows = []
197
- }
198
-
199
- currentCursor = cursor
200
- currentRows.push(row)
201
- }
202
-
203
- if (currentRows.length > 0) {
204
- diffs.push(this.parseDiffs(currentRows))
205
- }
206
-
207
- return diffs
208
- }
209
-
210
- async bootstrap(queryName: string): Promise<BootstrapResult> {
211
- const subName = this.subscriptions.get(queryName)
212
- if (!subName) {
213
- throw new Error(
214
- `Subscription for query '${queryName}' is not initialized. Start the server before accepting WebSocket clients.`
215
- )
216
- }
217
-
218
- const conn = new Client({ connectionString: this.connectionString })
219
- await conn.connect()
220
-
221
- const cursorName = `wavelet_boot_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
222
-
223
- try {
224
- await conn.query(`DECLARE ${cursorName} SUBSCRIPTION CURSOR FOR ${subName} FULL`)
225
-
226
- const snapshotRows: Record<string, unknown>[] = []
227
- const incrementalRows: any[] = []
228
- let readingSnapshot = true
229
-
230
- while (readingSnapshot) {
231
- const result = await conn.query(`FETCH 1000 FROM ${cursorName}`)
232
- if (result.rows.length === 0) break
233
-
234
- let firstIncrementalIndex = result.rows.findIndex((row) => this.normalizeCursor(row.rw_timestamp) !== null)
235
- if (firstIncrementalIndex === -1) firstIncrementalIndex = result.rows.length
236
-
237
- for (const row of result.rows.slice(0, firstIncrementalIndex)) {
238
- snapshotRows.push(this.stripSubscriptionMetadata(row))
239
- }
240
-
241
- if (firstIncrementalIndex < result.rows.length) {
242
- incrementalRows.push(...result.rows.slice(firstIncrementalIndex))
243
- readingSnapshot = false
244
- }
245
- }
246
-
247
- while (true) {
248
- const result = await conn.query(`FETCH 1000 FROM ${cursorName}`)
249
- if (result.rows.length === 0) break
250
- incrementalRows.push(...result.rows)
251
- }
252
-
253
- const diffs = this.parseDiffBatches(incrementalRows)
254
- const lastCursor = diffs.length > 0 ? diffs[diffs.length - 1].cursor : null
255
-
256
- return { snapshotRows, diffs, lastCursor }
257
- } finally {
258
- try {
259
- await conn.query(`CLOSE ${cursorName}`)
260
- } catch {}
261
- try {
262
- await conn.end()
263
- } catch {}
264
- }
265
- }
266
-
267
- async query(sql: string): Promise<any[]> {
268
- if (!this.client) throw new Error('Not connected')
269
- const result = await this.client.query(sql)
270
- return result.rows
271
- }
272
-
273
- async execute(sql: string): Promise<void> {
274
- if (!this.client) throw new Error('Not connected')
275
- await this.client.query(sql)
276
- }
277
-
278
- async close(): Promise<void> {
279
- this.running = false
280
-
281
- // Close per-query connections
282
- for (const [queryName, conn] of this.queryConnections) {
283
- const cursorName = this.cursorNames.get(queryName)
284
- try {
285
- if (cursorName) await conn.query(`CLOSE ${cursorName}`)
286
- } catch {}
287
- try {
288
- await conn.end()
289
- } catch {}
290
- }
291
- this.queryConnections.clear()
292
- this.cursorNames.clear()
293
-
294
- // Close shared connection
295
- try {
296
- await this.client?.end()
297
- } catch {}
298
- this.client = null
299
- }
300
-
301
- private stripSubscriptionMetadata(row: Record<string, unknown>): Record<string, unknown> {
302
- const { op: _op, rw_timestamp: _rwTimestamp, ...data } = row
303
- return data
304
- }
305
-
306
- private normalizeCursor(value: unknown): string | null {
307
- if (value === null || value === undefined) return null
308
- const cursor = String(value).trim()
309
- return cursor === '' ? null : cursor
310
- }
311
- }