api-ape 0.0.0 → 1.0.1

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 (63) hide show
  1. package/README.md +261 -0
  2. package/client/README.md +69 -0
  3. package/client/browser.js +17 -0
  4. package/client/connectSocket.js +260 -0
  5. package/dist/ape.js +454 -0
  6. package/example/ExpressJs/README.md +97 -0
  7. package/example/ExpressJs/api/message.js +11 -0
  8. package/example/ExpressJs/backend.js +37 -0
  9. package/example/ExpressJs/index.html +88 -0
  10. package/example/ExpressJs/package-lock.json +834 -0
  11. package/example/ExpressJs/package.json +10 -0
  12. package/example/ExpressJs/styles.css +128 -0
  13. package/example/NextJs/.dockerignore +29 -0
  14. package/example/NextJs/Dockerfile +52 -0
  15. package/example/NextJs/Dockerfile.dev +27 -0
  16. package/example/NextJs/README.md +113 -0
  17. package/example/NextJs/ape/client.js +66 -0
  18. package/example/NextJs/ape/embed.js +12 -0
  19. package/example/NextJs/ape/index.js +23 -0
  20. package/example/NextJs/ape/logic/chat.js +62 -0
  21. package/example/NextJs/ape/onConnect.js +69 -0
  22. package/example/NextJs/ape/onDisconnect.js +13 -0
  23. package/example/NextJs/ape/onError.js +9 -0
  24. package/example/NextJs/ape/onReceive.js +15 -0
  25. package/example/NextJs/ape/onSend.js +15 -0
  26. package/example/NextJs/api/message.js +44 -0
  27. package/example/NextJs/docker-compose.yml +22 -0
  28. package/example/NextJs/next-env.d.ts +5 -0
  29. package/example/NextJs/next.config.js +8 -0
  30. package/example/NextJs/package-lock.json +5107 -0
  31. package/example/NextJs/package.json +25 -0
  32. package/example/NextJs/pages/_app.tsx +6 -0
  33. package/example/NextJs/pages/index.tsx +182 -0
  34. package/example/NextJs/public/favicon.ico +0 -0
  35. package/example/NextJs/public/vercel.svg +4 -0
  36. package/example/NextJs/server.js +40 -0
  37. package/example/NextJs/styles/Chat.module.css +194 -0
  38. package/example/NextJs/styles/Home.module.css +129 -0
  39. package/example/NextJs/styles/globals.css +26 -0
  40. package/example/NextJs/tsconfig.json +20 -0
  41. package/example/README.md +66 -0
  42. package/index.d.ts +179 -0
  43. package/index.js +11 -0
  44. package/package.json +11 -4
  45. package/server/README.md +93 -0
  46. package/server/index.js +6 -0
  47. package/server/lib/broadcast.js +63 -0
  48. package/server/lib/loader.js +10 -0
  49. package/server/lib/main.js +23 -0
  50. package/server/lib/wiring.js +94 -0
  51. package/server/security/extractRootDomain.js +21 -0
  52. package/server/security/origin.js +13 -0
  53. package/server/security/reply.js +21 -0
  54. package/server/socket/open.js +10 -0
  55. package/server/socket/receive.js +66 -0
  56. package/server/socket/send.js +55 -0
  57. package/server/utils/deepRequire.js +45 -0
  58. package/server/utils/genId.js +24 -0
  59. package/todo.md +85 -0
  60. package/utils/jss.js +273 -0
  61. package/utils/jss.test.js +261 -0
  62. package/utils/messageHash.js +43 -0
  63. package/utils/messageHash.test.js +56 -0
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "chat-example",
3
+ "scripts": {
4
+ "start": "node backend.js"
5
+ },
6
+ "dependencies": {
7
+ "express": "^4.18.2",
8
+ "scribbles": "^1.5.0"
9
+ }
10
+ }
@@ -0,0 +1,128 @@
1
+ * {
2
+ box-sizing: border-box;
3
+ margin: 0;
4
+ padding: 0;
5
+ }
6
+
7
+ body {
8
+ min-height: 100vh;
9
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
10
+ color: #fff;
11
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif;
12
+ display: flex;
13
+ justify-content: center;
14
+ padding: 2rem;
15
+ }
16
+
17
+ chat-app {
18
+ max-width: 500px;
19
+ width: 100%;
20
+ }
21
+
22
+ .chat-container {
23
+ background: rgba(255, 255, 255, 0.05);
24
+ border-radius: 20px;
25
+ overflow: hidden;
26
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
27
+ }
28
+
29
+ .header {
30
+ display: flex;
31
+ justify-content: space-between;
32
+ align-items: center;
33
+ padding: 1rem 1.5rem;
34
+ background: rgba(255, 255, 255, 0.1);
35
+ }
36
+
37
+ .title {
38
+ font-size: 1.25rem;
39
+ font-weight: bold;
40
+ background: linear-gradient(90deg, #00d2ff, #3a7bd5);
41
+ -webkit-background-clip: text;
42
+ -webkit-text-fill-color: transparent;
43
+ background-clip: text;
44
+ }
45
+
46
+ .user-badge {
47
+ font-size: 0.85rem;
48
+ color: #0f0;
49
+ font-weight: bold;
50
+ }
51
+
52
+ .messages {
53
+ height: 350px;
54
+ overflow-y: auto;
55
+ padding: 1rem;
56
+ }
57
+
58
+ .messages::-webkit-scrollbar {
59
+ width: 6px;
60
+ }
61
+
62
+ .messages::-webkit-scrollbar-track {
63
+ background: rgba(255, 255, 255, 0.05);
64
+ }
65
+
66
+ .messages::-webkit-scrollbar-thumb {
67
+ background: rgba(255, 255, 255, 0.2);
68
+ border-radius: 3px;
69
+ }
70
+
71
+ .empty-state {
72
+ text-align: center;
73
+ color: #666;
74
+ margin-top: 140px;
75
+ }
76
+
77
+ .message {
78
+ display: flex;
79
+ flex-direction: column;
80
+ gap: 0.25rem;
81
+ padding: 0.75rem 1rem;
82
+ margin-bottom: 0.5rem;
83
+ background: rgba(255, 255, 255, 0.05);
84
+ border-radius: 12px;
85
+ border-left: 3px solid #3a7bd5;
86
+ }
87
+
88
+ .message.mine {
89
+ background: rgba(0, 210, 255, 0.15);
90
+ border-left-color: #00d2ff;
91
+ }
92
+
93
+ .message .username {
94
+ color: #00d2ff;
95
+ font-size: 0.85rem;
96
+ font-weight: bold;
97
+ }
98
+
99
+ .message .text {
100
+ color: #fff;
101
+ }
102
+
103
+ .input-area {
104
+ display: flex;
105
+ gap: 0.5rem;
106
+ padding: 1rem;
107
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
108
+ }
109
+
110
+ .message-input {
111
+ flex: 1;
112
+ padding: 0.75rem 1rem;
113
+ font-size: 1rem;
114
+ border: none;
115
+ border-radius: 50px;
116
+ background: rgba(255, 255, 255, 0.1);
117
+ color: #fff;
118
+ outline: none;
119
+ transition: background 0.2s;
120
+ }
121
+
122
+ .message-input::placeholder {
123
+ color: rgba(255, 255, 255, 0.5);
124
+ }
125
+
126
+ .message-input:focus {
127
+ background: rgba(255, 255, 255, 0.15);
128
+ }
@@ -0,0 +1,29 @@
1
+ # Dependencies
2
+ node_modules
3
+
4
+ # Next.js build output
5
+ .next
6
+
7
+ # Git
8
+ .git
9
+ .gitignore
10
+
11
+ # IDE
12
+ .idea
13
+ .vscode
14
+ *.swp
15
+ *.swo
16
+
17
+ # Debug
18
+ npm-debug.log*
19
+ yarn-debug.log*
20
+ yarn-error.log*
21
+
22
+ # Testing
23
+ coverage
24
+
25
+ # Misc
26
+ .DS_Store
27
+ *.pem
28
+ .env*.local
29
+ README.md
@@ -0,0 +1,52 @@
1
+ # Install dependencies only when needed
2
+ FROM node:18-alpine AS deps
3
+ RUN apk add --no-cache libc6-compat
4
+ WORKDIR /app
5
+
6
+ # Install dependencies based on the preferred package manager
7
+ COPY package.json package-lock.json* ./
8
+ RUN npm ci
9
+
10
+ # Rebuild the source code only when needed
11
+ FROM node:18-alpine AS builder
12
+ WORKDIR /app
13
+ COPY --from=deps /app/node_modules ./node_modules
14
+ COPY . .
15
+
16
+ # Next.js collects completely anonymous telemetry data about general usage.
17
+ # Learn more here: https://nextjs.org/telemetry
18
+ # Uncomment the following line in case you want to disable telemetry during the build.
19
+ # ENV NEXT_TELEMETRY_DISABLED 1
20
+
21
+ RUN npm run build
22
+
23
+ # Production image, copy all the files and run next
24
+ FROM node:18-alpine AS runner
25
+ WORKDIR /app
26
+
27
+ ENV NODE_ENV production
28
+ # Uncomment the following line in case you want to disable telemetry during runtime.
29
+ # ENV NEXT_TELEMETRY_DISABLED 1
30
+
31
+ RUN addgroup --system --gid 1001 nodejs
32
+ RUN adduser --system --uid 1001 nextjs
33
+
34
+ COPY --from=builder /app/public ./public
35
+
36
+ # Set the correct permission for prerender cache
37
+ RUN mkdir .next
38
+ RUN chown nextjs:nodejs .next
39
+
40
+ # Automatically leverage output traces to reduce image size
41
+ # https://nextjs.org/docs/advanced-features/output-file-tracing
42
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
43
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
44
+
45
+ USER nextjs
46
+
47
+ EXPOSE 3000
48
+
49
+ ENV PORT 3000
50
+ ENV HOSTNAME "0.0.0.0"
51
+
52
+ CMD ["node", "server.js"]
@@ -0,0 +1,27 @@
1
+ # Development Dockerfile with hot reload support
2
+ # Context: api-ape root (not example/NextJs)
3
+ FROM node:18-alpine
4
+
5
+ WORKDIR /api-ape
6
+
7
+ # First install api-ape root dependencies (ws, ua-parser-js, etc.)
8
+ COPY package.json package-lock.json* ./
9
+ RUN npm install
10
+
11
+ # Then install NextJs example dependencies
12
+ WORKDIR /api-ape/example/NextJs
13
+ COPY example/NextJs/package.json example/NextJs/package-lock.json* ./
14
+ RUN npm install
15
+
16
+ # Copy all source files
17
+ WORKDIR /api-ape
18
+ COPY . .
19
+
20
+ # Back to NextJs example
21
+ WORKDIR /api-ape/example/NextJs
22
+
23
+ # Expose port
24
+ EXPOSE 3000
25
+
26
+ # Run in development mode
27
+ CMD ["npm", "run", "dev"]
@@ -0,0 +1,113 @@
1
+ # 🦍 NextJs — Complete Example
2
+
3
+ A full-featured real-time chat application with Next.js and api-ape.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npm install
9
+ npm run dev
10
+ ```
11
+
12
+ Open http://localhost:3000 in your browser.
13
+
14
+ ## Docker
15
+
16
+ ```bash
17
+ docker-compose up --build
18
+ ```
19
+
20
+ ## Project Structure
21
+
22
+ ```
23
+ NextJs/
24
+ ├── server.js # Custom Next.js server with api-ape
25
+ ├── api/
26
+ │ └── message.js # Message controller
27
+ ├── ape/
28
+ │ ├── index.js # Ape exports
29
+ │ ├── client.js # Browser client wrapper
30
+ │ ├── onConnect.js # Connection lifecycle
31
+ │ ├── onDisconnect.js # Disconnect handler
32
+ │ ├── onReceive.js # Message logging
33
+ │ ├── onSend.js # Send logging
34
+ │ ├── onError.js # Error handling
35
+ │ └── embed.js # Embedded context values
36
+ ├── pages/
37
+ │ └── index.tsx # Chat UI
38
+ └── styles/
39
+ └── Chat.module.css # Chat styling
40
+ ```
41
+
42
+ ## Features
43
+
44
+ - **Custom Server** — Express + Next.js with api-ape integration
45
+ - **Connection Lifecycle** — onConnect, onDisconnect, onReceive, onSend hooks
46
+ - **User Presence** — Track online users count
47
+ - **Message History** — New users receive chat history
48
+ - **React Integration** — Hooks-based client usage
49
+ - **Docker Support** — Production-ready containerization
50
+
51
+ ## How It Works
52
+
53
+ ### Server (server.js)
54
+
55
+ ```js
56
+ const express = require('express')
57
+ const next = require('next')
58
+ const ape = require('api-ape')
59
+ const { onConnect } = require('./ape/onConnect')
60
+
61
+ const app = next({ dev: true })
62
+ const server = express()
63
+
64
+ ape(server, { where: 'api', onConnent: onConnect })
65
+ server.all('*', app.getRequestHandler())
66
+ server.listen(3000)
67
+ ```
68
+
69
+ ### Connection Lifecycle (ape/onConnect.js)
70
+
71
+ ```js
72
+ module.exports.onConnect = (socket, req, send) => ({
73
+ embed: { userId: generateId() },
74
+ onReceive: (queryId, data, type) => { ... },
75
+ onSend: (data, type) => { ... },
76
+ onDisconnent: () => { ... }
77
+ })
78
+ ```
79
+
80
+ ### React Client (pages/index.tsx)
81
+
82
+ ```bash
83
+ npm i api-ape
84
+ ```
85
+
86
+ ```jsx
87
+ import ape from 'api-ape'
88
+
89
+ // Configure and connect
90
+ ape.configure({ port: 3000 })
91
+ const { sender, setOnReciver } = ape()
92
+ ape.autoReconnect()
93
+
94
+ useEffect(() => {
95
+ setOnReciver('message', ({ data }) => {
96
+ setMessages(prev => [...prev, data.message])
97
+ })
98
+ }, [])
99
+
100
+ // Send message
101
+ sender.message({ user, text }).then(response => { ... })
102
+ ```
103
+
104
+ ## Key Concepts Demonstrated
105
+
106
+ | Concept | File |
107
+ |---------|------|
108
+ | Custom Next.js server | `server.js` |
109
+ | Connection lifecycle hooks | `ape/onConnect.js` |
110
+ | Embedded context values | `ape/embed.js` |
111
+ | React hooks integration | `pages/index.tsx` |
112
+ | Client wrapper | `ape/client.js` |
113
+ | Message validation | `api/message.js` |
@@ -0,0 +1,66 @@
1
+ /**
2
+ * api-ape client singleton
3
+ * Initializes once and is ready for use throughout the app
4
+ */
5
+
6
+ let apeClient = null
7
+ let isConnecting = false
8
+ let connectionPromise = null
9
+
10
+ /**
11
+ * Get the api-ape client - initializes on first call
12
+ */
13
+ export async function getApeClient() {
14
+ if (apeClient) {
15
+ return apeClient
16
+ }
17
+
18
+ if (isConnecting) {
19
+ return connectionPromise
20
+ }
21
+
22
+ isConnecting = true
23
+ connectionPromise = initClient()
24
+
25
+ try {
26
+ apeClient = await connectionPromise
27
+ return apeClient
28
+ } finally {
29
+ isConnecting = false
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Initialize the api-ape client
35
+ */
36
+ async function initClient() {
37
+ if (typeof window === 'undefined') {
38
+ return null
39
+ }
40
+
41
+ const module = await import('api-ape/client/connectSocket')
42
+ const connectSocket = module.default
43
+
44
+ // Configure for current port
45
+ const port = window.location.port || (window.location.protocol === 'https:' ? 443 : 80)
46
+ connectSocket.configure({ port: parseInt(port, 10) })
47
+
48
+ // Connect and get sender/receiver
49
+ const { sender, setOnReciver } = connectSocket()
50
+
51
+ // Enable auto-reconnect
52
+ connectSocket.autoReconnect()
53
+
54
+ console.log('🦍 api-ape client initialized')
55
+
56
+ return { sender, setOnReciver, connectSocket }
57
+ }
58
+
59
+ /**
60
+ * Check if client is connected
61
+ */
62
+ export function isConnected() {
63
+ return apeClient !== null
64
+ }
65
+
66
+ export default getApeClient
@@ -0,0 +1,12 @@
1
+ /**
2
+ * api-ape embed data
3
+ */
4
+
5
+ function createEmbed(clientID, sessionID) {
6
+ return {
7
+ clientID,
8
+ sessionID: sessionID || 'session-' + clientID,
9
+ }
10
+ }
11
+
12
+ module.exports = { createEmbed }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * api-ape handlers - main export
3
+ */
4
+
5
+ const { onConnect, history, online } = require('./onConnect')
6
+ const { createEmbed } = require('./embed')
7
+ const { onReceive } = require('./onReceive')
8
+ const { onSend } = require('./onSend')
9
+ const { onError } = require('./onError')
10
+ const { onDisconnect } = require('./onDisconnect')
11
+ const chat = require('./logic/chat')
12
+
13
+ module.exports = {
14
+ onConnect,
15
+ history,
16
+ online,
17
+ createEmbed,
18
+ onReceive,
19
+ onSend,
20
+ onError,
21
+ onDisconnect,
22
+ chat
23
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Chat controller for api-ape
3
+ * Uses this.broadcastOthers from api-ape
4
+ */
5
+
6
+ // In-memory message store
7
+ const messages = []
8
+ const MAX_MESSAGES = 100
9
+
10
+ /**
11
+ * Message controller - called when client sends type="message"
12
+ * Uses this.broadcastOthers to send to all OTHER clients
13
+ */
14
+ function message(data) {
15
+ const { user, text } = data
16
+
17
+ if (!user || !text) {
18
+ throw new Error('Missing user or text')
19
+ }
20
+
21
+ const msg = {
22
+ user,
23
+ text,
24
+ time: new Date().toISOString()
25
+ }
26
+
27
+ // Store message
28
+ messages.push(msg)
29
+ if (messages.length > MAX_MESSAGES) {
30
+ messages.shift()
31
+ }
32
+
33
+ // Broadcast to all OTHER clients (exclude sender)
34
+ // this.broadcastOthers is provided by api-ape!
35
+ this.broadcastOthers('message', { message: msg })
36
+
37
+ // Return to sender (fulfills promise)
38
+ return { ok: true, message: msg }
39
+ }
40
+
41
+ /**
42
+ * Get message history
43
+ */
44
+ function history() {
45
+ return messages.slice(-50)
46
+ }
47
+
48
+ /**
49
+ * Get history controller - called when client sends type="history"
50
+ */
51
+ function getHistory() {
52
+ return {
53
+ history: history(),
54
+ users: this.online()
55
+ }
56
+ }
57
+
58
+ module.exports = {
59
+ message,
60
+ history: getHistory,
61
+ _history: history // internal use
62
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * api-ape onConnect handler
3
+ * Creates the handlers object returned from onConnent
4
+ */
5
+
6
+ const { createEmbed } = require('./embed')
7
+ const { onReceive } = require('./onReceive')
8
+ const { onSend } = require('./onSend')
9
+ const { onError } = require('./onError')
10
+ const { broadcast, online } = require('api-ape/server/lib/broadcast')
11
+
12
+ // Get message history from the message controller
13
+ function getHistory() {
14
+ try {
15
+ const messageController = require('../api/message')
16
+ return messageController.getHistory ? messageController.getHistory() : []
17
+ } catch (e) {
18
+ return []
19
+ }
20
+ }
21
+
22
+ function onConnect(socket, req, send) {
23
+ const clientID = send.toString()
24
+ console.log(`🦍 Client connected: ${clientID}`)
25
+
26
+ const embed = createEmbed(clientID, req.headers?.['x-session-id'])
27
+
28
+ // Send init message with history and user count after a tiny delay
29
+ // (to ensure client is ready to receive)
30
+ setTimeout(() => {
31
+ console.log(`📤 Sending init to ${clientID}, users: ${online()}`)
32
+ try {
33
+ send('init', {
34
+ history: getHistory(),
35
+ users: online()
36
+ })
37
+ console.log(`✅ Init sent to ${clientID}`)
38
+ } catch (e) {
39
+ console.error(`❌ Failed to send init:`, e)
40
+ }
41
+
42
+ // Broadcast updated user count to all clients
43
+ broadcast('users', { count: online() })
44
+ }, 100)
45
+
46
+ return {
47
+ embed,
48
+
49
+ onReceive: (queryId, payload, type) =>
50
+ onReceive(clientID, queryId, payload, type),
51
+
52
+ onSend: (payload, type) =>
53
+ onSend(clientID, payload, type),
54
+
55
+ onError: (errStr) =>
56
+ onError(clientID, errStr),
57
+
58
+ onDisconnent: () => {
59
+ console.info(`👋 Disconnected [${clientID}]`)
60
+ // Broadcast updated user count after disconnect
61
+ // Use setTimeout to ensure client is removed first
62
+ setTimeout(() => {
63
+ broadcast('users', { count: online() })
64
+ }, 50)
65
+ }
66
+ }
67
+ }
68
+
69
+ module.exports = { onConnect }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * api-ape onDisconnect handler
3
+ */
4
+
5
+ const { broadcast, online } = require('./logic/chat')
6
+
7
+ function onDisconnect(clientID, unsubscribe) {
8
+ console.info(`👋 Disconnected [${clientID}]`)
9
+ unsubscribe()
10
+ broadcast('users', { count: online() })
11
+ }
12
+
13
+ module.exports = { onDisconnect }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * api-ape onError handler
3
+ */
4
+
5
+ function onError(clientID, errStr) {
6
+ console.error(`🦍 Error [${clientID}]:`, errStr)
7
+ }
8
+
9
+ module.exports = { onError }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * api-ape onReceive handler
3
+ */
4
+
5
+ function onReceive(clientID, queryId, payload, type) {
6
+ console.log(`📥 [${clientID}] ${type}:`, JSON.stringify(payload).slice(0, 50))
7
+
8
+ return (err, result) => {
9
+ if (err) {
10
+ console.error(`❌ [${clientID}] Error:`, err.message)
11
+ }
12
+ }
13
+ }
14
+
15
+ module.exports = { onReceive }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * api-ape onSend handler
3
+ */
4
+
5
+ function onSend(clientID, payload, type) {
6
+ console.log(`📤 [${clientID}] ${type}`)
7
+
8
+ return (err, result) => {
9
+ if (err) {
10
+ console.error(`❌ [${clientID}] Send failed:`, err.message)
11
+ }
12
+ }
13
+ }
14
+
15
+ module.exports = { onSend }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Message controller for api-ape
3
+ * Called when client sends type="message"
4
+ *
5
+ * Uses this.broadcastOthers from api-ape to broadcast to all other clients
6
+ */
7
+
8
+ // In-memory message store
9
+ const messages = []
10
+ const MAX_MESSAGES = 100
11
+
12
+ /**
13
+ * Message handler - receives { user, text } from client
14
+ * Broadcasts to all OTHER clients, returns to sender
15
+ */
16
+ module.exports = function message(data) {
17
+ const { user, text } = data
18
+
19
+ if (!user || !text) {
20
+ throw new Error('Missing user or text')
21
+ }
22
+
23
+ const msg = {
24
+ user,
25
+ text,
26
+ time: new Date().toISOString()
27
+ }
28
+
29
+ // Store message
30
+ messages.push(msg)
31
+ if (messages.length > MAX_MESSAGES) {
32
+ messages.shift()
33
+ }
34
+
35
+ // Broadcast to all OTHER clients (exclude sender)
36
+ // this.broadcastOthers is provided by api-ape!
37
+ this.broadcastOthers('message', { message: msg })
38
+
39
+ // Return to sender (fulfills promise)
40
+ return { ok: true, message: msg }
41
+ }
42
+
43
+ // Export history for other uses
44
+ module.exports.getHistory = () => messages.slice(-50)