@modernman00/shared-js-lib 1.2.89 → 1.6.2

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 CHANGED
@@ -129,6 +129,84 @@ mix.js('resources/js/app.js', 'public/js')
129
129
 
130
130
 
131
131
 
132
+ ### Example: AI Client Utilities (£0 Serverless AI)
133
+ ```javascript
134
+ import { AiVoiceRecorder, AiMediaGuard, AiSemanticSearch } from '@modernman00/shared-js-lib';
135
+
136
+ // 1. Voice-to-Text Recording (Whisper via Backend Proxy)
137
+ const recorder = new AiVoiceRecorder({
138
+ uploadUrl: '/api/ai/transcribe',
139
+ onStateChange: (state) => console.log('Recording state:', state),
140
+ onError: (err) => alert(err.message)
141
+ });
142
+
143
+ await recorder.start();
144
+ // ... later
145
+ const blob = await recorder.stop();
146
+ const { text } = await recorder.uploadAndTranscribe(blob);
147
+ document.querySelector('#task-input').value = text;
148
+
149
+ // 2. Photo NSFW Guard (Fail-Closed Image Safety Pre-check)
150
+ const file = document.querySelector('input[type="file"]').files[0];
151
+ const validation = AiMediaGuard.validateFile(file, { maxSizeBytes: 10 * 1024 * 1024 });
152
+ if (!validation.valid) {
153
+ alert(validation.error);
154
+ return;
155
+ }
156
+ const safety = await AiMediaGuard.precheckPhotoSafety(file, '/api/ai/moderate-photo');
157
+ if (safety.flagged) {
158
+ alert('Photo rejected: content contains NSFW imagery.');
159
+ }
160
+
161
+ // 3. Instant Semantic Search (Debounced & Abort-Protected)
162
+ const searchController = new AiSemanticSearch({
163
+ searchUrl: '/api/ai/semantic-search',
164
+ minQueryLength: 3,
165
+ debounceMs: 350,
166
+ onResults: (results) => renderCards(results),
167
+ onLoading: (isLoading) => toggleSpinner(isLoading)
168
+ });
169
+
170
+ document.querySelector('#search-input').addEventListener('input', (e) => {
171
+ searchController.handleInput(e.target.value);
172
+ });
173
+ // 4. Generative Event Flyer Studio (AI Art + Razor-Sharp Canvas Typography)
174
+ import { AiFlyerStudio, AiBillSplitter, AiEntertainmentPrompt } from '@modernman00/shared-js-lib';
175
+
176
+ const studio = new AiFlyerStudio({ width: 800, height: 1200 });
177
+ await studio.setBackgroundImage(aiGeneratedBackgroundDataUri);
178
+ studio.setData({
179
+ title: 'Liam 7th Birthday',
180
+ subtitle: 'Jurassic Dinosaur Adventure',
181
+ date: 'Saturday, 12 Nov',
182
+ time: '3:00 PM',
183
+ location: 'St. James Hall',
184
+ hostName: 'Sarah & Mark'
185
+ });
186
+ studio.render();
187
+ studio.download('liam-birthday-flyer.png');
188
+
189
+ // 5. Multimodal Bill & Bar Tab Splitter (Camera Photo -> Itemized Guest Assignment)
190
+ const splitter = new AiBillSplitter({ defaultTipPercent: 12.5 });
191
+ const scanResult = await splitter.scanReceipt(receiptPhotoFile);
192
+ if (scanResult.success) {
193
+ splitter.addGuest('g1', 'Alice');
194
+ splitter.addGuest('g2', 'Bob');
195
+ splitter.assignItem(0, 'g1'); // Alice pays for Nachos
196
+ splitter.assignItem(0, 'g2'); // Bob shares Nachos
197
+ const breakdown = splitter.calculateShares(10); // per-guest breakdown with tip
198
+ }
199
+
200
+ // 6. AI Speechwriter & Roast Master
201
+ const speech = await AiEntertainmentPrompt.requestSpeech({
202
+ honoureeName: 'Dave',
203
+ occasion: '30th Birthday',
204
+ anecdotes: ['Fell into pool in Ibiza', 'Sourdough bread fanatic'],
205
+ tone: 'humorous_roast'
206
+ });
207
+ console.log(speech.speechText, speech.toast);
208
+ ```
209
+
132
210
  Updating the Package
133
211
  To update to the latest version, run:
134
212
  npm update @modernman00/shared-js-lib
@@ -0,0 +1,325 @@
1
+ /**
2
+ * SovereignBroadcasterClient
3
+ *
4
+ * Engineered by the Advanced Technology Group (ATG) under Olutobi (Deloitte) and Victor (CTO).
5
+ * High-performance, zero-npm, drop-in replacement for Pusher JS.
6
+ * Connects directly to SovereignBroadcaster PHP SSE endpoints with auto-reconnect,
7
+ * HMAC channel authentication, and presence tracking.
8
+ */
9
+
10
+ export class SovereignChannel {
11
+ constructor(name, broadcaster) {
12
+ this.name = name;
13
+ this.broadcaster = broadcaster;
14
+ this.callbacks = new Map();
15
+ this.members = new Map();
16
+ this.subscribed = false;
17
+ }
18
+
19
+ bind(event, callback) {
20
+ if (!this.callbacks.has(event)) {
21
+ this.callbacks.set(event, []);
22
+ }
23
+ this.callbacks.get(event).push(callback);
24
+ return this;
25
+ }
26
+
27
+ unbind(event, callback) {
28
+ if (!this.callbacks.has(event)) return this;
29
+ if (!callback) {
30
+ this.callbacks.delete(event);
31
+ } else {
32
+ const filtered = this.callbacks.get(event).filter(cb => cb !== callback);
33
+ this.callbacks.set(event, filtered);
34
+ }
35
+ return this;
36
+ }
37
+
38
+ emit(event, data) {
39
+ const cbs = this.callbacks.get(event);
40
+ if (cbs && cbs.length > 0) {
41
+ cbs.forEach(cb => {
42
+ try {
43
+ cb(data);
44
+ } catch (e) {
45
+ console.error(`[SovereignBroadcaster] Error in event listener for ${event}:`, e);
46
+ }
47
+ });
48
+ }
49
+ }
50
+ }
51
+
52
+ export class SovereignBroadcasterClient {
53
+ constructor(options = {}) {
54
+ this.endpoint = options.endpoint || '/api/broadcasting/events';
55
+ this.authEndpoint = options.authEndpoint || '/api/broadcasting/auth';
56
+ this.headers = options.headers || {};
57
+ this.channels = new Map();
58
+ this.socketId = this._generateSocketId();
59
+ this.lastEventId = 0;
60
+ this.reconnectAttempts = 0;
61
+ this.maxReconnectDelay = 15000;
62
+ this.isConnected = false;
63
+
64
+ if (typeof document !== 'undefined' && !this.headers['X-CSRF-TOKEN']) {
65
+ const metaCsrf = document.querySelector('meta[name="csrf-token"]');
66
+ if (metaCsrf && metaCsrf.content) {
67
+ this.headers['X-CSRF-TOKEN'] = metaCsrf.content;
68
+ }
69
+ }
70
+ }
71
+
72
+ _generateSocketId() {
73
+ if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
74
+ const arr = new Uint32Array(2);
75
+ crypto.getRandomValues(arr);
76
+ return `${arr[0]}.${arr[1]}`;
77
+ }
78
+ const rand1 = Math.floor(Math.random() * 1000000);
79
+ const rand2 = Math.floor(Math.random() * 1000000);
80
+ return `${rand1}.${rand2}`;
81
+ }
82
+
83
+ subscribe(channelName) {
84
+ if (typeof channelName !== 'string' || !/^[A-Za-z0-9_.:@-]{1,191}$/.test(channelName)) {
85
+ throw new Error('[SovereignBroadcaster] Invalid channel identifier');
86
+ }
87
+ if (this.channels.has(channelName)) {
88
+ return this.channels.get(channelName);
89
+ }
90
+
91
+ const channel = new SovereignChannel(channelName, this);
92
+ this.channels.set(channelName, channel);
93
+
94
+ if (channelName.startsWith('private-') || channelName.startsWith('presence-')) {
95
+ this._authorizeChannel(channelName)
96
+ .then(authData => {
97
+ channel.authData = authData || {};
98
+ channel.subscribed = true;
99
+ this._connectSse(channelName);
100
+ channel.emit('pusher:subscription_succeeded', authData);
101
+ })
102
+ .catch(err => {
103
+ console.error(`[SovereignBroadcaster] Authorization failed for ${channelName}:`, err);
104
+ channel.emit('pusher:subscription_error', err);
105
+ });
106
+ } else {
107
+ channel.subscribed = true;
108
+ this._connectSse(channelName);
109
+ channel.emit('pusher:subscription_succeeded', {});
110
+ }
111
+
112
+ return channel;
113
+ }
114
+
115
+ unsubscribe(channelName) {
116
+ if (!this.channels.has(channelName)) return;
117
+ const channel = this.channels.get(channelName);
118
+ channel.subscribed = false;
119
+ this.channels.delete(channelName);
120
+
121
+ if (channel._eventSource) {
122
+ channel._eventSource.close();
123
+ }
124
+ }
125
+
126
+ _authorizeChannel(channelName) {
127
+ return fetch(this.authEndpoint, {
128
+ method: 'POST',
129
+ headers: {
130
+ 'Content-Type': 'application/json',
131
+ 'Accept': 'application/json',
132
+ ...this.headers
133
+ },
134
+ body: JSON.stringify({
135
+ channel_name: channelName,
136
+ socket_id: this.socketId
137
+ })
138
+ }).then(res => {
139
+ if (!res.ok) {
140
+ throw new Error(`HTTP ${res.status} auth failure`);
141
+ }
142
+ return res.json();
143
+ });
144
+ }
145
+
146
+ _connectSse(channelName) {
147
+ const channel = this.channels.get(channelName);
148
+ if (!channel || typeof window === 'undefined' || typeof EventSource === 'undefined') return;
149
+
150
+ const origin = window.location ? window.location.origin : 'http://localhost';
151
+ const url = new URL(this.endpoint, origin);
152
+ url.searchParams.set('channel', channelName);
153
+ url.searchParams.set('socket_id', this.socketId);
154
+ if (channel.authData && channel.authData.auth) {
155
+ url.searchParams.set('auth', String(channel.authData.auth));
156
+ }
157
+ if (channel.authData && channel.authData.channel_data) {
158
+ url.searchParams.set('channel_data', String(channel.authData.channel_data));
159
+ }
160
+ if (this.lastEventId > 0) {
161
+ url.searchParams.set('last_event_id', String(this.lastEventId));
162
+ }
163
+
164
+ const es = new EventSource(url.toString());
165
+ channel._eventSource = es;
166
+
167
+ es.onopen = () => {
168
+ this.isConnected = true;
169
+ this.reconnectAttempts = 0;
170
+ };
171
+
172
+ es.onmessage = (e) => {
173
+ if (e.lastEventId) {
174
+ this.lastEventId = parseInt(e.lastEventId, 10);
175
+ }
176
+ try {
177
+ const parsed = JSON.parse(e.data);
178
+ const eventName = parsed.event || 'message';
179
+ const payload = parsed.data !== undefined ? parsed.data : parsed;
180
+ channel.emit(eventName, payload);
181
+ } catch (err) {
182
+ channel.emit('message', e.data);
183
+ }
184
+ };
185
+
186
+ es.onerror = () => {
187
+ this.isConnected = false;
188
+ es.close();
189
+
190
+ if (channel.subscribed) {
191
+ this.reconnectAttempts++;
192
+ const delay = Math.min(1000 * Math.pow(1.5, this.reconnectAttempts), this.maxReconnectDelay);
193
+ setTimeout(() => {
194
+ if (channel.subscribed) {
195
+ this._connectSse(channelName);
196
+ }
197
+ }, delay);
198
+ }
199
+ };
200
+ }
201
+
202
+ disconnect() {
203
+ this.channels.forEach(ch => {
204
+ ch.subscribed = false;
205
+ if (ch._eventSource) {
206
+ ch._eventSource.close();
207
+ }
208
+ });
209
+ this.channels.clear();
210
+ this.isConnected = false;
211
+ }
212
+ }
213
+
214
+ /**
215
+ * HybridBroadcasterClient
216
+ *
217
+ * Provides primary Pusher routing with automatic, zero-downtime failover to
218
+ * SovereignBroadcaster if Pusher fails, disconnects, times out, or throws an error.
219
+ */
220
+ export class HybridBroadcasterClient {
221
+ constructor(options = {}) {
222
+ this.pusherKey = options.pusherKey || options.key || '';
223
+ this.pusherOptions = options.pusherOptions || { cluster: options.cluster || 'eu' };
224
+ this.sovereignOptions = {
225
+ endpoint: options.endpoint || '/api/broadcasting/events',
226
+ authEndpoint: options.authEndpoint || '/api/broadcasting/auth',
227
+ headers: options.headers || {}
228
+ };
229
+ this.activeClient = null;
230
+ this.channels = new Map();
231
+ this.isUsingFallback = false;
232
+ this.connectTimeoutMs = options.connectTimeoutMs || 4000;
233
+
234
+ this._initTransport();
235
+ }
236
+
237
+ _initTransport() {
238
+ const hasPusher = typeof window !== 'undefined' && typeof window.Pusher === 'function' && this.pusherKey !== '';
239
+ if (hasPusher) {
240
+ try {
241
+ const pusherInstance = new window.Pusher(this.pusherKey, this.pusherOptions);
242
+ this.activeClient = pusherInstance;
243
+
244
+ if (pusherInstance.connection) {
245
+ pusherInstance.connection.bind('error', (err) => {
246
+ console.warn('[HybridBroadcaster] Pusher error encountered, failing over to SovereignBroadcaster:', err);
247
+ this._failoverToSovereign();
248
+ });
249
+ pusherInstance.connection.bind('unavailable', () => {
250
+ console.warn('[HybridBroadcaster] Pusher unavailable, failing over to SovereignBroadcaster');
251
+ this._failoverToSovereign();
252
+ });
253
+ pusherInstance.connection.bind('failed', () => {
254
+ console.warn('[HybridBroadcaster] Pusher connection failed, failing over to SovereignBroadcaster');
255
+ this._failoverToSovereign();
256
+ });
257
+
258
+ setTimeout(() => {
259
+ if (pusherInstance.connection.state !== 'connected' && !this.isUsingFallback) {
260
+ console.warn('[HybridBroadcaster] Pusher connection timeout exceeded, failing over to SovereignBroadcaster');
261
+ this._failoverToSovereign();
262
+ }
263
+ }, this.connectTimeoutMs);
264
+ }
265
+ } catch (e) {
266
+ console.warn('[HybridBroadcaster] Failed to initialise Pusher, defaulting to SovereignBroadcaster:', e);
267
+ this._failoverToSovereign();
268
+ }
269
+ } else {
270
+ this._failoverToSovereign();
271
+ }
272
+ }
273
+
274
+ _failoverToSovereign() {
275
+ if (this.isUsingFallback) return;
276
+ this.isUsingFallback = true;
277
+ const sovereign = new SovereignBroadcasterClient(this.sovereignOptions);
278
+ this.activeClient = sovereign;
279
+
280
+ this.channels.forEach((channelData, channelName) => {
281
+ const sovChannel = sovereign.subscribe(channelName);
282
+ channelData.events.forEach((callbacks, eventName) => {
283
+ callbacks.forEach(cb => sovChannel.bind(eventName, cb));
284
+ });
285
+ });
286
+ }
287
+
288
+ subscribe(channelName) {
289
+ if (!this.channels.has(channelName)) {
290
+ this.channels.set(channelName, { events: new Map() });
291
+ }
292
+
293
+ const channelProxy = {
294
+ name: channelName,
295
+ bind: (event, callback) => {
296
+ const chData = this.channels.get(channelName);
297
+ if (!chData.events.has(event)) {
298
+ chData.events.set(event, []);
299
+ }
300
+ chData.events.get(event).push(callback);
301
+
302
+ if (this.activeClient && typeof this.activeClient.subscribe === 'function') {
303
+ const sub = this.activeClient.subscribe(channelName);
304
+ if (sub && typeof sub.bind === 'function') {
305
+ sub.bind(event, callback);
306
+ }
307
+ }
308
+ return channelProxy;
309
+ }
310
+ };
311
+
312
+ if (this.activeClient && typeof this.activeClient.subscribe === 'function') {
313
+ this.activeClient.subscribe(channelName);
314
+ }
315
+
316
+ return channelProxy;
317
+ }
318
+
319
+ disconnect() {
320
+ if (this.activeClient && typeof this.activeClient.disconnect === 'function') {
321
+ this.activeClient.disconnect();
322
+ }
323
+ this.channels.clear();
324
+ }
325
+ }