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.
- package/README.md +261 -0
- package/client/README.md +69 -0
- package/client/browser.js +17 -0
- package/client/connectSocket.js +260 -0
- package/dist/ape.js +454 -0
- package/example/ExpressJs/README.md +97 -0
- package/example/ExpressJs/api/message.js +11 -0
- package/example/ExpressJs/backend.js +37 -0
- package/example/ExpressJs/index.html +88 -0
- package/example/ExpressJs/package-lock.json +834 -0
- package/example/ExpressJs/package.json +10 -0
- package/example/ExpressJs/styles.css +128 -0
- package/example/NextJs/.dockerignore +29 -0
- package/example/NextJs/Dockerfile +52 -0
- package/example/NextJs/Dockerfile.dev +27 -0
- package/example/NextJs/README.md +113 -0
- package/example/NextJs/ape/client.js +66 -0
- package/example/NextJs/ape/embed.js +12 -0
- package/example/NextJs/ape/index.js +23 -0
- package/example/NextJs/ape/logic/chat.js +62 -0
- package/example/NextJs/ape/onConnect.js +69 -0
- package/example/NextJs/ape/onDisconnect.js +13 -0
- package/example/NextJs/ape/onError.js +9 -0
- package/example/NextJs/ape/onReceive.js +15 -0
- package/example/NextJs/ape/onSend.js +15 -0
- package/example/NextJs/api/message.js +44 -0
- package/example/NextJs/docker-compose.yml +22 -0
- package/example/NextJs/next-env.d.ts +5 -0
- package/example/NextJs/next.config.js +8 -0
- package/example/NextJs/package-lock.json +5107 -0
- package/example/NextJs/package.json +25 -0
- package/example/NextJs/pages/_app.tsx +6 -0
- package/example/NextJs/pages/index.tsx +182 -0
- package/example/NextJs/public/favicon.ico +0 -0
- package/example/NextJs/public/vercel.svg +4 -0
- package/example/NextJs/server.js +40 -0
- package/example/NextJs/styles/Chat.module.css +194 -0
- package/example/NextJs/styles/Home.module.css +129 -0
- package/example/NextJs/styles/globals.css +26 -0
- package/example/NextJs/tsconfig.json +20 -0
- package/example/README.md +66 -0
- package/index.d.ts +179 -0
- package/index.js +11 -0
- package/package.json +11 -4
- package/server/README.md +93 -0
- package/server/index.js +6 -0
- package/server/lib/broadcast.js +63 -0
- package/server/lib/loader.js +10 -0
- package/server/lib/main.js +23 -0
- package/server/lib/wiring.js +94 -0
- package/server/security/extractRootDomain.js +21 -0
- package/server/security/origin.js +13 -0
- package/server/security/reply.js +21 -0
- package/server/socket/open.js +10 -0
- package/server/socket/receive.js +66 -0
- package/server/socket/send.js +55 -0
- package/server/utils/deepRequire.js +45 -0
- package/server/utils/genId.js +24 -0
- package/todo.md +85 -0
- package/utils/jss.js +273 -0
- package/utils/jss.test.js +261 -0
- package/utils/messageHash.js +43 -0
- package/utils/messageHash.test.js +56 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
const jss = require('./jss')
|
|
2
|
+
|
|
3
|
+
describe('JJS - JSON SuperSet', () => {
|
|
4
|
+
|
|
5
|
+
describe('Primitives', () => {
|
|
6
|
+
test('handles strings', () => {
|
|
7
|
+
const input = { str: 'hello world' }
|
|
8
|
+
const result = jss.parse(jss.stringify(input))
|
|
9
|
+
expect(result.str).toBe('hello world')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('handles numbers', () => {
|
|
13
|
+
const input = { int: 42, float: 3.14, neg: -100 }
|
|
14
|
+
const result = jss.parse(jss.stringify(input))
|
|
15
|
+
expect(result.int).toBe(42)
|
|
16
|
+
expect(result.float).toBe(3.14)
|
|
17
|
+
expect(result.neg).toBe(-100)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('handles booleans', () => {
|
|
21
|
+
const input = { t: true, f: false }
|
|
22
|
+
const result = jss.parse(jss.stringify(input))
|
|
23
|
+
expect(result.t).toBe(true)
|
|
24
|
+
expect(result.f).toBe(false)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('handles null', () => {
|
|
28
|
+
const input = { n: null }
|
|
29
|
+
const result = jss.parse(jss.stringify(input))
|
|
30
|
+
expect(result.n).toBe(null)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('Special Types', () => {
|
|
35
|
+
test('preserves Date objects', () => {
|
|
36
|
+
const date = new Date('2025-01-01T12:00:00Z')
|
|
37
|
+
const input = { created: date }
|
|
38
|
+
const result = jss.parse(jss.stringify(input))
|
|
39
|
+
expect(result.created).toBeInstanceOf(Date)
|
|
40
|
+
expect(result.created.getTime()).toBe(date.getTime())
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('encodes RegExp objects', () => {
|
|
44
|
+
const regex = /hello/
|
|
45
|
+
const input = { pattern: regex }
|
|
46
|
+
const result = jss.parse(jss.stringify(input))
|
|
47
|
+
// RegExp is encoded/decoded - verify it's a RegExp
|
|
48
|
+
expect(result.pattern).toBeInstanceOf(RegExp)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('preserves Error objects', () => {
|
|
52
|
+
const error = new Error('Something went wrong')
|
|
53
|
+
error.name = 'CustomError'
|
|
54
|
+
const input = { err: error }
|
|
55
|
+
const result = jss.parse(jss.stringify(input))
|
|
56
|
+
expect(result.err).toBeInstanceOf(Error)
|
|
57
|
+
expect(result.err.message).toBe('Something went wrong')
|
|
58
|
+
expect(result.err.name).toBe('CustomError')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('handles undefined in objects', () => {
|
|
62
|
+
const input = { defined: 'yes', notDefined: undefined }
|
|
63
|
+
const result = jss.parse(jss.stringify(input))
|
|
64
|
+
// undefined properties should round-trip
|
|
65
|
+
expect(result.notDefined).toBe(undefined)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('preserves Set objects', () => {
|
|
69
|
+
const set = new Set([1, 2, 3, 'a', 'b'])
|
|
70
|
+
const input = { items: set }
|
|
71
|
+
const result = jss.parse(jss.stringify(input))
|
|
72
|
+
expect(result.items).toBeInstanceOf(Set)
|
|
73
|
+
expect(result.items.has(1)).toBe(true)
|
|
74
|
+
expect(result.items.has('a')).toBe(true)
|
|
75
|
+
expect(result.items.size).toBe(5)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('preserves Map objects', () => {
|
|
79
|
+
const map = new Map([['key1', 'value1'], ['key2', 42]])
|
|
80
|
+
const input = { data: map }
|
|
81
|
+
const result = jss.parse(jss.stringify(input))
|
|
82
|
+
expect(result.data).toBeInstanceOf(Map)
|
|
83
|
+
expect(result.data.get('key1')).toBe('value1')
|
|
84
|
+
expect(result.data.get('key2')).toBe(42)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('Objects and Arrays', () => {
|
|
89
|
+
test('handles nested objects', () => {
|
|
90
|
+
const input = {
|
|
91
|
+
user: {
|
|
92
|
+
name: 'Alice',
|
|
93
|
+
profile: {
|
|
94
|
+
age: 30
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const result = jss.parse(jss.stringify(input))
|
|
99
|
+
expect(result.user.name).toBe('Alice')
|
|
100
|
+
expect(result.user.profile.age).toBe(30)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('handles arrays', () => {
|
|
104
|
+
const input = { items: [1, 2, 3, 'four', 'five'] }
|
|
105
|
+
const result = jss.parse(jss.stringify(input))
|
|
106
|
+
expect(result.items).toEqual([1, 2, 3, 'four', 'five'])
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('handles arrays with Dates', () => {
|
|
110
|
+
const date = new Date('2025-06-15')
|
|
111
|
+
const input = { mixed: ['text', 42, date, null] }
|
|
112
|
+
const result = jss.parse(jss.stringify(input))
|
|
113
|
+
expect(result.mixed[0]).toBe('text')
|
|
114
|
+
expect(result.mixed[1]).toBe(42)
|
|
115
|
+
expect(result.mixed[2]).toBeInstanceOf(Date)
|
|
116
|
+
expect(result.mixed[3]).toBe(null)
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('encode/decode', () => {
|
|
121
|
+
test('encode returns tagged object for Date', () => {
|
|
122
|
+
const input = { d: new Date('2025-01-01') }
|
|
123
|
+
const encoded = jss.encode(input)
|
|
124
|
+
expect(encoded['d<!D>']).toBeDefined()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('decode restores Date from tagged object', () => {
|
|
128
|
+
const encoded = { 'd<!D>': 1735689600000 }
|
|
129
|
+
const decoded = jss.decode(encoded)
|
|
130
|
+
expect(decoded.d).toBeInstanceOf(Date)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('encode handles Error type', () => {
|
|
134
|
+
const input = { e: new Error('test') }
|
|
135
|
+
const encoded = jss.encode(input)
|
|
136
|
+
expect(encoded['e<!E>']).toBeDefined()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('encode handles Set type', () => {
|
|
140
|
+
const input = { s: new Set([1, 2]) }
|
|
141
|
+
const encoded = jss.encode(input)
|
|
142
|
+
expect(encoded['s<!S>']).toBeDefined()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('encode handles Map type', () => {
|
|
146
|
+
const input = { m: new Map([['a', 1]]) }
|
|
147
|
+
const encoded = jss.encode(input)
|
|
148
|
+
expect(encoded['m<!M>']).toBeDefined()
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('Circular References', () => {
|
|
153
|
+
test('handles self-referencing object', () => {
|
|
154
|
+
const original = {
|
|
155
|
+
id: 123,
|
|
156
|
+
name: 'Test'
|
|
157
|
+
}
|
|
158
|
+
original.foo = original
|
|
159
|
+
|
|
160
|
+
const encoded = jss.encode(original)
|
|
161
|
+
const result = jss.decode(encoded)
|
|
162
|
+
|
|
163
|
+
expect(result.id).toBe(123)
|
|
164
|
+
expect(result.name).toBe('Test')
|
|
165
|
+
expect(result.foo).toBe(result)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('handles self-referencing object', () => {
|
|
169
|
+
const original = {
|
|
170
|
+
name: 'Test',
|
|
171
|
+
cat: {
|
|
172
|
+
cars: true
|
|
173
|
+
},
|
|
174
|
+
bar: {
|
|
175
|
+
baz: true
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
original.cat.foo = original.bar.baz
|
|
179
|
+
|
|
180
|
+
const encoded = jss.encode(original)
|
|
181
|
+
const result = jss.decode(encoded)
|
|
182
|
+
|
|
183
|
+
expect(result.cat.foo).toBe(result.bar.baz)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('handles multiple self-references', () => {
|
|
187
|
+
const original = { id: 1 }
|
|
188
|
+
original.refA = original
|
|
189
|
+
original.refB = original
|
|
190
|
+
|
|
191
|
+
const result = jss.decode(jss.encode(original))
|
|
192
|
+
|
|
193
|
+
expect(result.refA).toBe(result)
|
|
194
|
+
expect(result.refB).toBe(result)
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
describe('Shared References', () => {
|
|
199
|
+
test('shared object referenced twice', () => {
|
|
200
|
+
const shared = { value: 42 }
|
|
201
|
+
const original = {
|
|
202
|
+
first: shared,
|
|
203
|
+
second: shared
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const result = jss.decode(jss.encode(original))
|
|
207
|
+
|
|
208
|
+
expect(result.first.value).toBe(42)
|
|
209
|
+
expect(result.second.value).toBe(42)
|
|
210
|
+
expect(result.first).toBe(result.second) // same object reference
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('shared object in array', () => {
|
|
214
|
+
const shared = { id: 'shared' }
|
|
215
|
+
const original = {
|
|
216
|
+
items: [shared, shared, shared]
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const result = jss.decode(jss.encode(original))
|
|
220
|
+
|
|
221
|
+
expect(result.items[0]).toBe(result.items[1])
|
|
222
|
+
expect(result.items[1]).toBe(result.items[2])
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('deeply nested shared reference', () => {
|
|
226
|
+
const shared = { data: 'test' }
|
|
227
|
+
const original = {
|
|
228
|
+
level1: {
|
|
229
|
+
level2: {
|
|
230
|
+
ref: shared
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
otherRef: shared
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const result = jss.decode(jss.encode(original))
|
|
237
|
+
|
|
238
|
+
expect(result.level1.level2.ref.data).toBe('test')
|
|
239
|
+
expect(result.level1.level2.ref).toBe(result.otherRef)
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
describe('Round-trip', () => {
|
|
244
|
+
test('object with multiple special types survives round-trip', () => {
|
|
245
|
+
const original = {
|
|
246
|
+
id: 123,
|
|
247
|
+
name: 'Test',
|
|
248
|
+
createdAt: new Date(),
|
|
249
|
+
tags: new Set(['a', 'b']),
|
|
250
|
+
meta: new Map([['x', 1]])
|
|
251
|
+
}
|
|
252
|
+
const result = jss.parse(jss.stringify(original))
|
|
253
|
+
|
|
254
|
+
expect(result.id).toBe(original.id)
|
|
255
|
+
expect(result.name).toBe(original.name)
|
|
256
|
+
expect(result.createdAt.getTime()).toBe(original.createdAt.getTime())
|
|
257
|
+
expect(result.tags).toBeInstanceOf(Set)
|
|
258
|
+
expect(result.meta).toBeInstanceOf(Map)
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
2
|
+
/*
|
|
3
|
+
function charValue(char){
|
|
4
|
+
return alphabet.indexOf(char.toUpperCase())
|
|
5
|
+
} // END charValue
|
|
6
|
+
|
|
7
|
+
function fromBase32(b32){
|
|
8
|
+
if (0 === b32.length) {
|
|
9
|
+
return 0
|
|
10
|
+
}
|
|
11
|
+
return charValue(b32.slice(-1)) + fromBase32(b32.slice(0,-1)) * 32
|
|
12
|
+
} // END fromBase32
|
|
13
|
+
*/
|
|
14
|
+
function toBase32 (n){
|
|
15
|
+
const remainder = Math.floor(n/32)
|
|
16
|
+
const current = n % 32
|
|
17
|
+
if (0 === remainder) {
|
|
18
|
+
return alphabet[current]
|
|
19
|
+
}
|
|
20
|
+
return toBase32(remainder)+alphabet[current]
|
|
21
|
+
} // END toBase32
|
|
22
|
+
|
|
23
|
+
function jenkinsOneAtATimeHash(keyString){
|
|
24
|
+
|
|
25
|
+
var hash = 0
|
|
26
|
+
|
|
27
|
+
for (var charIndex = 0; charIndex < keyString.length; ++charIndex)
|
|
28
|
+
{
|
|
29
|
+
hash += keyString.charCodeAt(charIndex);
|
|
30
|
+
hash += hash << 10;
|
|
31
|
+
hash ^= hash >> 6;
|
|
32
|
+
}
|
|
33
|
+
hash += hash << 3;
|
|
34
|
+
hash ^= hash >> 11;
|
|
35
|
+
//4,294,967,295 is FFFFFFFF, the maximum 32 bit unsigned integer value, used here as a mask.
|
|
36
|
+
return (((hash + (hash << 15)) & 4294967295) >>> 0)
|
|
37
|
+
} // END jenkinsOneAtATimeHash
|
|
38
|
+
|
|
39
|
+
function messageHash(messageSt){
|
|
40
|
+
return toBase32(jenkinsOneAtATimeHash(messageSt))
|
|
41
|
+
} // END messageHash
|
|
42
|
+
|
|
43
|
+
module.exports = messageHash
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const messageHash = require('./messageHash')
|
|
2
|
+
|
|
3
|
+
describe('messageHash', () => {
|
|
4
|
+
|
|
5
|
+
test('returns consistent hash for same input', () => {
|
|
6
|
+
const input = 'hello world'
|
|
7
|
+
const hash1 = messageHash(input)
|
|
8
|
+
const hash2 = messageHash(input)
|
|
9
|
+
expect(hash1).toBe(hash2)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('returns different hashes for different inputs', () => {
|
|
13
|
+
const hash1 = messageHash('message one')
|
|
14
|
+
const hash2 = messageHash('message two')
|
|
15
|
+
expect(hash1).not.toBe(hash2)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('returns base32-encoded string', () => {
|
|
19
|
+
const hash = messageHash('test message')
|
|
20
|
+
// Base32 alphabet used: 0-9, A-Z excluding I, L, O, U
|
|
21
|
+
expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]+$/)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('handles empty string', () => {
|
|
25
|
+
const hash = messageHash('')
|
|
26
|
+
expect(typeof hash).toBe('string')
|
|
27
|
+
expect(hash.length).toBeGreaterThan(0)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('handles special characters', () => {
|
|
31
|
+
const hash = messageHash('特殊文字 🎉 emoji!')
|
|
32
|
+
expect(typeof hash).toBe('string')
|
|
33
|
+
expect(hash.length).toBeGreaterThan(0)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('handles long strings', () => {
|
|
37
|
+
const longString = 'x'.repeat(10000)
|
|
38
|
+
const hash = messageHash(longString)
|
|
39
|
+
expect(typeof hash).toBe('string')
|
|
40
|
+
// Hash should be compact regardless of input length
|
|
41
|
+
expect(hash.length).toBeLessThan(20)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('handles JSON strings', () => {
|
|
45
|
+
const json = JSON.stringify({ type: '/message', data: { user: 'test' } })
|
|
46
|
+
const hash = messageHash(json)
|
|
47
|
+
expect(typeof hash).toBe('string')
|
|
48
|
+
expect(hash.length).toBeGreaterThan(0)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('different order produces different hash', () => {
|
|
52
|
+
const hash1 = messageHash('abc')
|
|
53
|
+
const hash2 = messageHash('bca')
|
|
54
|
+
expect(hash1).not.toBe(hash2)
|
|
55
|
+
})
|
|
56
|
+
})
|