@shoru/kitten 0.0.6 → 0.1.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.
@@ -0,0 +1,41 @@
1
+ import { DisconnectReason } from 'baileys';
2
+ import { pino } from '#internals.js';
3
+
4
+ export const ConnectionState = Object.freeze({
5
+ DISCONNECTED: 'disconnected',
6
+ CONNECTING: 'connecting',
7
+ CONNECTED: 'connected',
8
+ RECONNECTING: 'reconnecting',
9
+ });
10
+
11
+ export const DISCONNECT_HANDLERS = new Map([
12
+ [DisconnectReason.connectionClosed, { message: 'Connection closed', recoverable: true }],
13
+ [DisconnectReason.restartRequired, { message: 'Restart required', recoverable: true, isRestart: true }],
14
+ [DisconnectReason.timedOut, { message: 'Connection timed out', recoverable: true }],
15
+ [DisconnectReason.connectionLost, { message: 'Connection lost', recoverable: true }],
16
+ [DisconnectReason.unavailableService, { message: 'Service unavailable', recoverable: true }],
17
+ [DisconnectReason.loggedOut, { message: 'Session logged out', recoverable: false, deleteSession: true }],
18
+ [DisconnectReason.connectionReplaced, { message: 'Connection replaced by another session', recoverable: false }],
19
+ [DisconnectReason.badSession, { message: 'Corrupted session', recoverable: false, deleteSession: true }],
20
+ [DisconnectReason.multideviceMismatch, { message: 'Multi-device mismatch', recoverable: false }],
21
+ [DisconnectReason.forbidden, { message: 'Account banned', recoverable: false, deleteSession: true }],
22
+ [405, { message: 'Not logged in', recoverable: false, deleteSession: true }],
23
+ ]);
24
+
25
+ export const silentLogger = Object.freeze({
26
+ trace: () => {},
27
+ debug: () => {},
28
+ info: () => {},
29
+ warn: () => {},
30
+ error: () => {},
31
+ fatal: () => {},
32
+ prompt: () => {},
33
+ child: () => silentLogger,
34
+ });
35
+
36
+ export const silentPino = pino({ level: 'silent' });
37
+
38
+ export const DEFAULT_MAX_RETRIES = 30;
39
+
40
+ export const defaultBackoff = (attempt) => Math.min(1000 * 2 ** (attempt - 1), 60_000);
41
+
@@ -0,0 +1,47 @@
1
+ import { isBoom } from '@hapi/boom';
2
+ import { DISCONNECT_HANDLERS } from './constants.js';
3
+
4
+ export class ConnectionError extends Error {
5
+ constructor(message, { statusCode, recoverable = true } = {}) {
6
+ super(message);
7
+ this.name = 'ConnectionError';
8
+ this.statusCode = statusCode;
9
+ this.recoverable = recoverable;
10
+ }
11
+ }
12
+
13
+ export function parseDisconnectReason(lastDisconnect) {
14
+ const error = lastDisconnect?.error;
15
+ let statusCode = null;
16
+
17
+ if (isBoom(error)) {
18
+ statusCode = error.output?.statusCode;
19
+ } else if (error?.output?.statusCode) {
20
+ statusCode = error.output.statusCode;
21
+ } else if (typeof error?.statusCode === 'number') {
22
+ statusCode = error.statusCode;
23
+ }
24
+
25
+ if (!statusCode) {
26
+ return {
27
+ message: error?.message || 'Connection failure',
28
+ statusCode: 'NETWORK_ERROR',
29
+ recoverable: true,
30
+ deleteSession: false,
31
+ };
32
+ }
33
+
34
+ const handler = DISCONNECT_HANDLERS.get(statusCode);
35
+
36
+ if (!handler) {
37
+ return {
38
+ message: error?.message || `Unknown disconnect reason (code: ${statusCode})`,
39
+ statusCode,
40
+ recoverable: true,
41
+ deleteSession: false,
42
+ };
43
+ }
44
+
45
+ return { ...handler, statusCode };
46
+ }
47
+
@@ -0,0 +1,69 @@
1
+ import { EventEmitter } from 'events';
2
+
3
+ /**
4
+ * Creates a lightweight, zero-latency persistent event bus for WASocket.
5
+ * Forwards Baileys events directly to subscribers with zero buffering overhead,
6
+ * maintaining persistent listener registrations across socket reconnections.
7
+ */
8
+ export function createPersistentEventBus() {
9
+ const emitter = new EventEmitter();
10
+ emitter.setMaxListeners(0); // Unlimited listeners
11
+
12
+ return {
13
+ on(event, listener) {
14
+ emitter.on(event, listener);
15
+ return this;
16
+ },
17
+
18
+ off(event, listener) {
19
+ emitter.off(event, listener);
20
+ return this;
21
+ },
22
+
23
+ addListener(event, listener) {
24
+ emitter.addListener(event, listener);
25
+ return this;
26
+ },
27
+
28
+ removeListener(event, listener) {
29
+ emitter.removeListener(event, listener);
30
+ return this;
31
+ },
32
+
33
+ removeAllListeners(event) {
34
+ emitter.removeAllListeners(event);
35
+ return this;
36
+ },
37
+
38
+ emit(event, data) {
39
+ return emitter.emit(event, data);
40
+ },
41
+
42
+ process(handler) {
43
+ const listener = async (event, data) => {
44
+ try {
45
+ await handler({ [event]: data });
46
+ } catch {
47
+ /* noop */
48
+ }
49
+ };
50
+ emitter.on('event', listener);
51
+ return () => {
52
+ emitter.off('event', listener);
53
+ };
54
+ },
55
+
56
+ // Baileys buffer compatibility stubs (Baileys raw socket already handles buffering)
57
+ buffer() {},
58
+ flush() {
59
+ return false;
60
+ },
61
+ isBuffering() {
62
+ return false;
63
+ },
64
+ createBufferedFunction(work) {
65
+ return work;
66
+ },
67
+ };
68
+ }
69
+
@@ -1,2 +1,9 @@
1
- export * from "./getConnectionConfig.js"
2
- export * from "./client.js"
1
+ export * from './constants.js';
2
+ export * from './errors.js';
3
+ export * from './registry.js';
4
+ export * from './socket-proxy.js';
5
+ export * from './event-bus.js';
6
+ export * from './auth-handler.js';
7
+ export * from './sync-manager.js';
8
+ export * from './getConnectionConfig.js';
9
+ export * from './client.js';
@@ -0,0 +1,63 @@
1
+ const clients = new Map();
2
+
3
+ let isSyncing = false;
4
+ let isConfiguring = false;
5
+
6
+ export const ClientRegistry = {
7
+ get(id) {
8
+ return clients.get(id);
9
+ },
10
+
11
+ has(id) {
12
+ return clients.has(id);
13
+ },
14
+
15
+ set(id, client) {
16
+ if (id != null) {
17
+ clients.set(id, client);
18
+ }
19
+ },
20
+
21
+ delete(id) {
22
+ if (id != null) {
23
+ clients.delete(id);
24
+ }
25
+ },
26
+
27
+ get size() {
28
+ return clients.size;
29
+ },
30
+
31
+ keys() {
32
+ return clients.keys();
33
+ },
34
+
35
+ values() {
36
+ return clients.values();
37
+ },
38
+
39
+ entries() {
40
+ return clients.entries();
41
+ },
42
+
43
+ [Symbol.iterator]() {
44
+ return clients.values();
45
+ },
46
+
47
+ get isSyncing() {
48
+ return isSyncing;
49
+ },
50
+
51
+ set isSyncing(val) {
52
+ isSyncing = Boolean(val);
53
+ },
54
+
55
+ get isConfiguring() {
56
+ return isConfiguring;
57
+ },
58
+
59
+ set isConfiguring(val) {
60
+ isConfiguring = Boolean(val);
61
+ },
62
+ };
63
+
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Creates a transparent proxy around the underlying WASocket instance.
3
+ * Intercepts the `ev` property to always return the persistent EventBuffer,
4
+ * ensuring all event listeners survive reconnections seamlessly.
5
+ *
6
+ * @param {() => any} getRawSock - Accessor for the active raw WASocket
7
+ * @param {() => any} getEv - Accessor for the persistent EventBuffer
8
+ * @returns {Proxy} The wrapped socket proxy
9
+ */
10
+ export function createSocketProxy(getRawSock, getEv) {
11
+ return new Proxy({}, {
12
+ get(target, prop, receiver) {
13
+ if (prop === 'ev') {
14
+ return getEv();
15
+ }
16
+ const raw = getRawSock();
17
+ if (!raw) {
18
+ return undefined;
19
+ }
20
+ const value = Reflect.get(raw, prop, raw);
21
+ if (typeof value === 'function') {
22
+ return value.bind(raw);
23
+ }
24
+ return value;
25
+ },
26
+
27
+ set(target, prop, value, receiver) {
28
+ const raw = getRawSock();
29
+ if (raw) {
30
+ return Reflect.set(raw, prop, value, raw);
31
+ }
32
+ return true;
33
+ },
34
+
35
+ has(target, prop) {
36
+ if (prop === 'ev') return true;
37
+ const raw = getRawSock();
38
+ return raw ? Reflect.has(raw, prop) : false;
39
+ },
40
+
41
+ ownKeys(target) {
42
+ const raw = getRawSock();
43
+ const keys = raw ? Reflect.ownKeys(raw) : [];
44
+ if (!keys.includes('ev')) keys.push('ev');
45
+ return keys;
46
+ },
47
+
48
+ getOwnPropertyDescriptor(target, prop) {
49
+ if (prop === 'ev') {
50
+ return { enumerable: true, configurable: true, value: getEv() };
51
+ }
52
+ const raw = getRawSock();
53
+ return raw ? Reflect.getOwnPropertyDescriptor(raw, prop) : undefined;
54
+ }
55
+ });
56
+ }
57
+
@@ -0,0 +1,79 @@
1
+ import { listSessions } from '#auth.js';
2
+ import { ClientRegistry } from './registry.js';
3
+
4
+ /**
5
+ * Restores a specific session in the background.
6
+ *
7
+ * @param {string|number} sessionId - Session identifier to restore
8
+ * @param {any} ClientClass - The Client class constructor
9
+ * @returns {Promise<boolean>} True if restored, false if already active
10
+ */
11
+ export async function restoreSession(sessionId, ClientClass) {
12
+ if (ClientRegistry.has(sessionId)) {
13
+ return false;
14
+ }
15
+
16
+ const client = new ClientClass({
17
+ id: sessionId,
18
+ silent: true,
19
+ sync: true,
20
+ maxRetries: 3,
21
+ });
22
+
23
+ await client.connect();
24
+ return true;
25
+ }
26
+
27
+ /**
28
+ * Synchronizes and restores all other existing sessions in the background.
29
+ *
30
+ * @param {object} params
31
+ * @param {string|number} params.currentId - The current active client session ID
32
+ * @param {string} params.flag - Logging tag
33
+ * @param {object} params.logger - Logger instance
34
+ * @param {any} params.ClientClass - The Client class constructor
35
+ */
36
+ export async function syncOtherSessions({ currentId, flag, logger, ClientClass }) {
37
+ if (ClientRegistry.isSyncing) return;
38
+ ClientRegistry.isSyncing = true;
39
+
40
+ try {
41
+ const allSessionIds = listSessions();
42
+ const otherSessionIds = allSessionIds.filter((id) => id !== currentId);
43
+
44
+ if (otherSessionIds.length === 0) {
45
+ logger.debug(`[${flag}] No other sessions to sync`);
46
+ return;
47
+ }
48
+
49
+ logger.debug(
50
+ `[${flag}] Syncing ${otherSessionIds.length} other session(s) in background`
51
+ );
52
+
53
+ const results = await Promise.allSettled(
54
+ otherSessionIds.map((sessionId) => restoreSession(sessionId, ClientClass))
55
+ );
56
+
57
+ let successCount = 0;
58
+ let skipCount = 0;
59
+ let failCount = 0;
60
+
61
+ for (const result of results) {
62
+ if (result.status === 'fulfilled') {
63
+ if (result.value === true) successCount++;
64
+ else skipCount++;
65
+ } else {
66
+ failCount++;
67
+ }
68
+ }
69
+
70
+ logger.debug(
71
+ `[${flag}] Session sync complete: ${successCount} restored, ${skipCount} skipped, ${failCount} failed`
72
+ );
73
+ } catch (err) {
74
+ logger.error(err, `[${flag}] Error during session sync`);
75
+ } finally {
76
+ ClientRegistry.isSyncing = false;
77
+ }
78
+ }
79
+
@@ -1,39 +1,39 @@
1
- import { Browsers } from "baileys";
2
- import pino from "pino";
3
-
4
- const socket = {
5
- browser: Browsers.ubuntu('Chrome'),
6
- markOnlineOnConnect: false,
7
- syncFullHistory: false,
8
- generateHighQualityLinkPreview: true,
9
- shouldIgnoreJid: () => false,
10
- shouldSyncHistoryMessage: () => false,
11
- logger: pino({ level: "silent" }),
12
- }
13
-
14
- const db = {
15
- path: './db',
16
- compression: true,
17
- mapSize: 2 * 1024 * 1024 * 1024, // 2 GB
18
- maxReaders: 126,
19
- noSync: false,
20
- noMetaSync: false,
21
- };
22
-
23
- const plugins = {
24
- dir: 'plugins',
25
- prefixes: ['.', '\\', '!'],
26
- defaultEvent: 'messages.upsert',
27
- hmr: {
28
- enable: false,
29
- debounce: 200,
30
- debug: false
31
- }
32
- }
33
-
34
- export default {
35
- db,
36
- socket,
37
- plugins,
38
- timeZone: 'Africa/Casablanca'
39
- }
1
+ import { Browsers } from "baileys";
2
+ import pino from "pino";
3
+
4
+ const socket = {
5
+ browser: Browsers.ubuntu('Chrome'),
6
+ markOnlineOnConnect: false,
7
+ syncFullHistory: false,
8
+ generateHighQualityLinkPreview: false,
9
+ shouldIgnoreJid: () => false,
10
+ shouldSyncHistoryMessage: () => false,
11
+ logger: pino({ level: "silent" }),
12
+ };
13
+
14
+ const db = {
15
+ path: './db',
16
+ compression: true,
17
+ mapSize: 2 * 1024 * 1024 * 1024, // 2 GB
18
+ maxReaders: 126,
19
+ noSync: true,
20
+ noMetaSync: true,
21
+ };
22
+
23
+ const plugins = {
24
+ dir: 'plugins',
25
+ prefixes: ['.', '\\', '!'],
26
+ defaultEvent: 'messages.upsert',
27
+ hmr: {
28
+ enable: false,
29
+ debounce: 200,
30
+ debug: false,
31
+ },
32
+ };
33
+
34
+ export default {
35
+ db,
36
+ socket,
37
+ plugins,
38
+ timeZone: 'Africa/Casablanca',
39
+ };