bytex-sdk 5.4.0 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +138 -28
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -407,77 +407,187 @@ export class BytexAI {
407
407
  }
408
408
 
409
409
  // ════════════════════════════════════════════════════════════════
410
- // ByteX REALTIME — Database Realtime via Supabase
411
- // Strategy: Uses ByteX's Supabase OR user's own (BYOC)
412
- // Free tier: 200 concurrent connections per Supabase project
410
+ // ByteX REALTIME — Database Realtime (BYOC Model)
411
+ // Strategy: ALWAYS uses user's own Supabase (BYOC)
412
+ // Falls back to HTTP polling if no Supabase configured
413
+ // Free tier: 200 concurrent connections per USER's Supabase project
413
414
  // ════════════════════════════════════════════════════════════════
414
415
  /**
415
- * BytexRealtime — Subscribe to database changes in real-time.
416
+ * BytexRealtime — Subscribe to database changes and broadcast events.
416
417
  *
417
- * Usage:
418
- * const rt = new BytexRealtime(bytex.supabase);
419
- * rt.on('api_keys', (payload) => console.log(payload));
418
+ * ⚠️ IMPORTANT: Always use BYOC (user's own Supabase), NOT ByteX's central Supabase.
419
+ * ByteX's Supabase has a limit of 200 concurrent connections total.
420
+ * With BYOC, EACH USER gets their own 200 free concurrent connections.
421
+ *
422
+ * Usage (BYOC — Recommended):
423
+ * const rt = new BytexRealtime({
424
+ * supabaseUrl: 'https://your-project.supabase.co',
425
+ * supabaseKey: 'your-anon-key'
426
+ * });
427
+ *
428
+ * Usage (Polling fallback — no Supabase needed):
429
+ * const rt = new BytexRealtime({ pollUrl: 'https://api.bytex.work/', pollInterval: 5000 });
420
430
  */
421
431
  export class BytexRealtime {
422
- constructor(supabaseClient) {
423
- if (!supabaseClient) throw new Error('[BytexRealtime] Pass a Supabase client. Use: new BytexRealtime(bytex.supabase)');
424
- this.client = supabaseClient;
432
+ constructor(config = {}) {
425
433
  this._channels = new Map();
434
+ this._pollers = new Map();
435
+ this._mode = 'idle';
436
+
437
+ // Mode 1: BYOC Supabase client passed directly (legacy support)
438
+ if (config && typeof config.from === 'function') {
439
+ console.warn('[BytexRealtime] ⚠️ Passing ByteX\'s own Supabase client is not recommended for production. Use BYOC credentials to avoid shared connection limits.');
440
+ this.client = config;
441
+ this._mode = 'supabase';
442
+ return;
443
+ }
444
+
445
+ // Mode 2: BYOC — user provides their own Supabase credentials
446
+ if (config.supabaseUrl && config.supabaseKey) {
447
+ this.client = createClient(config.supabaseUrl, config.supabaseKey, {
448
+ realtime: { params: { eventsPerSecond: 10 } }
449
+ });
450
+ this._mode = 'supabase';
451
+ return;
452
+ }
453
+
454
+ // Mode 3: Polling fallback — no Supabase required
455
+ if (config.pollUrl) {
456
+ this._pollUrl = config.pollUrl;
457
+ this._pollInterval = config.pollInterval || 5000;
458
+ this._mode = 'polling';
459
+ return;
460
+ }
461
+
462
+ throw new Error(
463
+ '[BytexRealtime] Configuration required. Options:\n' +
464
+ ' 1. BYOC (recommended): { supabaseUrl, supabaseKey } — user\'s own Supabase\n' +
465
+ ' 2. Polling fallback: { pollUrl, pollInterval? } — works without Supabase'
466
+ );
426
467
  }
427
468
 
428
469
  /**
429
- * Subscribe to realtime changes on a table.
430
- * @param {string} table - Table name (e.g. 'api_keys', 'files')
470
+ * Subscribe to realtime database changes on a table (Supabase mode only).
471
+ * @param {string} table - Table name (e.g. 'messages', 'orders')
431
472
  * @param {function} callback - Receives { event, new, old }
432
473
  * @param {{ event?: 'INSERT'|'UPDATE'|'DELETE'|'*', filter?: string }} options
433
474
  */
434
475
  on(table, callback, options = {}) {
476
+ if (this._mode === 'polling') {
477
+ return this._pollTable(table, callback, options);
478
+ }
479
+
480
+ if (this._mode !== 'supabase') throw new Error('[BytexRealtime] No Supabase client. Provide supabaseUrl + supabaseKey.');
481
+
435
482
  const event = options.event || '*';
436
483
  const channelName = `bytex-rt-${table}-${Date.now()}`;
437
484
  const channel = this.client
438
485
  .channel(channelName)
439
486
  .on('postgres_changes', {
440
487
  event,
441
- schema: 'public',
488
+ schema: options.schema || 'public',
442
489
  table,
443
490
  ...(options.filter && { filter: options.filter })
444
491
  }, (payload) => callback(payload))
445
- .subscribe();
492
+ .subscribe((status) => {
493
+ if (status === 'CHANNEL_ERROR') {
494
+ console.error(`[BytexRealtime] Channel error on table "${table}". Check your Supabase config.`);
495
+ }
496
+ });
497
+
446
498
  this._channels.set(channelName, channel);
447
499
  return channelName;
448
500
  }
449
501
 
450
502
  /**
451
- * Subscribe to a broadcast channel (WebSocket replacement).
452
- * @param {string} channelName - Unique room/channel name
503
+ * Subscribe to a broadcast channel — acts like a room/WebSocket.
504
+ * @param {string} roomName - Unique room identifier
453
505
  * @param {function} callback - Receives { event, payload }
506
+ * @returns {{ send, leave }} - Methods to send messages or leave the room
454
507
  */
455
- join(channelName, callback) {
456
- const channel = this.client.channel(channelName);
457
- channel.on('broadcast', { event: '*' }, (msg) => callback(msg)).subscribe();
458
- this._channels.set(channelName, channel);
508
+ join(roomName, callback) {
509
+ if (this._mode !== 'supabase') {
510
+ throw new Error('[BytexRealtime] Broadcast channels require Supabase. Provide supabaseUrl + supabaseKey.');
511
+ }
512
+
513
+ const channel = this.client.channel(`bytex-room-${roomName}`, {
514
+ config: { broadcast: { self: false } }
515
+ });
516
+
517
+ channel
518
+ .on('broadcast', { event: '*' }, (msg) => callback(msg))
519
+ .subscribe();
520
+
521
+ this._channels.set(`bytex-room-${roomName}`, channel);
522
+
459
523
  return {
524
+ /** Send a message to all members of this room. */
460
525
  send: (event, payload) => channel.send({ type: 'broadcast', event, payload }),
461
- leave: () => this.off(channelName)
526
+ /** Leave the room and clean up. */
527
+ leave: () => this.off(`bytex-room-${roomName}`)
462
528
  };
463
529
  }
464
530
 
465
531
  /**
466
- * Unsubscribe from a channel.
467
- * @param {string} channelName
532
+ * Polling fallback — polls a URL and calls callback when response changes.
533
+ * Used when no Supabase is configured.
534
+ * @private
468
535
  */
469
- off(channelName) {
470
- const ch = this._channels.get(channelName);
471
- if (ch) { this.client.removeChannel(ch); this._channels.delete(channelName); }
536
+ _pollTable(table, callback, options) {
537
+ let lastHash = null;
538
+ const pollerId = `poll-${table}-${Date.now()}`;
539
+
540
+ const poll = async () => {
541
+ try {
542
+ const url = `${this._pollUrl}?action=list&table=${table}`;
543
+ const res = await fetch(url);
544
+ const text = await res.text();
545
+ const hash = btoa(text.substring(0, 200));
546
+ if (lastHash !== null && hash !== lastHash) {
547
+ callback({ event: '*', source: 'poll', data: text });
548
+ }
549
+ lastHash = hash;
550
+ } catch (e) {
551
+ console.warn(`[BytexRealtime] Poll error: ${e.message}`);
552
+ }
553
+ };
554
+
555
+ poll();
556
+ const timer = setInterval(poll, this._pollInterval);
557
+ this._pollers.set(pollerId, timer);
558
+ return pollerId;
472
559
  }
473
560
 
474
- /** Remove all active subscriptions. */
561
+ /**
562
+ * Check current realtime mode.
563
+ * @returns {'supabase'|'polling'|'idle'}
564
+ */
565
+ get mode() { return this._mode; }
566
+
567
+ /**
568
+ * Get current active connection count (Supabase mode only).
569
+ */
570
+ get connectionCount() { return this._channels.size; }
571
+
572
+ /** Unsubscribe from a specific channel or poller. */
573
+ off(channelId) {
574
+ const ch = this._channels.get(channelId);
575
+ if (ch) { this.client?.removeChannel(ch); this._channels.delete(channelId); return; }
576
+
577
+ const timer = this._pollers.get(channelId);
578
+ if (timer) { clearInterval(timer); this._pollers.delete(channelId); }
579
+ }
580
+
581
+ /** Disconnect all subscriptions and clean up. */
475
582
  destroy() {
476
- this._channels.forEach((ch) => this.client.removeChannel(ch));
583
+ this._channels.forEach((ch) => this.client?.removeChannel(ch));
477
584
  this._channels.clear();
585
+ this._pollers.forEach((t) => clearInterval(t));
586
+ this._pollers.clear();
478
587
  }
479
588
  }
480
589
 
590
+
481
591
  // ════════════════════════════════════════════════════════════════
482
592
  // ByteX CRON — Scheduled Jobs via Cloudflare Workers Cron Triggers
483
593
  // Strategy: BYOC — user provides their Cloudflare API token
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bytex-sdk",
3
- "version": "5.4.0",
3
+ "version": "5.5.0",
4
4
  "description": "Official ByteX Cloud SDK with AI (BYOM) support",
5
5
  "main": "index.js",
6
6
  "scripts": {