bytex-sdk 5.3.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.
- package/index.js +523 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -405,3 +405,526 @@ export class BytexAI {
|
|
|
405
405
|
return data.candidates[0].content.parts[0].text;
|
|
406
406
|
}
|
|
407
407
|
}
|
|
408
|
+
|
|
409
|
+
// ════════════════════════════════════════════════════════════════
|
|
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
|
|
414
|
+
// ════════════════════════════════════════════════════════════════
|
|
415
|
+
/**
|
|
416
|
+
* BytexRealtime — Subscribe to database changes and broadcast events.
|
|
417
|
+
*
|
|
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 });
|
|
430
|
+
*/
|
|
431
|
+
export class BytexRealtime {
|
|
432
|
+
constructor(config = {}) {
|
|
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
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Subscribe to realtime database changes on a table (Supabase mode only).
|
|
471
|
+
* @param {string} table - Table name (e.g. 'messages', 'orders')
|
|
472
|
+
* @param {function} callback - Receives { event, new, old }
|
|
473
|
+
* @param {{ event?: 'INSERT'|'UPDATE'|'DELETE'|'*', filter?: string }} options
|
|
474
|
+
*/
|
|
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
|
+
|
|
482
|
+
const event = options.event || '*';
|
|
483
|
+
const channelName = `bytex-rt-${table}-${Date.now()}`;
|
|
484
|
+
const channel = this.client
|
|
485
|
+
.channel(channelName)
|
|
486
|
+
.on('postgres_changes', {
|
|
487
|
+
event,
|
|
488
|
+
schema: options.schema || 'public',
|
|
489
|
+
table,
|
|
490
|
+
...(options.filter && { filter: options.filter })
|
|
491
|
+
}, (payload) => callback(payload))
|
|
492
|
+
.subscribe((status) => {
|
|
493
|
+
if (status === 'CHANNEL_ERROR') {
|
|
494
|
+
console.error(`[BytexRealtime] Channel error on table "${table}". Check your Supabase config.`);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
this._channels.set(channelName, channel);
|
|
499
|
+
return channelName;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Subscribe to a broadcast channel — acts like a room/WebSocket.
|
|
504
|
+
* @param {string} roomName - Unique room identifier
|
|
505
|
+
* @param {function} callback - Receives { event, payload }
|
|
506
|
+
* @returns {{ send, leave }} - Methods to send messages or leave the room
|
|
507
|
+
*/
|
|
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
|
+
|
|
523
|
+
return {
|
|
524
|
+
/** Send a message to all members of this room. */
|
|
525
|
+
send: (event, payload) => channel.send({ type: 'broadcast', event, payload }),
|
|
526
|
+
/** Leave the room and clean up. */
|
|
527
|
+
leave: () => this.off(`bytex-room-${roomName}`)
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Polling fallback — polls a URL and calls callback when response changes.
|
|
533
|
+
* Used when no Supabase is configured.
|
|
534
|
+
* @private
|
|
535
|
+
*/
|
|
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;
|
|
559
|
+
}
|
|
560
|
+
|
|
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. */
|
|
582
|
+
destroy() {
|
|
583
|
+
this._channels.forEach((ch) => this.client?.removeChannel(ch));
|
|
584
|
+
this._channels.clear();
|
|
585
|
+
this._pollers.forEach((t) => clearInterval(t));
|
|
586
|
+
this._pollers.clear();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
// ════════════════════════════════════════════════════════════════
|
|
592
|
+
// ByteX CRON — Scheduled Jobs via Cloudflare Workers Cron Triggers
|
|
593
|
+
// Strategy: BYOC — user provides their Cloudflare API token
|
|
594
|
+
// Free tier: 3 Cron Triggers per Cloudflare account (FREE)
|
|
595
|
+
// ════════════════════════════════════════════════════════════════
|
|
596
|
+
/**
|
|
597
|
+
* BytexCron — Create and manage Cloudflare Workers Cron Triggers.
|
|
598
|
+
*
|
|
599
|
+
* Requires BYOC (user's Cloudflare account):
|
|
600
|
+
* const cron = new BytexCron({ cfToken: '...', cfAccountId: '...', workerName: 'my-worker' });
|
|
601
|
+
*/
|
|
602
|
+
export class BytexCron {
|
|
603
|
+
constructor({ cfToken, cfAccountId, workerName } = {}) {
|
|
604
|
+
if (!cfToken || !cfAccountId || !workerName)
|
|
605
|
+
throw new Error('[BytexCron] Required: { cfToken, cfAccountId, workerName } — Use BYOC config from bytex dashboard.');
|
|
606
|
+
this.token = cfToken;
|
|
607
|
+
this.accountId = cfAccountId;
|
|
608
|
+
this.workerName = workerName;
|
|
609
|
+
this.base = `https://api.cloudflare.com/client/v4/accounts/${cfAccountId}`;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** List all existing cron triggers for the worker. */
|
|
613
|
+
async list() {
|
|
614
|
+
const res = await fetch(`${this.base}/workers/scripts/${this.workerName}/schedules`, {
|
|
615
|
+
headers: { 'Authorization': `Bearer ${this.token}` }
|
|
616
|
+
});
|
|
617
|
+
const data = await res.json();
|
|
618
|
+
if (!data.success) throw new Error(`[BytexCron] ${JSON.stringify(data.errors)}`);
|
|
619
|
+
return data.result?.schedules || [];
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Add a new cron trigger (max 3 in free tier).
|
|
624
|
+
* @param {string} cron - Cron expression (e.g. '0 0 * * *' = daily at midnight)
|
|
625
|
+
*/
|
|
626
|
+
async add(cron) {
|
|
627
|
+
const existing = await this.list();
|
|
628
|
+
const schedules = [...existing.map(s => ({ cron: s.cron })), { cron }];
|
|
629
|
+
const res = await fetch(`${this.base}/workers/scripts/${this.workerName}/schedules`, {
|
|
630
|
+
method: 'PUT',
|
|
631
|
+
headers: { 'Authorization': `Bearer ${this.token}`, 'Content-Type': 'application/json' },
|
|
632
|
+
body: JSON.stringify(schedules)
|
|
633
|
+
});
|
|
634
|
+
const data = await res.json();
|
|
635
|
+
if (!data.success) throw new Error(`[BytexCron] ${JSON.stringify(data.errors)}`);
|
|
636
|
+
return data.result;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Remove a cron trigger.
|
|
641
|
+
* @param {string} cron - The exact cron string to remove
|
|
642
|
+
*/
|
|
643
|
+
async remove(cron) {
|
|
644
|
+
const existing = await this.list();
|
|
645
|
+
const schedules = existing.filter(s => s.cron !== cron).map(s => ({ cron: s.cron }));
|
|
646
|
+
const res = await fetch(`${this.base}/workers/scripts/${this.workerName}/schedules`, {
|
|
647
|
+
method: 'PUT',
|
|
648
|
+
headers: { 'Authorization': `Bearer ${this.token}`, 'Content-Type': 'application/json' },
|
|
649
|
+
body: JSON.stringify(schedules)
|
|
650
|
+
});
|
|
651
|
+
const data = await res.json();
|
|
652
|
+
if (!data.success) throw new Error(`[BytexCron] ${JSON.stringify(data.errors)}`);
|
|
653
|
+
return data.result;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ════════════════════════════════════════════════════════════════
|
|
658
|
+
// ByteX QUEUE — Message Queue via Cloudflare Queues
|
|
659
|
+
// Strategy: BYOC — user provides their Cloudflare API token
|
|
660
|
+
// Free tier: 1,000,000 messages/month per account (FREE)
|
|
661
|
+
// ════════════════════════════════════════════════════════════════
|
|
662
|
+
/**
|
|
663
|
+
* BytexQueue — Create and publish to Cloudflare Queues.
|
|
664
|
+
*
|
|
665
|
+
* Requires BYOC (user's Cloudflare account):
|
|
666
|
+
* const queue = new BytexQueue({ cfToken: '...', cfAccountId: '...' });
|
|
667
|
+
*/
|
|
668
|
+
export class BytexQueue {
|
|
669
|
+
constructor({ cfToken, cfAccountId } = {}) {
|
|
670
|
+
if (!cfToken || !cfAccountId)
|
|
671
|
+
throw new Error('[BytexQueue] Required: { cfToken, cfAccountId } — Use BYOC config from bytex dashboard.');
|
|
672
|
+
this.token = cfToken;
|
|
673
|
+
this.accountId = cfAccountId;
|
|
674
|
+
this.base = `https://api.cloudflare.com/client/v4/accounts/${cfAccountId}/queues`;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** List all existing queues. */
|
|
678
|
+
async list() {
|
|
679
|
+
const res = await fetch(this.base, { headers: { 'Authorization': `Bearer ${this.token}` } });
|
|
680
|
+
const data = await res.json();
|
|
681
|
+
if (!data.success) throw new Error(`[BytexQueue] ${JSON.stringify(data.errors)}`);
|
|
682
|
+
return data.result || [];
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Create a new queue.
|
|
687
|
+
* @param {string} name - Queue name (lowercase, hyphens only)
|
|
688
|
+
*/
|
|
689
|
+
async create(name) {
|
|
690
|
+
const res = await fetch(this.base, {
|
|
691
|
+
method: 'POST',
|
|
692
|
+
headers: { 'Authorization': `Bearer ${this.token}`, 'Content-Type': 'application/json' },
|
|
693
|
+
body: JSON.stringify({ queue_name: name })
|
|
694
|
+
});
|
|
695
|
+
const data = await res.json();
|
|
696
|
+
if (!data.success) throw new Error(`[BytexQueue] ${JSON.stringify(data.errors)}`);
|
|
697
|
+
return data.result;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Delete a queue.
|
|
702
|
+
* @param {string} name - Queue name
|
|
703
|
+
*/
|
|
704
|
+
async delete(name) {
|
|
705
|
+
const res = await fetch(`${this.base}/${name}`, {
|
|
706
|
+
method: 'DELETE',
|
|
707
|
+
headers: { 'Authorization': `Bearer ${this.token}` }
|
|
708
|
+
});
|
|
709
|
+
const data = await res.json();
|
|
710
|
+
if (!data.success) throw new Error(`[BytexQueue] ${JSON.stringify(data.errors)}`);
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Publish a message to a queue.
|
|
716
|
+
* @param {string} queueName - Target queue
|
|
717
|
+
* @param {object|string} body - Message content
|
|
718
|
+
* @param {object} options - { delaySeconds? }
|
|
719
|
+
*/
|
|
720
|
+
async publish(queueName, body, options = {}) {
|
|
721
|
+
const message = { body: typeof body === 'string' ? body : JSON.stringify(body) };
|
|
722
|
+
if (options.delaySeconds) message.delay_seconds = options.delaySeconds;
|
|
723
|
+
const res = await fetch(`${this.base}/${queueName}/messages`, {
|
|
724
|
+
method: 'POST',
|
|
725
|
+
headers: { 'Authorization': `Bearer ${this.token}`, 'Content-Type': 'application/json' },
|
|
726
|
+
body: JSON.stringify({ messages: [message] })
|
|
727
|
+
});
|
|
728
|
+
const data = await res.json();
|
|
729
|
+
if (!data.success) throw new Error(`[BytexQueue] ${JSON.stringify(data.errors)}`);
|
|
730
|
+
return data.result;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Publish multiple messages in a single batch (max 100).
|
|
735
|
+
* @param {string} queueName - Target queue
|
|
736
|
+
* @param {Array<object|string>} items - Array of message bodies
|
|
737
|
+
*/
|
|
738
|
+
async publishBatch(queueName, items) {
|
|
739
|
+
const messages = items.map(body => ({ body: typeof body === 'string' ? body : JSON.stringify(body) }));
|
|
740
|
+
const res = await fetch(`${this.base}/${queueName}/messages`, {
|
|
741
|
+
method: 'POST',
|
|
742
|
+
headers: { 'Authorization': `Bearer ${this.token}`, 'Content-Type': 'application/json' },
|
|
743
|
+
body: JSON.stringify({ messages })
|
|
744
|
+
});
|
|
745
|
+
const data = await res.json();
|
|
746
|
+
if (!data.success) throw new Error(`[BytexQueue] ${JSON.stringify(data.errors)}`);
|
|
747
|
+
return data.result;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// ════════════════════════════════════════════════════════════════
|
|
752
|
+
// ByteX PROXY — Reverse Proxy via Cloudflare Workers
|
|
753
|
+
// Strategy: Uses ByteX's Cloudflare (no BYOC needed for basic use)
|
|
754
|
+
// Free tier: 100,000 requests/day (FREE)
|
|
755
|
+
// ════════════════════════════════════════════════════════════════
|
|
756
|
+
/**
|
|
757
|
+
* BytexProxy — Deploy and manage Cloudflare Worker reverse proxies.
|
|
758
|
+
*
|
|
759
|
+
* Usage:
|
|
760
|
+
* // Use ByteX's proxy endpoint (no config needed):
|
|
761
|
+
* const url = BytexProxy.buildUrl('https://api.target.com/endpoint', { headers: {...} });
|
|
762
|
+
*
|
|
763
|
+
* // Or deploy your own proxy Worker (BYOC):
|
|
764
|
+
* const proxy = new BytexProxy({ cfToken: '...', cfAccountId: '...' });
|
|
765
|
+
* await proxy.deploy('my-proxy', 'https://api.target.com');
|
|
766
|
+
*/
|
|
767
|
+
export class BytexProxy {
|
|
768
|
+
constructor({ cfToken, cfAccountId } = {}) {
|
|
769
|
+
this.token = cfToken;
|
|
770
|
+
this.accountId = cfAccountId;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Build a proxied URL via ByteX's own proxy layer.
|
|
775
|
+
* Useful for CORS bypass and header injection.
|
|
776
|
+
* @param {string} targetUrl - The destination URL to proxy
|
|
777
|
+
* @param {{ headers?: object }} options
|
|
778
|
+
*/
|
|
779
|
+
static buildUrl(targetUrl, options = {}) {
|
|
780
|
+
const encoded = encodeURIComponent(targetUrl);
|
|
781
|
+
const base = 'https://api.bytex.work/?action=proxy&target=';
|
|
782
|
+
return `${base}${encoded}`;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Deploy a Cloudflare Worker that proxies to a target origin (BYOC).
|
|
787
|
+
* @param {string} workerName - Unique name for this proxy worker
|
|
788
|
+
* @param {string} targetOrigin - Base URL of the backend (e.g. 'https://my-api.com')
|
|
789
|
+
* @param {{ injectHeaders?: object, stripHeaders?: string[] }} options
|
|
790
|
+
*/
|
|
791
|
+
async deploy(workerName, targetOrigin, options = {}) {
|
|
792
|
+
if (!this.token || !this.accountId)
|
|
793
|
+
throw new Error('[BytexProxy] BYOC required for deploy. Pass { cfToken, cfAccountId }.');
|
|
794
|
+
|
|
795
|
+
const injectHeaders = options.injectHeaders
|
|
796
|
+
? Object.entries(options.injectHeaders).map(([k, v]) => `req.headers.set('${k}', '${v}');`).join('\n ')
|
|
797
|
+
: '';
|
|
798
|
+
const stripHeaders = (options.stripHeaders || [])
|
|
799
|
+
.map(h => `req.headers.delete('${h}');`).join('\n ');
|
|
800
|
+
|
|
801
|
+
const workerScript = `
|
|
802
|
+
export default {
|
|
803
|
+
async fetch(request) {
|
|
804
|
+
const url = new URL(request.url);
|
|
805
|
+
const target = new URL('${targetOrigin}' + url.pathname + url.search);
|
|
806
|
+
const req = new Request(target, request);
|
|
807
|
+
${injectHeaders}
|
|
808
|
+
${stripHeaders}
|
|
809
|
+
const response = await fetch(req);
|
|
810
|
+
const res = new Response(response.body, response);
|
|
811
|
+
res.headers.set('Access-Control-Allow-Origin', '*');
|
|
812
|
+
return res;
|
|
813
|
+
}
|
|
814
|
+
}`;
|
|
815
|
+
|
|
816
|
+
const fd = new FormData();
|
|
817
|
+
fd.append('metadata', JSON.stringify({ main_module: 'worker.js', compatibility_date: '2024-01-01' }));
|
|
818
|
+
fd.append('worker.js', new Blob([workerScript], { type: 'application/javascript+module' }), 'worker.js');
|
|
819
|
+
|
|
820
|
+
const res = await fetch(`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/workers/scripts/${workerName}`, {
|
|
821
|
+
method: 'PUT',
|
|
822
|
+
headers: { 'Authorization': `Bearer ${this.token}` },
|
|
823
|
+
body: fd
|
|
824
|
+
});
|
|
825
|
+
const data = await res.json();
|
|
826
|
+
if (!data.success) throw new Error(`[BytexProxy] ${JSON.stringify(data.errors)}`);
|
|
827
|
+
return { workerName, url: `https://${workerName}.workers.dev`, target: targetOrigin };
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// ════════════════════════════════════════════════════════════════
|
|
832
|
+
// ByteX MQTT — IoT Messaging via user's MQTT Broker
|
|
833
|
+
// Strategy: BYOC — user provides their broker (HiveMQ/EMQX/any)
|
|
834
|
+
// Free tier: HiveMQ Cloud free (10 devices), EMQX Serverless (1M msg/mo)
|
|
835
|
+
// ════════════════════════════════════════════════════════════════
|
|
836
|
+
/**
|
|
837
|
+
* BytexMQTT — Lightweight MQTT client wrapper (BYOC).
|
|
838
|
+
*
|
|
839
|
+
* Requires the 'mqtt' package: npm install mqtt
|
|
840
|
+
* Requires user's own MQTT broker (HiveMQ, EMQX, Mosquitto, etc.)
|
|
841
|
+
*
|
|
842
|
+
* Usage:
|
|
843
|
+
* const mq = new BytexMQTT({
|
|
844
|
+
* brokerUrl: 'mqtts://your-cluster.hivemq.cloud:8883',
|
|
845
|
+
* username: 'user', password: 'pass'
|
|
846
|
+
* });
|
|
847
|
+
* await mq.connect();
|
|
848
|
+
* mq.subscribe('bytex/files/#', (topic, msg) => console.log(topic, msg));
|
|
849
|
+
* mq.publish('bytex/files/new', { name: 'photo.jpg' });
|
|
850
|
+
*/
|
|
851
|
+
export class BytexMQTT {
|
|
852
|
+
constructor({ brokerUrl, username, password, clientId } = {}) {
|
|
853
|
+
if (!brokerUrl)
|
|
854
|
+
throw new Error('[BytexMQTT] Required: { brokerUrl } — Get free broker at hivemq.com or emqx.com (BYOC).');
|
|
855
|
+
this.brokerUrl = brokerUrl;
|
|
856
|
+
this.options = {
|
|
857
|
+
username, password,
|
|
858
|
+
clientId: clientId || `bytex-${Math.random().toString(36).substring(2, 9)}`,
|
|
859
|
+
clean: true,
|
|
860
|
+
reconnectPeriod: 3000,
|
|
861
|
+
connectTimeout: 10000,
|
|
862
|
+
};
|
|
863
|
+
this.client = null;
|
|
864
|
+
this._handlers = new Map();
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Connect to the MQTT broker.
|
|
869
|
+
* @returns {Promise<void>}
|
|
870
|
+
*/
|
|
871
|
+
async connect() {
|
|
872
|
+
let mqtt;
|
|
873
|
+
try { mqtt = (await import('mqtt')).default; }
|
|
874
|
+
catch { throw new Error('[BytexMQTT] Install the mqtt package first: npm install mqtt'); }
|
|
875
|
+
|
|
876
|
+
return new Promise((resolve, reject) => {
|
|
877
|
+
this.client = mqtt.connect(this.brokerUrl, this.options);
|
|
878
|
+
this.client.on('connect', () => resolve());
|
|
879
|
+
this.client.on('error', (err) => reject(new Error(`[BytexMQTT] Connection failed: ${err.message}`)));
|
|
880
|
+
this.client.on('message', (topic, buf) => {
|
|
881
|
+
const msg = buf.toString();
|
|
882
|
+
let parsed; try { parsed = JSON.parse(msg); } catch { parsed = msg; }
|
|
883
|
+
this._handlers.forEach((fn, pattern) => {
|
|
884
|
+
if (this._topicMatch(pattern, topic)) fn(topic, parsed);
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Publish a message to a topic.
|
|
892
|
+
* @param {string} topic
|
|
893
|
+
* @param {object|string} payload
|
|
894
|
+
* @param {{ qos?: 0|1|2, retain?: boolean }} options
|
|
895
|
+
*/
|
|
896
|
+
publish(topic, payload, options = {}) {
|
|
897
|
+
if (!this.client) throw new Error('[BytexMQTT] Not connected. Call connect() first.');
|
|
898
|
+
const msg = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
899
|
+
this.client.publish(topic, msg, { qos: options.qos ?? 1, retain: options.retain ?? false });
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Subscribe to a topic or wildcard pattern.
|
|
904
|
+
* @param {string} topic - Supports MQTT wildcards: '#' (multi-level), '+' (single-level)
|
|
905
|
+
* @param {function} callback - (topic, parsedPayload) => void
|
|
906
|
+
* @param {{ qos?: 0|1|2 }} options
|
|
907
|
+
*/
|
|
908
|
+
subscribe(topic, callback, options = {}) {
|
|
909
|
+
if (!this.client) throw new Error('[BytexMQTT] Not connected. Call connect() first.');
|
|
910
|
+
this.client.subscribe(topic, { qos: options.qos ?? 1 });
|
|
911
|
+
this._handlers.set(topic, callback);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** Unsubscribe from a topic. */
|
|
915
|
+
unsubscribe(topic) {
|
|
916
|
+
if (this.client) this.client.unsubscribe(topic);
|
|
917
|
+
this._handlers.delete(topic);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** Gracefully disconnect from the broker. */
|
|
921
|
+
disconnect() {
|
|
922
|
+
if (this.client) { this.client.end(); this.client = null; }
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
_topicMatch(pattern, topic) {
|
|
926
|
+
const p = pattern.replace(/\+/g, '[^/]+').replace(/#$/, '.*');
|
|
927
|
+
return new RegExp(`^${p}$`).test(topic);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|