Compare commits

..

29 Commits

Author SHA1 Message Date
002f2c47c6 v0.27.6 2023-07-19 18:22:35 +08:00
8633d9ea95 fix: particle spawner error 2023-07-19 18:21:56 +08:00
07a5d0c8de feat: add plugin checker
Signed-off-by: MiaoWoo <admin@yumc.pw>
2023-02-18 16:01:15 +08:00
dd76e563c8 feat: add qrcode package
Signed-off-by: MiaoWoo <admin@yumc.pw>
2023-02-18 15:21:42 +08:00
7b85ff5b7c feat: 同步 socket.io 上游代码
Signed-off-by: MiaoWoo <admin@yumc.pw>
2023-02-09 13:49:48 +08:00
359aeb9d63 chore: remove client package
Signed-off-by: MiaoWoo <admin@yumc.pw>
2023-02-09 12:44:04 +08:00
3901d9fb5f feat: 更新依赖版本
Signed-off-by: MiaoWoo <admin@yumc.pw>
2023-02-08 17:03:17 +08:00
c2867da047 v0.26.0 2023-02-08 16:24:09 +08:00
65832c9fae feat: 完善粒子播放
Signed-off-by: MiaoWoo <admin@yumc.pw>
2023-02-08 16:23:05 +08:00
d9dffa704d v2.25.1 2022-11-28 17:30:45 +08:00
b39f29de6a v2.25.1-beta.1 2022-11-28 17:18:50 +08:00
56334c6f6e v0.25.1-beta.0 2022-11-28 17:15:39 +08:00
72673b2a67 feat: optimize event slow exec cache
Signed-off-by: MiaoWoo <admin@yumc.pw>
2022-11-28 17:14:29 +08:00
597bdb721a v0.25.0 2022-11-22 17:37:54 +08:00
6e0456d777 feat: 添加注释
Signed-off-by: MiaoWoo <admin@yumc.pw>
2022-11-22 17:35:13 +08:00
fb67b06230 v0.25.0-beta.0 2022-11-22 17:12:22 +08:00
203560dcf2 chore: add --verify-access
Signed-off-by: MiaoWoo <admin@yumc.pw>
2022-11-22 17:10:15 +08:00
3f58f5992c style: xml-http-request.ts
Signed-off-by: MiaoWoo <admin@yumc.pw>
2022-11-22 15:50:54 +08:00
ac16754c9c fix: buffer polyfill error
Signed-off-by: MiaoWoo <admin@yumc.pw>
2022-11-22 13:48:41 +08:00
42d637dd63 fix: rename Buffer to BufferPolyfill
Signed-off-by: MiaoWoo <admin@yumc.pw>
2022-11-21 23:27:12 +08:00
df0d246136 feat: upgrade socket.io to v4
Signed-off-by: MiaoWoo <admin@yumc.pw>
2022-11-21 23:18:39 +08:00
e563e1b507 feat: update chat to 1.19.2
Signed-off-by: MiaoWoo <admin@yumc.pw>
2022-11-21 23:17:14 +08:00
3b822c613a dep: update dependencies
Signed-off-by: MiaoWoo <admin@yumc.pw>
2022-11-21 14:29:17 +08:00
2967c2a1fe v0.24.3 2022-10-14 14:12:02 +08:00
1c579c9789 feat: framework optimization
Signed-off-by: MiaoWoo <admin@yumc.pw>
2022-10-14 10:07:36 +08:00
2fe9bce2ea v0.24.2 2022-07-17 07:13:54 +08:00
496d278a93 fix: item fromJson error
Signed-off-by: MiaoWoo <admin@yumc.pw>
2022-07-17 07:11:36 +08:00
bdf674b678 v0.24.1 2022-07-07 00:54:16 +08:00
67fe13deac fix: migrate config error
Signed-off-by: MiaoWoo <admin@yumc.pw>
2022-07-07 00:51:10 +08:00
104 changed files with 12890 additions and 5893 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.yarn*
.vscode .vscode
.theia .theia
node_modules node_modules

View File

@@ -1,16 +1,16 @@
{ {
"version": "0.24.0", "version": "0.27.6",
"useWorkspaces": true, "useWorkspaces": true,
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"
], ],
"command": { "command": {
"run": { "run": {
"stream": true "stream": true
}
},
"publishConfig": {
"access": "public"
} }
},
"publishConfig": {
"access": "public"
}
} }

View File

@@ -12,14 +12,16 @@
"build": "lerna run build", "build": "lerna run build",
"ug": "yarn upgrade-interactive --latest", "ug": "yarn upgrade-interactive --latest",
"np": "./script/push.sh", "np": "./script/push.sh",
"lsp": "npm login --registry=https://registry.npmjs.org --scope=@ccms", "lsp": "npm login -scope=@ccms",
"lp": "lerna publish --registry https://registry.npmjs.org", "lp": "lerna publish --verify-access --force-publish",
"lpb": "lerna publish --registry https://registry.npmjs.org --canary --preid beta --pre-dist-tag beta" "lpb": "lerna publish --preid beta --dist-tag beta --verify-access --force-publish",
"lpc": "lerna publish --canary --preid beta --pre-dist-tag beta --verify-access --force-publish",
"lpf": "lerna publish from-package --yes"
}, },
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
], ],
"devDependencies": { "devDependencies": {
"lerna": "^4.0.0" "lerna": "^6.4.1"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/amqp", "name": "@ccms/amqp",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript amqp package", "description": "MiaoScript amqp package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -19,17 +19,17 @@
"test": "echo \"Error: run tests from root\" && exit 1" "test": "echo \"Error: run tests from root\" && exit 1"
}, },
"dependencies": { "dependencies": {
"@ccms/api": "^0.24.0", "@ccms/api": "^0.27.6",
"@ccms/common": "^0.24.0", "@ccms/common": "^0.27.6",
"@ccms/container": "^0.24.0" "@ccms/container": "^0.27.6"
}, },
"devDependencies": { "devDependencies": {
"@ccms/nashorn": "^0.24.0", "@ccms/nashorn": "^0.27.6",
"@javatypes/amqp-client": "^0.0.3", "@javatypes/amqp-client": "^0.0.3",
"@javatypes/spring-amqp": "^0.0.3", "@javatypes/spring-amqp": "^0.0.3",
"@javatypes/spring-rabbit": "^0.0.3", "@javatypes/spring-rabbit": "^0.0.3",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/api", "name": "@ccms/api",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript api package", "description": "MiaoScript api package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -19,16 +19,16 @@
"test": "echo \"Error: run tests from root\" && exit 1" "test": "echo \"Error: run tests from root\" && exit 1"
}, },
"dependencies": { "dependencies": {
"@ccms/common": "^0.24.0", "@ccms/common": "^0.27.6",
"@ccms/container": "^0.24.0", "@ccms/container": "^0.27.6",
"@ccms/polyfill": "^0.24.0", "@ccms/polyfill": "^0.27.6",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"source-map-builder": "^0.0.7" "source-map-builder": "^0.0.7"
}, },
"devDependencies": { "devDependencies": {
"@types/base64-js": "^1.3.0", "@types/base64-js": "^1.3.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
} }
} }

View File

@@ -44,13 +44,13 @@ export namespace jsconsole {
let file = Paths.get(Paths.get(fileName, '..', sourceMappingURL).toFile().getCanonicalPath()).toFile() let file = Paths.get(Paths.get(fileName, '..', sourceMappingURL).toFile().getCanonicalPath()).toFile()
if (file.exists()) { if (file.exists()) {
sourceContent = base.read(file) sourceContent = base.read(file)
sourceFileMaps[fileName] = file.getCanonicalPath()
} else if (global.debug) { } else if (global.debug) {
console.debug('readSourceMap can\'t found', fileName, 'source map file', sourceMappingURL) console.debug('readSourceMap can\'t found', fileName, 'source map file', sourceMappingURL)
} }
} }
if (sourceContent) { if (sourceContent) {
sourceMaps[fileName] = new SourceMapBuilder(JSON.parse(sourceContent)) sourceMaps[fileName] = new SourceMapBuilder(JSON.parse(sourceContent))
sourceFileMaps[fileName] = Paths.get(fileName, '..', sourceMaps[fileName].sources[0]).toFile().getCanonicalPath()
} }
} }
} }
@@ -88,9 +88,9 @@ export namespace jsconsole {
let { fileName, lineNumber } = readSourceMap(trace.fileName, trace.lineNumber) let { fileName, lineNumber } = readSourceMap(trace.fileName, trace.lineNumber)
if (fileName.startsWith(root)) { fileName = fileName.split(root)[1] } if (fileName.startsWith(root)) { fileName = fileName.split(root)[1] }
if (color) { if (color) {
cache.push(` §e->§c ${fileName}:${lineNumber} => §4${trace.methodName}`) cache.push(` §e->§c ${fileName}:${lineNumber}(${trace.lineNumber}) => §4${trace.methodName}`)
} else { } else {
cache.push(` -> ${fileName}:${lineNumber} => ${trace.methodName}`) cache.push(` -> ${fileName}:${lineNumber}(${trace.lineNumber}) => ${trace.methodName}`)
} }
} else { } else {
let className = trace.className let className = trace.className

View File

@@ -30,6 +30,10 @@ export namespace database {
* 链接属性 * 链接属性
*/ */
properties?: { [key: string]: any } properties?: { [key: string]: any }
/**
* 调试模式
*/
debug?: boolean
} }
@injectable() @injectable()

View File

@@ -26,6 +26,7 @@ export namespace event {
private mapEvent = []; private mapEvent = [];
private listenerMap = []; private listenerMap = [];
private cacheSlowEventKey = {};
protected baseEventDir = ''; protected baseEventDir = '';
@@ -92,17 +93,23 @@ export namespace event {
return eventCls return eventCls
} }
execute(name, exec, eventCls) { /**
* 创建命令执行器
* @param name 插件名称
* @param exec 执行方法
* @param eventCls 事件类
* @returns
*/
createExecute(name, exec, eventCls) {
return (...args: any[]) => { return (...args: any[]) => {
let event = args[args.length - 1]
try { try {
let event = args[args.length - 1] if (!eventCls.isAssignableFrom(event.getClass())) { return }
if (eventCls.isAssignableFrom(event.getClass())) { let time = Date.now(); exec(event); let cost = Date.now() - time
let time = Date.now() if (cost > global.ScriptSlowExecuteTime && !event.async) {
exec(event) let eventKey = `${name}-${this.class2Name(eventCls)}`
let cost = Date.now() - time if (!this.cacheSlowEventKey[eventKey]) { return this.cacheSlowEventKey[eventKey] = cost }
if (cost > global.ScriptSlowExecuteTime) { console.i18n("ms.api.event.execute.slow", { name, event: this.class2Name(eventCls), cost })
console.i18n("ms.api.event.execute.slow", { name, event: this.class2Name(eventCls), cost })
}
} }
} catch (ex: any) { } catch (ex: any) {
console.i18n("ms.api.event.execute.error", { name, event: this.class2Name(eventCls), ex }) console.i18n("ms.api.event.execute.error", { name, event: this.class2Name(eventCls), ex })
@@ -113,11 +120,11 @@ export namespace event {
/** /**
* 添加事件监听 * 添加事件监听
* @param plugin {any} * @param plugin {any} 插件
* @param event {string} * @param event {string} 事件名称
* @param exec {function} * @param exec {function} 事件执行器
* @param priority {string} [LOWEST,LOW,NORMAL,HIGH,HIGHEST,MONITOR] * @param priority {string} [LOWEST,LOW,NORMAL,HIGH,HIGHEST,MONITOR] 优先级
* @param ignoreCancel * @param ignoreCancel 是否忽略已取消事件
*/ */
listen(plugin: any, event: string, exec: (event: any) => void, priority: EventPriority = EventPriority.NORMAL, ignoreCancel = false) { listen(plugin: any, event: string, exec: (event: any) => void, priority: EventPriority = EventPriority.NORMAL, ignoreCancel = false) {
if (!plugin || !plugin.description || !plugin.description.name) throw new TypeError(i18n.translate("ms.api.event.listen.plugin.name.empty")) if (!plugin || !plugin.description || !plugin.description.name) throw new TypeError(i18n.translate("ms.api.event.listen.plugin.name.empty"))
@@ -135,7 +142,7 @@ export namespace event {
// noinspection JSUnusedGlobalSymbols // noinspection JSUnusedGlobalSymbols
var listener = this.register( var listener = this.register(
eventCls, eventCls,
this.execute(name, exec, eventCls), this.createExecute(name, exec, eventCls),
priority, priority,
ignoreCancel ignoreCancel
) )
@@ -171,7 +178,7 @@ export namespace event {
disable(plugin: any) { disable(plugin: any) {
var eventCache = this.listenerMap[plugin.description.name] var eventCache = this.listenerMap[plugin.description.name]
if (eventCache) { if (eventCache) {
eventCache.forEach(off => off()) eventCache.forEach((off: () => any) => off())
delete this.listenerMap[plugin.description.name] delete this.listenerMap[plugin.description.name]
} }
} }

View File

@@ -5,7 +5,7 @@ export namespace item {
export abstract class Item { export abstract class Item {
abstract builder(): ItemBuilder abstract builder(): ItemBuilder
abstract toJson(item: any): string abstract toJson(item: any): string
abstract fromJSON(json: string): any abstract fromJson(json: string): any
} }
export interface ItemBuilder { export interface ItemBuilder {
from(item: any): ItemBuilder from(item: any): ItemBuilder

View File

@@ -6,6 +6,11 @@ const UUID = Java.type('java.util.UUID')
const Math = Java.type('java.lang.Math') const Math = Java.type('java.lang.Math')
export namespace particle { export namespace particle {
@injectable()
export abstract class ParticleSpawner {
abstract spawn(location: any, particle: Particle)
abstract spawnToPlayer(player: any, location: any, particle: Particle)
}
/** /**
* 表示一个特效对象 * 表示一个特效对象
* *
@@ -23,6 +28,11 @@ export namespace particle {
private extra: number = 0; private extra: number = 0;
private data: Object = null; private data: Object = null;
/**
* Only Show To Player
*/
private player: any
constructor() { constructor() {
this.uuid = UUID.randomUUID().toString() this.uuid = UUID.randomUUID().toString()
} }
@@ -105,6 +115,15 @@ export namespace particle {
return this return this
} }
getPlayer() {
return this.player
}
setPlayer(player) {
this.player = player
return this
}
/** /**
* 通过给定一个坐标就可以使用已经指定的参数来播放粒子 * 通过给定一个坐标就可以使用已经指定的参数来播放粒子
* *
@@ -112,9 +131,14 @@ export namespace particle {
*/ */
spawn(location: any) { spawn(location: any) {
if (!this.spawner) throw new Error(`particle ${this.uuid} not set spawner can't spawn!`) if (!this.spawner) throw new Error(`particle ${this.uuid} not set spawner can't spawn!`)
this.spawner.spawn(location, this) if (this.player) {
this.spawner.spawnToPlayer(this.player, location, this)
} else {
this.spawner.spawn(location, this)
}
} }
} }
/** /**
* 表示一条线 * 表示一条线
* *
@@ -158,8 +182,7 @@ export namespace particle {
show() { show() {
for (let i = 0; i < this.length; i += this.step) { for (let i = 0; i < this.length; i += this.step) {
let vectorTemp = this.vector.clone().multiply(i) this.spawn(this.start.clone().add(this.vector.clone().multiply(i)))
this.spawn(this.start.clone().add(vectorTemp))
} }
} }
@@ -234,15 +257,6 @@ export namespace particle {
this.length = this.vector.length() this.length = this.vector.length()
this.vector.normalize() this.vector.normalize()
} }
public static buildLine(locA: any, locB: any, step: number, particle: any) {
let vectorAB = locB.clone().subtract(locA).toVector()
let vectorLength = vectorAB.length()
vectorAB.normalize()
for (let i = 0; i < vectorLength; i += step) {
ParticleManager.globalSpawner.spawn(locA.clone().add(vectorAB.clone().multiply(i)), particle)
}
}
} }
/** /**
* 表示一个弧 * 表示一个弧
@@ -331,9 +345,10 @@ export namespace particle {
@injectable() @injectable()
export abstract class ParticleManager { export abstract class ParticleManager {
public static globalSpawner: ParticleSpawner = undefined
@Autowired() @Autowired()
private taskManager: task.TaskManager private taskManager: task.TaskManager
@Autowired()
private particleSpawner: particle.ParticleSpawner
protected taskId: java.util.concurrent.atomic.AtomicInteger protected taskId: java.util.concurrent.atomic.AtomicInteger
protected cacheTasks = new Map<string, ParticleTask>() protected cacheTasks = new Map<string, ParticleTask>()
@@ -354,6 +369,10 @@ export namespace particle {
return this.taskManager return this.taskManager
} }
public getParticleSpawner() {
return this.particleSpawner
}
public create(particle: Particle, plugin?: plugin.Plugin) { public create(particle: Particle, plugin?: plugin.Plugin) {
let uuid = particle.getUUID() let uuid = particle.getUUID()
if (this.cacheTasks.has(uuid)) { if (this.cacheTasks.has(uuid)) {
@@ -389,14 +408,12 @@ export namespace particle {
} }
} }
protected create0(owner: plugin.Plugin, particle: Particle): ParticleTask { protected create0(owner: plugin.Plugin, particle: Particle): ParticleTask {
particle.setSpawner(this.getGlobalSpawner()) particle.setSpawner(this.getParticleSpawner())
return new ParticleTask(owner, particle, this) return new ParticleTask(owner, particle, this)
} }
protected abstract getGlobalSpawner(): ParticleSpawner
} }
export class ParticleTask { export class ParticleTask {
private particle: Particle private particle: Particle
private isAsync: boolean = false private isAsync: boolean = false
private interval: number = 0 private interval: number = 0
@@ -486,9 +503,4 @@ export namespace particle {
} }
} }
} }
export abstract class ParticleSpawner {
abstract spawnParticle(location: any, particle: any, count: number)
abstract spawn(location: any, particle: Particle)
}
} }

View File

@@ -29,6 +29,7 @@ export namespace server {
origin: any origin: any
[key: string]: any [key: string]: any
} }
@injectable() @injectable()
export abstract class NativePluginManager { export abstract class NativePluginManager {
list(): NativePlugin[] { list(): NativePlugin[] {
@@ -53,6 +54,7 @@ export namespace server {
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
} }
/** /**
* MiaoScript Server * MiaoScript Server
*/ */
@@ -98,6 +100,7 @@ export namespace server {
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
} }
@injectable() @injectable()
export class ServerChecker { export class ServerChecker {
@Autowired(ServerType) @Autowired(ServerType)
@@ -116,6 +119,22 @@ export namespace server {
} }
} }
} }
@injectable()
export class NativePluginChecker {
@Autowired(NativePluginManager)
private nativePluginManager: NativePluginManager
check(plugins: string[]) {
// Not set plugins -> allow
if (!plugins || !plugins.length) return true
for (const plugin of plugins) {
if (!this.nativePluginManager.has(plugin)) { return false }
}
return true
}
}
@injectable() @injectable()
export abstract class ReflectServer extends server.Server { export abstract class ReflectServer extends server.Server {
@Autowired(ContainerInstance) @Autowired(ContainerInstance)
@@ -185,15 +204,25 @@ export namespace server {
} }
} }
} }
if (this.rootLogger.class.name.indexOf('slf4j') !== -1) {
try {
let LogManager = Java.type('org.apache.logging.log4j.LogManager')
this.rootLogger = LogManager.getLogger('ROOT')
} catch (error: any) {
if (global.debug) {
console.ex(error)
}
}
}
if (this.rootLogger && this.rootLogger.class.name.indexOf('Logger') === -1) { if (this.rootLogger && this.rootLogger.class.name.indexOf('Logger') === -1) {
console.error('Error Logger Class: ' + this.rootLogger.class.name) console.error('Error Logger Class: ' + this.rootLogger.class.name)
this.rootLogger = undefined this.rootLogger = undefined
} }
if (!this.rootLogger) { console.error("Can't found rootLogger!") }
// get root logger // get root logger
for (let index = 0; index < 5 && this.rootLogger.parent; index++) { for (let index = 0; index < 5 && this.rootLogger.parent; index++) {
this.rootLogger = this.rootLogger.parent this.rootLogger = this.rootLogger.parent
} }
if (!this.rootLogger) { console.error("Can't found rootLogger!") }
this.container.bind(constants.ServiceIdentifier.RootLogger).toConstantValue(this.rootLogger) this.container.bind(constants.ServiceIdentifier.RootLogger).toConstantValue(this.rootLogger)
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/bukkit", "name": "@ccms/bukkit",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript bukkit package", "description": "MiaoScript bukkit package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -21,12 +21,12 @@
"devDependencies": { "devDependencies": {
"@javatypes/spigot-api": "^0.0.3", "@javatypes/spigot-api": "^0.0.3",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
}, },
"dependencies": { "dependencies": {
"@ccms/api": "^0.24.0", "@ccms/api": "^0.27.6",
"@ccms/common": "^0.24.0", "@ccms/common": "^0.27.6",
"@ccms/container": "^0.24.0" "@ccms/container": "^0.27.6"
} }
} }

View File

@@ -161,16 +161,17 @@ class BukkitChatInvoke_1_17_1 extends BukkitChatInvoke_1_16_5 {
return base.getClass('net.minecraft.network.protocol.Packet') return base.getClass('net.minecraft.network.protocol.Packet')
} }
} }
class BukkitChatInvoke_1_18_2 extends BukkitChatInvoke_1_17_1 {
class BukkitChatInvoke_1_19 extends BukkitChatInvoke_1_17_1 {
getSendPacketMethodName(playerConnectionClass: any) { getSendPacketMethodName(playerConnectionClass: any) {
return playerConnectionClass.getMethod('a', this.getPacketClass()).getName() return playerConnectionClass.getMethod('a', this.getPacketClass()).getName()
} }
}
class BukkitChatInvoke_1_19 extends BukkitChatInvoke_1_18_2 {
getPacketPlayOutChatClass() { getPacketPlayOutChatClass() {
return base.getClass('net.minecraft.network.protocol.game.ClientboundSystemChatPacket') return base.getClass('net.minecraft.network.protocol.game.ClientboundSystemChatPacket')
} }
getPacketPlayOutChat(sender: any, json: any, type: number) { getPacketPlayOutChat(sender: any, json: any, type: number) {
return new this.PacketPlayOutChat(this.ChatSerializer[this.nmsChatSerializerMethodName](json), type == 0 ? 1 : type) return new this.PacketPlayOutChat(this.ChatSerializer[this.nmsChatSerializerMethodName](json), type)
} }
} }
@@ -181,6 +182,8 @@ try {
let nmsSubVersion = nmsVersion.split("_")[1] let nmsSubVersion = nmsVersion.split("_")[1]
if (nmsSubVersion >= 19) { if (nmsSubVersion >= 19) {
bukkitChatInvoke = new BukkitChatInvoke_1_19(nmsVersion) bukkitChatInvoke = new BukkitChatInvoke_1_19(nmsVersion)
} else if (nmsSubVersion >= 18) {
bukkitChatInvoke = new BukkitChatInvoke_1_18_2(nmsVersion)
} else if (nmsSubVersion >= 17) { } else if (nmsSubVersion >= 17) {
bukkitChatInvoke = new BukkitChatInvoke_1_17_1(nmsVersion) bukkitChatInvoke = new BukkitChatInvoke_1_17_1(nmsVersion)
} else if (nmsSubVersion >= 16) { } else if (nmsSubVersion >= 16) {

View File

@@ -12,6 +12,7 @@ export class BukkitItem extends item.Item {
private NBTTagCompound: any private NBTTagCompound: any
private nmsSaveNBTMethodName: any private nmsSaveNBTMethodName: any
private MojangsonParser: any private MojangsonParser: any
private nmsItemStack: any
private mpParseMethodName: any private mpParseMethodName: any
private nmsVersion: any private nmsVersion: any
constructor() { constructor() {
@@ -25,8 +26,8 @@ export class BukkitItem extends item.Item {
let nbt = new this.NBTTagCompound() let nbt = new this.NBTTagCompound()
return this.CraftItemStack.asNMSCopy(item)[this.nmsSaveNBTMethodName](nbt).toString() return this.CraftItemStack.asNMSCopy(item)[this.nmsSaveNBTMethodName](nbt).toString()
} }
fromJSON(json: string) { fromJson(json: string) {
return this.CraftItemStack.asBukkitCopy(this.MojangsonParser[this.mpParseMethodName](json)) return this.CraftItemStack.asBukkitCopy(new this.nmsItemStack(this.MojangsonParser[this.mpParseMethodName](json)))
} }
private obcCls(name: string) { private obcCls(name: string) {
return base.getClass(['org.bukkit.craftbukkit', this.nmsVersion, name].join('.')) return base.getClass(['org.bukkit.craftbukkit', this.nmsVersion, name].join('.'))
@@ -44,6 +45,7 @@ export class BukkitItem extends item.Item {
// @ts-ignore // @ts-ignore
let asNMSCopyMethod = CraftItemStackClass.getMethod('asNMSCopy', ItemStack.class) let asNMSCopyMethod = CraftItemStackClass.getMethod('asNMSCopy', ItemStack.class)
let nmsItemStackClass = asNMSCopyMethod.getReturnType() let nmsItemStackClass = asNMSCopyMethod.getReturnType()
this.nmsItemStack = Java.type(nmsItemStackClass.getName())
let nmsNBTTagCompoundClass = undefined let nmsNBTTagCompoundClass = undefined
for (let method of Java.from(nmsItemStackClass.getMethods())) { for (let method of Java.from(nmsItemStackClass.getMethods())) {
let rt = method.getReturnType() let rt = method.getReturnType()
@@ -61,7 +63,7 @@ export class BukkitItem extends item.Item {
} }
} }
try { try {
this.MojangsonParser = this.nmsCls('MojangsonParser') this.MojangsonParser = this.nmsCls('MojangsonParser').static
} catch (error) { } catch (error) {
this.MojangsonParser = Java.type('net.minecraft.nbt.MojangsonParser') this.MojangsonParser = Java.type('net.minecraft.nbt.MojangsonParser')
} }

View File

@@ -3,20 +3,11 @@ import { particle } from '@ccms/api'
@provideSingleton(particle.ParticleManager) @provideSingleton(particle.ParticleManager)
export class BukkitParticleManager extends particle.ParticleManager { export class BukkitParticleManager extends particle.ParticleManager {
private globalSpawner = new BukkitParticleSpawner()
constructor() {
super()
particle.ParticleManager.globalSpawner = this.globalSpawner
}
protected getGlobalSpawner() {
return this.globalSpawner
}
} }
@provideSingleton(particle.ParticleSpawner)
export class BukkitParticleSpawner extends particle.ParticleSpawner { export class BukkitParticleSpawner extends particle.ParticleSpawner {
spawnParticle(location: any, particle: any, count: number = 1) { spawn(location: org.bukkit.Location, particle: particle.Particle) {
location.getWorld().spawnParticle(particle, location, count)
}
spawn(location: any, particle: particle.Particle) {
location.getWorld().spawnParticle( location.getWorld().spawnParticle(
particle.getParticle(), particle.getParticle(),
location, location,
@@ -28,4 +19,15 @@ export class BukkitParticleSpawner extends particle.ParticleSpawner {
particle.getData() particle.getData()
) )
} }
spawnToPlayer(player: org.bukkit.entity.Player, location: org.bukkit.Location, particle: particle.Particle) {
player.spawnParticle(
particle.getParticle(),
location,
particle.getCount(),
particle.getOffsetX(),
particle.getOffsetY(),
particle.getOffsetZ(),
particle.getExtra(),
particle.getData())
}
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/bungee", "name": "@ccms/bungee",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript bungee package", "description": "MiaoScript bungee package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -21,12 +21,12 @@
"devDependencies": { "devDependencies": {
"@javatypes/bungee-api": "^0.0.3", "@javatypes/bungee-api": "^0.0.3",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
}, },
"dependencies": { "dependencies": {
"@ccms/api": "^0.24.0", "@ccms/api": "^0.27.6",
"@ccms/common": "^0.24.0", "@ccms/common": "^0.27.6",
"@ccms/container": "^0.24.0" "@ccms/container": "^0.27.6"
} }
} }

View File

@@ -1 +0,0 @@
src/emp.ts

View File

@@ -1,37 +0,0 @@
{
"private": true,
"name": "@ccms/client",
"version": "0.24.0",
"description": "MiaoScript client package",
"keywords": [
"miaoscript",
"minecraft",
"bukkit",
"sponge"
],
"author": "MiaoWoo <admin@yumc.pw>",
"homepage": "https://github.com/circlecloud/ms.git",
"license": "ISC",
"main": "dist/index.js",
"scripts": {
"dev": "ts-node-dev --respawn --debounce=1500 src/index.ts",
"clean": "rimraf dist",
"watch": "tsc --watch",
"build": "yarn clean && tsc",
"start": "node dist/index.js",
"debug": "DEBUG=minecraft-protocol node dist/index.js",
"emp": "node dist/emp.js",
"test": "echo \"Error: run tests from root\" && exit 1"
},
"dependencies": {
"axios": "^0.27.2",
"minecraft-protocol": "^1.34.0",
"minecraft-protocol-forge": "^1.0.0",
"proxy-agent": "^5.0.0"
},
"devDependencies": {
"@types/node": "^17.0.25",
"rimraf": "^3.0.2",
"typescript": "^4.6.3"
}
}

View File

@@ -1,101 +0,0 @@
class MessagePart {
text: string
color: string
clickEvent: MessagePartEvent
hoverEvent: MessagePartEvent
translate: string
with: MessagePart[]
extra: MessagePart[]
}
class MessagePartEvent {
action: string
value: string
}
var colorMap = []
colorMap['0'] = '38;5;0'
colorMap['1'] = '38;5;4'
colorMap['2'] = '38;5;2'
colorMap['3'] = '38;5;6'
colorMap['4'] = '38;5;1'
colorMap['5'] = '38;5;5'
colorMap['6'] = '38;5;3'
colorMap['7'] = '38;5;7'
colorMap['8'] = '38;5;8'
colorMap['9'] = '38;5;12'
colorMap['a'] = '38;5;10'
colorMap['b'] = '38;5;14'
colorMap['c'] = '38;5;9'
colorMap['d'] = '38;5;13'
colorMap['e'] = '38;5;11'
colorMap['f'] = '38;5;15'
colorMap['r'] = '0'
colorMap['l'] = '1'
colorMap['n'] = '4'
var regexMap = []
for (const c in colorMap) {
regexMap[colorMap[c]] = new RegExp(`§${c}`, "g")
}
function mcColor2ANSI(str) {
for (const regex in regexMap) {
str = str.replace(regexMap[regex], `\u001b[${regex}m`)
}
return str;
}
let jsonColorMap = {
"black": '0',
"dark_blue": '1',
"dark_green": '2',
"dark_aqua": '3',
"dark_red": '4',
"dark_purple": '5',
"gold": '6',
"gray": '7',
"dark_gray": '8',
"blue": '9',
"green": 'a',
"aqua": 'b',
"red": 'c',
"light_purple": 'd',
"yellow": 'e',
"white": 'f',
"obfuscated": 'k',
"bold": 'l',
"strikethrough": 'm',
"underline": 'n',
"italic": 'o',
"reset": 'r',
};
function json2text(json: MessagePart): string {
let temp = "";
if (json.color) {
temp += `§${jsonColorMap[json.color]}`
}
temp += json.text || json.translate || ''
if (json.extra) {
json.extra.forEach((ext) => {
temp += json2text(ext)
})
}
return temp += '§r'
}
function $(input: any) {
if (typeof input === "string") {
input = JSON.parse(input)
}
input = json2text(input) + '§r'
if (input.startsWith('§卐')) {
input = input.substring(2)
}
return mcColor2ANSI(input)
}
export {
json2text,
mcColor2ANSI,
$
}

View File

@@ -1,33 +0,0 @@
import { $ } from './color'
export function attachEvents(client) {
client.on('chat', (packet) => {
// Listen for chat messages and echo them back.
var jsonMsg = JSON.parse(packet.message)
console.log($(jsonMsg))
})
client.on('state', (newState, oldState) => {
console.log('Client Change State', oldState, 'to', newState)
let targetServer = process.argv[6]
if (newState == "play" && targetServer) {
setTimeout(() => {
client.write('chat', {
message: '/server ' + targetServer
})
}, 3000)
}
})
client.on('update_health', (packet) => {
if (packet.health <= 0) {
console.log("Player Dead Auto Respawn...")
client.write('client_command', { payload: 0 })
} else if (packet.health > 0) {
}
})
client.on('kick_disconnect', (packet) => {
console.log($(packet.reason))
})
client.on('disconnect', (packet) => {
console.log($(packet.reason))
})
}

View File

@@ -1,14 +0,0 @@
export function attachForge(client) {
client.on('custom_payload', function(packet) {
if (packet.channel === 'FML|HS') {
client.write('custom_payload', {
channel: 'FML|HS',
data: Buffer.of(0x01, 0x02)
});
client.write('custom_payload', {
channel: 'FML|HS',
data: Buffer.of(0x02, 0x00)
});
}
});
}

View File

@@ -1,124 +0,0 @@
import { createInterface } from 'readline'
import { Client, createClient } from 'minecraft-protocol'
import { attachForge } from './forge'
import { attachEvents } from './event'
let readUserInfo = process.argv[2] || 'Mr_jtb'
let realUserInfo = readUserInfo.split(":")
let username = realUserInfo[0]
let password = realUserInfo[1] || ''
let version = process.argv[3] || '1.12.2'
let readAddress = process.argv[4] || '192.168.2.25:25565'
let realAddress = readAddress.split(":")
let address = realAddress[0]
let port = parseInt(realAddress[1] || "25565")
let client = commandLineCreateClient()
function commandLineCreateClient() {
return createConnection(address, port, username, password)
}
function createConnection(host: string, port: number, username: string, password: string) {
let clientOptions: any = {
version,
host,
port,
username,
password,
// clientToken: 'd02c7f39-2376-45da-a5a5-50e24fa8b185',
//@ts-ignore
// authServer: 'https://skin.yumc.pw/api/yggdrasil/authserver',
// sessionServer: 'https://skin.yumc.pw/api/yggdrasil/sessionserver'
}
if (clientOptions.password) {
clientOptions.clientToken = 'd02c7f39-2376-45da-a5a5-50e24fa8b185'
clientOptions.authServer = 'https://skin.yumc.pw/api/yggdrasil/authserver'
clientOptions.sessionServer = 'https://skin.yumc.pw/api/yggdrasil/sessionserver'
}
let client = createClient(clientOptions)
attachCommon(client)
attachForge(client)
attachEvents(client)
return client
}
function attachCommon(client: Client) {
client.on('login', () => {
// client.registerChannel('updater', ['string', []])
// client.registerChannel('updater-enabled', ['string', []])
// client.registerChannel('dragoncore', ['string', []])
// client.registerChannel('dragoncore:main', ['string', []])
client.on('REGISTER', (array) => {
for (const channel of array) {
client.on('channel', console.log)
}
})
// client.on('dragoncore:main', (data) => {
// console.log(data)
// })
})
client.on('custom_payload', (data) => {
console.log('custom_payload' + JSON.stringify(data))
})
client.on('error', (error) => {
console.log("Client Error", error)
})
client.on('end', (resone) => {
console.log("Client End Resone:", resone)
if (`${resone}` != "SocketClosed") {
setTimeout(() => {
client = commandLineCreateClient()
}, 500)
} else {
process.exit(0)
}
})
}
const rl = createInterface({
input: process.stdin,
output: process.stdout,
completer: (line, func) => {
let args = line.split(' ')
let comp = args[args.length - 1]
client.once('tab_complete', (msg) => {
let mcts = msg.matches.filter(s => s)
func(null, [mcts, comp])
})
client.write('tab_complete', {
text: line
})
},
terminal: true,
prompt: ''
})
rl.on('line', function (line) {
switch (line) {
case "":
break
case "eval":
break
case "write":
break
case "/respawn":
client.write('client_command', { payload: 0 })
break
case "//reco":
client.end("")
client = commandLineCreateClient()
break
case "//quit":
console.info('Disconnected')
client.end("")
break
case "//end":
console.info('Forcibly ended client')
process.exit(0)
default:
client.write('chat', { message: line })
}
rl.prompt()
})

View File

@@ -1,5 +0,0 @@
while :; do
yarn emp
echo 进程退出 休眠120秒!
sleep 120
done

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/common", "name": "@ccms/common",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript common package", "description": "MiaoScript common package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -19,11 +19,11 @@
"test": "echo \"Error: run tests from root\" && exit 1" "test": "echo \"Error: run tests from root\" && exit 1"
}, },
"devDependencies": { "devDependencies": {
"@ccms/nashorn": "^0.24.0", "@ccms/nashorn": "^0.27.6",
"@javatypes/jdk": "^0.0.3", "@javatypes/jdk": "^0.0.3",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
}, },
"gitHead": "562e2d00175c9d3a99c8b672aa07e6d92706a027" "gitHead": "562e2d00175c9d3a99c8b672aa07e6d92706a027"
} }

View File

@@ -19,12 +19,20 @@ interface RequestConfig {
method?: Method method?: Method
headers?: { [key: string]: string } headers?: { [key: string]: string }
params?: { [key: string]: string } params?: { [key: string]: string }
data?: any data?: any,
connectTimeout?: number,
readTimeout?: number,
} }
function request(config: RequestConfig) { function request(config: RequestConfig) {
// @ts-ignore XMLHttpRequest class only exist nashorn polyfill // @ts-ignore XMLHttpRequest class only exist nashorn polyfill
let xhr = new XMLHttpRequest() let xhr = new XMLHttpRequest()
if (config.connectTimeout) {
xhr.connectTimeout = config.connectTimeout
}
if (config.readTimeout) {
xhr.readTimeout = config.readTimeout
}
xhr.open(config.method, config.url, false) xhr.open(config.method, config.url, false)
for (const header in config.headers) { for (const header in config.headers) {
xhr.setRequestHeader(header, config.headers[header]) xhr.setRequestHeader(header, config.headers[header])

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/compile", "name": "@ccms/compile",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript compile package", "description": "MiaoScript compile package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -20,7 +20,7 @@
}, },
"devDependencies": { "devDependencies": {
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/container", "name": "@ccms/container",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript container package", "description": "MiaoScript container package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -19,10 +19,10 @@
"test": "echo \"Error: run tests from root\" && exit 1" "test": "echo \"Error: run tests from root\" && exit 1"
}, },
"devDependencies": { "devDependencies": {
"@ccms/nashorn": "^0.24.0", "@ccms/nashorn": "^0.27.6",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
}, },
"dependencies": { "dependencies": {
"inversify": "^6.0.1", "inversify": "^6.0.1",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/core", "name": "@ccms/core",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript core package", "description": "MiaoScript core package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -20,12 +20,12 @@
}, },
"devDependencies": { "devDependencies": {
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
}, },
"dependencies": { "dependencies": {
"@ccms/api": "^0.24.0", "@ccms/api": "^0.27.6",
"@ccms/container": "^0.24.0" "@ccms/container": "^0.27.6"
}, },
"gitHead": "781524f83e52cad26d7c480513e3c525df867121" "gitHead": "781524f83e52cad26d7c480513e3c525df867121"
} }

1
packages/core/src/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
script

View File

@@ -9,6 +9,7 @@ import * as fs from '@ccms/common/dist/fs'
import { VersionUtils } from '@ccms/common/dist/version' import { VersionUtils } from '@ccms/common/dist/version'
const UUID = Java.type('java.util.UUID') const UUID = Java.type('java.util.UUID')
const MiaoScriptAPI = Java.type('pw.yumc.MiaoScript.api.MiaoScriptAPI')
@provideSingleton(MiaoScriptCore) @provideSingleton(MiaoScriptCore)
class MiaoScriptCore { class MiaoScriptCore {
@@ -24,6 +25,10 @@ class MiaoScriptCore {
enable() { enable() {
process.emit('core.before.enable') process.emit('core.before.enable')
this.loadServerConsole() this.loadServerConsole()
try {
MiaoScriptAPI.setPluginManager(this.pluginManager)
} catch (error) {
}
this.loadPlugins() this.loadPlugins()
process.emit('core.after.enable') process.emit('core.after.enable')
console.i18n("ms.core.engine.completed", { console.i18n("ms.core.engine.completed", {
@@ -105,7 +110,7 @@ function loadCoreScript(name) {
try { try {
let scriptname = name + (global.debug ? '-debug' : '') let scriptname = name + (global.debug ? '-debug' : '')
engineLoad({ engineLoad({
script: http.get(`https://ms.yumc.pw/api/plugin/download/name/${scriptname}`), script: http.get(`https://mscript.yumc.pw/api/plugin/download/name/${scriptname}`),
name: `core/${scriptname}.js` name: `core/${scriptname}.js`
}) })
} catch (error: any) { } catch (error: any) {
@@ -141,6 +146,7 @@ function createCore() {
container.bind(server.ServerType).toConstantValue(type) container.bind(server.ServerType).toConstantValue(type)
container.bind(server.ServerChecker).toSelf().inSingletonScope() container.bind(server.ServerChecker).toSelf().inSingletonScope()
container.bind(server.NativePluginManager).toSelf().inSingletonScope() container.bind(server.NativePluginManager).toSelf().inSingletonScope()
container.bind(server.NativePluginChecker).toSelf().inSingletonScope()
process.emit('core.after.initialize.detect') process.emit('core.after.initialize.detect')
process.emit('core.before.package.initialize') process.emit('core.before.package.initialize')

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/database", "name": "@ccms/database",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript database package", "description": "MiaoScript database package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -21,11 +21,11 @@
"devDependencies": { "devDependencies": {
"@javatypes/spring-jdbc": "^0.0.3", "@javatypes/spring-jdbc": "^0.0.3",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
}, },
"dependencies": { "dependencies": {
"@ccms/api": "^0.24.0", "@ccms/api": "^0.27.6",
"@ccms/container": "^0.24.0" "@ccms/container": "^0.27.6"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/i18n", "name": "@ccms/i18n",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript i18n package", "description": "MiaoScript i18n package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -19,11 +19,11 @@
"test": "echo \"Error: run tests from root\" && exit 1" "test": "echo \"Error: run tests from root\" && exit 1"
}, },
"devDependencies": { "devDependencies": {
"@ccms/nashorn": "^0.24.0", "@ccms/nashorn": "^0.27.6",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
}, },
"dependencies": { "dependencies": {
"js-yaml": "^4.1.0" "js-yaml": "^4.1.0"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/keyvalue", "name": "@ccms/keyvalue",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript keyvalue package", "description": "MiaoScript keyvalue package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -19,18 +19,18 @@
"test": "echo \"Error: run tests from root\" && exit 1" "test": "echo \"Error: run tests from root\" && exit 1"
}, },
"dependencies": { "dependencies": {
"@ccms/api": "^0.24.0", "@ccms/api": "^0.27.6",
"@ccms/common": "^0.24.0", "@ccms/common": "^0.27.6",
"@ccms/container": "^0.24.0" "@ccms/container": "^0.27.6"
}, },
"devDependencies": { "devDependencies": {
"@ccms/nashorn": "^0.24.0", "@ccms/nashorn": "^0.27.6",
"@javatypes/amqp-client": "^0.0.3", "@javatypes/amqp-client": "^0.0.3",
"@javatypes/spring-amqp": "^0.0.3", "@javatypes/spring-amqp": "^0.0.3",
"@javatypes/spring-rabbit": "^0.0.3", "@javatypes/spring-rabbit": "^0.0.3",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
}, },
"gitHead": "2589633069d24f646ac09261b1b2304c21d4ea75" "gitHead": "2589633069d24f646ac09261b1b2304c21d4ea75"
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/molang", "name": "@ccms/molang",
"version": "0.24.0", "version": "0.27.6",
"description": "A fast parser for Minecraft's MoLang", "description": "A fast parser for Minecraft's MoLang",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@@ -21,9 +21,9 @@
}, },
"homepage": "https://github.com/solvedDev/MoLang#readme", "homepage": "https://github.com/solvedDev/MoLang#readme",
"devDependencies": { "devDependencies": {
"@types/node": "^17.0.25", "@types/node": "^18.13.0",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"tslib": "^2.3.1", "tslib": "^2.5.0",
"typescript": "^4.6.3" "typescript": "^4.9.5"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/nashorn", "name": "@ccms/nashorn",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript nashorn package", "description": "MiaoScript nashorn package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -21,7 +21,7 @@
}, },
"devDependencies": { "devDependencies": {
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
} }
} }

View File

@@ -64,8 +64,18 @@ declare global {
* 引擎渠道 * 引擎渠道
*/ */
ScriptEngineChannel: string ScriptEngineChannel: string
/**
* 慢执行检测时间
*/
ScriptSlowExecuteTime: number ScriptSlowExecuteTime: number
ScriptEngineStartTime: number ScriptEngineStartTime: number
/**
* 设置全局对象
* @param key 对象名称
* @param value 对象值
* @param config 对象属性
* @returns
*/
setGlobal: (key: string, value: any, config?: PropertyDescriptor & ThisType<any>) => void setGlobal: (key: string, value: any, config?: PropertyDescriptor & ThisType<any>) => void
noop: () => void noop: () => void
console: Console console: Console

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/nodejs", "name": "@ccms/nodejs",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript nodejs package", "description": "MiaoScript nodejs package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -19,11 +19,11 @@
"test": "echo \"Error: run tests from root\" && exit 1" "test": "echo \"Error: run tests from root\" && exit 1"
}, },
"devDependencies": { "devDependencies": {
"@ccms/nashorn": "^0.24.0", "@ccms/nashorn": "^0.27.6",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"tslib": "^2.3.1", "tslib": "^2.5.0",
"typescript": "^4.6.3" "typescript": "^4.9.5"
}, },
"gitHead": "781524f83e52cad26d7c480513e3c525df867121" "gitHead": "781524f83e52cad26d7c480513e3c525df867121"
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/nukkit", "name": "@ccms/nukkit",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript nukkit package", "description": "MiaoScript nukkit package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -21,12 +21,12 @@
"devDependencies": { "devDependencies": {
"@javatypes/nukkit-api": "^0.0.3", "@javatypes/nukkit-api": "^0.0.3",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
}, },
"dependencies": { "dependencies": {
"@ccms/api": "^0.24.0", "@ccms/api": "^0.27.6",
"@ccms/common": "^0.24.0", "@ccms/common": "^0.27.6",
"@ccms/container": "^0.24.0" "@ccms/container": "^0.27.6"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/plugin", "name": "@ccms/plugin",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript plugin package", "description": "MiaoScript plugin package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -22,15 +22,15 @@
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
}, },
"dependencies": { "dependencies": {
"@ccms/api": "^0.24.0", "@ccms/api": "^0.27.6",
"@ccms/common": "^0.24.0", "@ccms/common": "^0.27.6",
"@ccms/container": "^0.24.0", "@ccms/container": "^0.27.6",
"@ccms/i18n": "^0.24.0", "@ccms/i18n": "^0.27.6",
"@ccms/verify": "^0.21.1", "@ccms/verify": "^0.25.1",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"js-yaml": "^4.1.0" "js-yaml": "^4.1.0"
} }

View File

@@ -78,7 +78,7 @@ export class PluginCommandManager {
permission = `${pluginInstance.description.name.toLocaleLowerCase()}.${command}.${subcommand || 'main'}` permission = `${pluginInstance.description.name.toLocaleLowerCase()}.${command}.${subcommand || 'main'}`
} }
if (!sender.hasPermission(permission)) { if (!sender.hasPermission(permission)) {
return pluginInstance.logger.sender(sender, `§c你需要 ${permission} 权限 才可执行此命令.`) return pluginInstance.logger.sender(sender, `§c你需要 §4${permission} §c权限 才可执行此命令.`)
} }
} }
args.shift() args.shift()
@@ -98,7 +98,7 @@ export class PluginCommandManager {
return (args.length == 1 ? cmdSubCache : []).concat(originCompleter?.apply(pluginInstance, [sender, command, args]) || []) return (args.length == 1 ? cmdSubCache : []).concat(originCompleter?.apply(pluginInstance, [sender, command, args]) || [])
} }
} }
if (!cmdCompleter) { console.warn(`[${pluginInstance.description.name}] command ${cmd.name} is not registry tabCompleter`) } if (!cmdCompleter) { console.debug(`[${pluginInstance.description.name}] command ${cmd.name} is not registry tabCompleter`) }
return [cmdExecutor, cmdCompleter] return [cmdExecutor, cmdCompleter]
} }
} }

View File

@@ -7,8 +7,7 @@ import { interfaces } from './interfaces'
import { getPluginConfigMetadata } from './utils' import { getPluginConfigMetadata } from './utils'
import { PluginConfigLoader } from './config/interfaces' import { PluginConfigLoader } from './config/interfaces'
import './config/loader/json-loader' import './config/loader'
import './config/loader/yaml-loader'
@provideSingleton(PluginConfigManager) @provideSingleton(PluginConfigManager)
export class PluginConfigManager { export class PluginConfigManager {
@@ -68,30 +67,25 @@ export class PluginConfigManager {
private loadConfig0(plugin: plugin.Plugin, metadata: interfaces.ConfigMetadata) { private loadConfig0(plugin: plugin.Plugin, metadata: interfaces.ConfigMetadata) {
try { try {
let defaultValue = metadata.default ?? plugin[metadata.variable] let defaultValue = metadata.default ?? plugin[metadata.variable]
let configValue = defaultValue || {} metadata.file = fs.concat(
if (defaultValue) { fs.file(plugin.description.loadMetadata.file).parent,
metadata.file = fs.concat( plugin.description.name,
fs.file(plugin.description.loadMetadata.file).parent, metadata.filename
plugin.description.name, )
metadata.filename let configLoader = this.getConfigLoader(metadata.format)
) if (!fs.exists(metadata.file) && defaultValue) {
let configLoader = this.getConfigLoader(metadata.format) base.save(metadata.file, configLoader.dump(defaultValue))
if (!fs.exists(metadata.file)) { console.i18n("ms.plugin.manager.config.save.default", {
base.save(metadata.file, configLoader.dump(defaultValue)) plugin: plugin.description.name,
console.i18n("ms.plugin.manager.config.save.default", { name: metadata.name,
plugin: plugin.description.name, format: metadata.format
name: metadata.name, })
format: metadata.format
})
} else if (metadata.migrate) {
configValue = configLoader.load(base.read(metadata.file)) || {}
if (defaultValue && this.setDefaultValue(configValue, defaultValue, !!metadata.default)) {
base.save(metadata.file, configLoader.dump(configValue))
}
console.debug(`[${plugin.description.name}] Load Config ${metadata.variable} from file ${metadata.file} =>
${JSON.stringify(configValue, undefined, 4).substring(0, 500)}`)
}
} }
let configValue = configLoader.load(base.read(metadata.file)) || {}
if (metadata.migrate && defaultValue && this.setDefaultValue(configValue, defaultValue, !!metadata.default)) {
base.save(metadata.file, configLoader.dump(configValue))
}
console.debug(`[${plugin.description.name}] Load Config ${metadata.variable} from file ${metadata.file}`)
this.defienConfigProp(plugin, metadata, configValue) this.defienConfigProp(plugin, metadata, configValue)
} catch (error: any) { } catch (error: any) {
console.i18n("ms.plugin.manager.config.load.error", { console.i18n("ms.plugin.manager.config.load.error", {

View File

@@ -5,9 +5,11 @@ import { getPluginListenerMetadata } from './utils'
@provideSingleton(PluginEventManager) @provideSingleton(PluginEventManager)
export class PluginEventManager { export class PluginEventManager {
@Autowired() @Autowired()
private EventManager: event.Event private eventManager: event.Event
@Autowired() @Autowired()
private ServerChecker: server.ServerChecker private serverChecker: server.ServerChecker
@Autowired()
private nativePluginChecker: server.NativePluginChecker
constructor() { constructor() {
process.on('plugin.before.enable', this.registryListener.bind(this)) process.on('plugin.before.enable', this.registryListener.bind(this))
@@ -15,26 +17,31 @@ export class PluginEventManager {
} }
mapEventName() { mapEventName() {
return this.EventManager.mapEventName().toFixed(0) return this.eventManager.mapEventName().toFixed(0)
} }
private registryListener(pluginInstance: plugin.Plugin) { private registryListener(pluginInstance: plugin.Plugin) {
let events = getPluginListenerMetadata(pluginInstance) let events = getPluginListenerMetadata(pluginInstance)
for (const event of events) { for (const event of events) {
// ignore space listener // ignore space listener
if (!this.ServerChecker.check(event.servers)) { if (!this.serverChecker.check(event.servers)) {
console.debug(`[${pluginInstance.description.name}] ${event.target.constructor.name} incompatible event ${event.name} server(${event.servers}) ignore.`) console.debug(`[${pluginInstance.description.name}] ${event.target.constructor.name} incompatible server(${event.servers}) ignore event ${event.name}.`)
continue
}
// ignore space listener
if (!this.nativePluginChecker.check(event.plugins)) {
console.debug(`[${pluginInstance.description.name}] ${event.target.constructor.name} require native plugins(${event.plugins}) ignore event ${event.name}.`)
continue continue
} }
// here must bind this to pluginInstance // here must bind this to pluginInstance
let exec = event.target[event.executor] let exec = event.target[event.executor]
let execBinded = exec.bind(pluginInstance) let execBinded = exec.bind(pluginInstance)
execBinded.executor = event.executor execBinded.executor = event.executor
exec.off = this.EventManager.listen(pluginInstance, event.name, execBinded, event.priority, event.ignoreCancel) exec.off = this.eventManager.listen(pluginInstance, event.name, execBinded, event.priority, event.ignoreCancel)
} }
} }
private unregistryListener(pluginInstance: plugin.Plugin) { private unregistryListener(pluginInstance: plugin.Plugin) {
this.EventManager.disable(pluginInstance) this.eventManager.disable(pluginInstance)
} }
} }

View File

@@ -73,6 +73,10 @@ export namespace interfaces {
* 是否忽略已取消的事件 * 是否忽略已取消的事件
*/ */
ignoreCancel?: boolean ignoreCancel?: boolean
/**
* 依赖插件 没有就不加载
*/
plugins?: string[]
} }
export interface ConfigMetadata extends plugin.BaseMetadata { export interface ConfigMetadata extends plugin.BaseMetadata {
/** /**

View File

@@ -2,12 +2,11 @@ import i18n from '@ccms/i18n'
import { plugin, server } from '@ccms/api' import { plugin, server } from '@ccms/api'
import { provideSingleton, Container, ContainerInstance, Autowired } from '@ccms/container' import { provideSingleton, Container, ContainerInstance, Autowired } from '@ccms/container'
import './config'
import { interfaces } from './interfaces' import { interfaces } from './interfaces'
import { PluginTaskManager } from './task' import { PluginTaskManager } from './task'
import { PluginEventManager } from './event' import { PluginEventManager } from './event'
import { PluginCommandManager } from './command'
import { PluginConfigManager } from './config' import { PluginConfigManager } from './config'
import { PluginCommandManager } from './command'
const Thread = Java.type('java.lang.Thread') const Thread = Java.type('java.lang.Thread')
@@ -93,18 +92,17 @@ export class PluginManagerImpl implements plugin.PluginManager {
for (const [, scanner] of this.sacnnerMap) { for (const [, scanner] of this.sacnnerMap) {
try { try {
console.i18n('ms.plugin.manager.scan', { scanner: scanner.type, folder }) console.i18n('ms.plugin.manager.scan', { scanner: scanner.type, folder })
let plugins = scanner.scan(folder) let loadMetadatas = scanner.scan(folder)
console.i18n('ms.plugin.manager.scan.finish', { scanner: scanner.type, folder, size: plugins.length }) console.i18n('ms.plugin.manager.scan.finish', { scanner: scanner.type, folder, size: loadMetadatas.length })
plugins.forEach(loadMetadata => { for (const loadMetadata of loadMetadatas) {
try { try {
this.loadAndRequirePlugin(loadMetadata) this.loadAndRequirePlugin(loadMetadata)
} catch (error: any) { } catch (error: any) {
console.error(`plugin scanner ${scanner.type} load ${loadMetadata.file} occurred error ${error}`) console.console(`§c扫描器 §4${scanner.type} §c文件 §4${loadMetadata.file.toString().replace(root, '')} §c编译失败 请提供下列错误给开发者`)
console.ex(error) console.ex(error)
} }
}) }
} catch (error: any) { } catch (error: any) {
console.error(`plugin scanner ${scanner.type} occurred error ${error}`)
console.ex(error) console.ex(error)
} }
} }
@@ -229,8 +227,16 @@ export class PluginManagerImpl implements plugin.PluginManager {
}) })
} }
has(name: string) {
return this.instanceMap.has(name)
}
get(name: string) {
return this.instanceMap.get(name) || null
}
getPlugin(name: string) { getPlugin(name: string) {
return this.instanceMap.get(name) return this.instanceMap.get(name) || null
} }
getPlugins() { getPlugins() {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/polyfill", "name": "@ccms/polyfill",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript polyfill package", "description": "MiaoScript polyfill package",
"author": "MiaoWoo <admin@yumc.pw>", "author": "MiaoWoo <admin@yumc.pw>",
"homepage": "https://github.com/circlecloud/ms.git", "homepage": "https://github.com/circlecloud/ms.git",
@@ -14,14 +14,14 @@
"test": "echo \"Error: run tests from root\" && exit 1" "test": "echo \"Error: run tests from root\" && exit 1"
}, },
"dependencies": { "dependencies": {
"@ccms/i18n": "^0.24.0", "@ccms/i18n": "^0.27.6",
"@ccms/nodejs": "^0.24.0", "@ccms/nodejs": "^0.27.6",
"core-js": "^3.22.1" "core-js": "^3.27.2"
}, },
"devDependencies": { "devDependencies": {
"@ccms/nashorn": "^0.24.0", "@ccms/nashorn": "^0.27.6",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@ import 'core-js'
process.on('exit', () => require.disable()) process.on('exit', () => require.disable())
global.setGlobal('Proxy', require('./proxy').Proxy) global.setGlobal('Proxy', require('./proxy').Proxy)
global.setGlobal('XMLHttpRequest', require('./xml-http-request').XMLHttpRequest) global.setGlobal('XMLHttpRequest', require('./xml-http-request').XMLHttpRequest)
global.setGlobal('Buffer', require('./buffer').Buffer)
global.setGlobal('Blob', require('blob-polyfill').Blob) global.setGlobal('Blob', require('blob-polyfill').Blob)
console.i18n("ms.polyfill.completed", { time: (new Date().getTime() - polyfillStartTime) / 1000 }) console.i18n("ms.polyfill.completed", { time: (new Date().getTime() - polyfillStartTime) / 1000 })
export default true export default true

View File

@@ -68,13 +68,14 @@ type EventType =
| 'timeout' | 'timeout'
| 'loadend' | 'loadend'
| 'loadstart' | 'loadstart'
type HttpHeader = { [key: string]: string } type RequestHttpHeader = { [key: string]: string }
type HttpHeader = { [key: string]: string[] }
const executor = Executors.newCachedThreadPool() const executor = Executors.newCachedThreadPool()
export class XMLHttpRequest { export class XMLHttpRequest {
private _timeout: number = 120000; private _connectTimeout: number = 5000;
private _readTimeout: number = 120000;
private _responseType: ResponseType = 'text'; private _responseType: ResponseType = 'text';
private _withCredentials: boolean private _withCredentials: boolean
@@ -84,7 +85,7 @@ export class XMLHttpRequest {
private _url: string private _url: string
private _async: boolean private _async: boolean
private _mimeType: string private _mimeType: string
private _requestHeaders: HttpHeader = {}; private _requestHeaders: RequestHttpHeader = {};
private _status: number = 0; private _status: number = 0;
private _statusText: string = null; private _statusText: string = null;
@@ -96,10 +97,22 @@ export class XMLHttpRequest {
private _connection = null; private _connection = null;
get timeout() { get timeout() {
return this._timeout return this._readTimeout
} }
set timeout(timeout: number) { set timeout(timeout: number) {
this._timeout = timeout this._readTimeout = timeout
}
get connectTimeout() {
return this._connectTimeout
}
set connectTimeout(timeout: number) {
this._connectTimeout = timeout
}
get readTimeout() {
return this._readTimeout
}
set readTimeout(timeout: number) {
this._readTimeout = timeout
} }
get readyState() { get readyState() {
return this._readyState return this._readyState
@@ -143,7 +156,7 @@ export class XMLHttpRequest {
this._requestHeaders[key] = val this._requestHeaders[key] = val
} }
getResponseHeader(key: string): string { getResponseHeader(key: string): string {
return this._responseHeaders[key] return this._responseHeaders[key]?.[0]
} }
getAllResponseHeaders(): any { getAllResponseHeaders(): any {
return this._responseHeaders return this._responseHeaders
@@ -169,8 +182,8 @@ export class XMLHttpRequest {
this._connection.setRequestMethod(this._method) this._connection.setRequestMethod(this._method)
this._connection.setDoOutput(true) this._connection.setDoOutput(true)
this._connection.setDoInput(true) this._connection.setDoInput(true)
this._connection.setConnectTimeout(this._timeout) this._connection.setConnectTimeout(this._connectTimeout)
this._connection.setReadTimeout(this._timeout) this._connection.setReadTimeout(this._readTimeout)
this.setRequestHeader('X-Requested-With', 'XMLHttpRequest') this.setRequestHeader('X-Requested-With', 'XMLHttpRequest')
this.setReadyState(ReadyState.OPENED) this.setReadyState(ReadyState.OPENED)
@@ -181,7 +194,7 @@ export class XMLHttpRequest {
} }
if (this._readyState !== ReadyState.OPENED) { throw new Error(`Error Status ${this._readyState}!`) } if (this._readyState !== ReadyState.OPENED) { throw new Error(`Error Status ${this._readyState}!`) }
let future = executor.submit(new Callable({ call: () => this._send(body) })) let future = executor.submit(new Callable({ call: () => this._send(body) }))
if (!this._async) { future.get(this._timeout, TimeUnit.MILLISECONDS) } if (!this._async) { future.get(this._connectTimeout + this._readTimeout + 100, TimeUnit.MILLISECONDS) }
return future return future
} }
get() { get() {
@@ -199,22 +212,23 @@ export class XMLHttpRequest {
} }
abort() { abort() {
this._connection.disconnect() this._connection.disconnect()
this.onabort && this.onabort() this.onabort?.()
} }
private _send(body?: string | object) { private _send(body?: string | object) {
try { try {
this._connection.connect() this._connection.connect()
this.onloadstart && this.onloadstart() this.onloadstart?.()
if (body) { if (body) {
let bodyType = Object.prototype.toString.call(body) let bodyType = Object.prototype.toString.call(body)
if (typeof body !== "string") { throw new Error(`body(${bodyType}) must be string!`) } if (typeof body !== "string") { throw new Error(`body(${bodyType}) must be string.`) }
var out = this._connection.getOutputStream() var out = this._connection.getOutputStream()
out.write(new JavaString(body).getBytes(UTF_8)) out.write(new JavaString(body).getBytes(UTF_8))
out.flush() out.flush()
out.close() out.close()
} }
this.setReadyState(ReadyState.LOADING) this.setReadyState(ReadyState.LOADING)
this.onload?.()
this._status = this._connection.getResponseCode() this._status = this._connection.getResponseCode()
this._statusText = this._connection.getResponseMessage() this._statusText = this._connection.getResponseMessage()
if (this._status >= 0 && this._status < 300) { if (this._status >= 0 && this._status < 300) {
@@ -224,8 +238,8 @@ export class XMLHttpRequest {
} else { } else {
this._responseText = this.readOutput(this._connection.getErrorStream()) this._responseText = this.readOutput(this._connection.getErrorStream())
} }
this.setResponseHeaders(this._connection.getHeaderFields()) this.setResponseHeaders()
this.onloadend && this.onloadend() this.onloadend?.()
} catch (ex: any) { } catch (ex: any) {
if (ex instanceof SocketTimeoutException && this.ontimeout) { if (ex instanceof SocketTimeoutException && this.ontimeout) {
return this.ontimeout(ex) return this.ontimeout(ex)
@@ -239,15 +253,15 @@ export class XMLHttpRequest {
} }
} }
private setResponseHeaders(header: any) { private setResponseHeaders() {
header.forEach((key: string | number, value: string | any[]) => { this._connection.getHeaderFields().forEach((key: string | number, value: any[]) => {
this._responseHeaders[key + ''] = value[value.length - 1] + '' this._responseHeaders[key + ''] = Java.from(value)
}) })
} }
private setReadyState(state: ReadyState) { private setReadyState(state: ReadyState) {
this._readyState = state this._readyState = state
this.onreadystatechange && this.onreadystatechange() this.onreadystatechange?.()
} }
private readOutput(input: any) { private readOutput(input: any) {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/protocol", "name": "@ccms/protocol",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript protocol package", "description": "MiaoScript protocol package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -21,7 +21,7 @@
}, },
"devDependencies": { "devDependencies": {
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
} }
} }

View File

@@ -0,0 +1,26 @@
{
"name": "@ccms/qrcode",
"version": "0.27.6",
"description": "MiaoScript qrcode package",
"keywords": [
"miaoscript",
"minecraft",
"bukkit",
"sponge"
],
"author": "MiaoWoo <admin@yumc.pw>",
"homepage": "https://github.com/circlecloud/ms.git",
"license": "ISC",
"main": "dist/index.js",
"scripts": {
"clean": "rimraf dist",
"watch": "tsc --watch",
"build": "yarn clean && tsc",
"test": "echo \"Error: run tests from root\" && exit 1"
},
"devDependencies": {
"reflect-metadata": "^0.1.13",
"rimraf": "^4.1.2",
"typescript": "^4.9.5"
}
}

1243
packages/qrcode/src/index.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/sponge", "name": "@ccms/sponge",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript sponge package", "description": "MiaoScript sponge package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -21,12 +21,12 @@
"devDependencies": { "devDependencies": {
"@javatypes/sponge-api": "^0.0.3", "@javatypes/sponge-api": "^0.0.3",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
}, },
"dependencies": { "dependencies": {
"@ccms/api": "^0.24.0", "@ccms/api": "^0.27.6",
"@ccms/common": "^0.24.0", "@ccms/common": "^0.27.6",
"@ccms/container": "^0.24.0" "@ccms/container": "^0.27.6"
} }
} }

View File

@@ -1,32 +1,15 @@
import { provideSingleton } from '@ccms/container' import { provideSingleton } from '@ccms/container'
import { particle, plugin } from '@ccms/api' import { particle } from '@ccms/api'
@provideSingleton(particle.ParticleManager) @provideSingleton(particle.ParticleManager)
export class SpongeParticleManager extends particle.ParticleManager { export class SpongeParticleManager extends particle.ParticleManager {
private globalSpawner = new SpongeParticleSpawner()
constructor() {
super()
particle.ParticleManager.globalSpawner = this.globalSpawner
}
protected getGlobalSpawner() {
return this.globalSpawner
}
} }
@provideSingleton(particle.ParticleSpawner)
export class SpongeParticleSpawner extends particle.ParticleSpawner { export class SpongeParticleSpawner extends particle.ParticleSpawner {
spawnParticle(location: org.spongepowered.api.world.Location<any>, particle: any, count: number = 1) {
location.getPosition()
// location.getWorld().spawnParticle(particle, location, count)
}
spawn(location: any, particle: particle.Particle) { spawn(location: any, particle: particle.Particle) {
location.getWorld().spawnParticle( throw new Error('Not Impl.')
particle.getParticle(), }
location, spawnToPlayer(player: any, location: any, particle: particle.Particle) {
particle.getCount(), throw new Error('Not Impl.')
particle.getOffsetX(),
particle.getOffsetY(),
particle.getOffsetZ(),
particle.getExtra(),
particle.getData()
)
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/spring", "name": "@ccms/spring",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript spring package", "description": "MiaoScript spring package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -20,13 +20,13 @@
}, },
"devDependencies": { "devDependencies": {
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
}, },
"dependencies": { "dependencies": {
"@ccms/api": "^0.24.0", "@ccms/api": "^0.27.6",
"@ccms/common": "^0.24.0", "@ccms/common": "^0.27.6",
"@ccms/container": "^0.24.0", "@ccms/container": "^0.27.6",
"@ccms/database": "^0.24.0" "@ccms/database": "^0.27.6"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/web", "name": "@ccms/web",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript web package", "description": "MiaoScript web package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -25,11 +25,11 @@
"@javatypes/spring-web": "^0.0.3", "@javatypes/spring-web": "^0.0.3",
"@javatypes/tomcat": "^0.0.3", "@javatypes/tomcat": "^0.0.3",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
}, },
"dependencies": { "dependencies": {
"@ccms/api": "^0.24.0", "@ccms/api": "^0.27.6",
"@ccms/container": "^0.24.0" "@ccms/container": "^0.27.6"
} }
} }

View File

@@ -0,0 +1,4 @@
{
"tabWidth": 2,
"semi": true
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ccms/websocket", "name": "@ccms/websocket",
"version": "0.24.0", "version": "0.27.6",
"description": "MiaoScript websocket package", "description": "MiaoScript websocket package",
"keywords": [ "keywords": [
"miaoscript", "miaoscript",
@@ -19,14 +19,15 @@
"test": "echo \"Error: run tests from root\" && exit 1" "test": "echo \"Error: run tests from root\" && exit 1"
}, },
"dependencies": { "dependencies": {
"@socket.io/component-emitter": "3.1.0",
"backo2": "^1.0.2", "backo2": "^1.0.2",
"parseuri": "^0.0.6" "parseuri": "^0.0.6"
}, },
"devDependencies": { "devDependencies": {
"@ccms/nashorn": "^0.24.0", "@ccms/nashorn": "^0.27.6",
"@javatypes/tomcat-websocket-api": "^0.0.3", "@javatypes/tomcat-websocket-api": "^0.0.3",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"typescript": "^4.6.3" "typescript": "^4.9.5"
} }
} }

View File

@@ -38,7 +38,7 @@ export class WebSocket extends EventEmitter {
private client: Transport private client: Transport
constructor(url: string, subProtocol: string = '', headers: WebSocketHeader = {}) { constructor(url: string, subProtocol: string | string[] = '', headers: WebSocketHeader = {}) {
super() super()
this.manager = manager this.manager = manager
this._url = url this._url = url

View File

@@ -1 +1,57 @@
export = (namepsace) => (...args) => { }//console.debug(namepsace, ...args) export = (namepsace) =>
(...args) => {
console.trace(`[${namepsace}] ` + format(...args))
} //console.debug(namepsace, ...args)
let formatters: any = {}
formatters.s = function (v) {
return v
}
formatters.j = function (v) {
try {
return JSON.stringify(v)
} catch (error: any) {
return "[UnexpectedJSONParseError]: " + error.message
}
}
/**
* Coerce `val`.
*
* @param {Mixed} val
* @return {Mixed}
* @api private
*/
function coerce(val) {
if (val instanceof Error) {
return val.stack || val.message
}
return val
}
function format(...args) {
// Apply any `formatters` transformations
args[0] = coerce(args[0])
if (typeof args[0] !== "string") {
// Anything else let's inspect with %O
args.unshift("%O")
}
let index = 0
args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format) => {
// If we encounter an escaped % then don't increase the array index
if (match === "%%") {
return "%"
}
index++
const formatter = formatters[format]
if (typeof formatter === "function") {
const val = args[index]
match = formatter.call(format, val)
// Now we need to remove `args[index]` since it's inlined in the `format`
args.splice(index, 1)
index--
}
return match
})
return args[0]
}

View File

@@ -0,0 +1,12 @@
// imported from https://github.com/component/has-cors
let value = false;
try {
value = typeof XMLHttpRequest !== 'undefined' &&
'withCredentials' in new XMLHttpRequest();
} catch (err) {
// if XMLHttp support is disabled in IE then it will throw
// when trying to create
}
export const hasCORS = value;

View File

@@ -0,0 +1,38 @@
// imported from https://github.com/galkn/querystring
/**
* Compiles a querystring
* Returns string representation of the object
*
* @param {Object}
* @api private
*/
export function encode(obj) {
let str = ''
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
if (str.length) str += '&'
str += encodeURIComponent(i) + '=' + encodeURIComponent(obj[i])
}
}
return str
}
/**
* Parses a simple querystring into an object
*
* @param {String} qs
* @api private
*/
export function decode(qs) {
let qry = {}
let pairs = qs.split('&')
for (let i = 0, l = pairs.length; i < l; i++) {
let pair = pairs[i].split('=')
qry[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1])
}
return qry
}

View File

@@ -0,0 +1,68 @@
// imported from https://github.com/galkn/parseuri
/**
* Parses an URI
*
* @author Steven Levithan <stevenlevithan.com> (MIT license)
* @api private
*/
const re = /^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
const parts = [
'source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'anchor'
]
export function parse(str) {
const src = str,
b = str.indexOf('['),
e = str.indexOf(']')
if (b != -1 && e != -1) {
str = str.substring(0, b) + str.substring(b, e).replace(/:/g, ';') + str.substring(e, str.length)
}
let m = re.exec(str || ''),
uri = {} as any,
i = 14
while (i--) {
uri[parts[i]] = m[i] || ''
}
if (b != -1 && e != -1) {
uri.source = src
uri.host = uri.host.substring(1, uri.host.length - 1).replace(/;/g, ':')
uri.authority = uri.authority.replace('[', '').replace(']', '').replace(/;/g, ':')
uri.ipv6uri = true
}
uri.pathNames = pathNames(uri, uri['path'])
uri.queryKey = queryKey(uri, uri['query'])
return uri
}
function pathNames(obj, path) {
const regx = /\/{2,9}/g,
names = path.replace(regx, "/").split("/")
if (path.slice(0, 1) == '/' || path.length === 0) {
names.splice(0, 1)
}
if (path.slice(-1) == '/') {
names.splice(names.length - 1, 1)
}
return names
}
function queryKey(uri, query) {
const data = {}
query.replace(/(?:^|&)([^&=]*)=?([^&]*)/g, function ($0, $1, $2) {
if ($1) {
data[$1] = $2
}
})
return data
}

View File

@@ -0,0 +1,62 @@
// imported from https://github.com/unshiftio/yeast
'use strict'
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_'.split('')
, length = 64
, map = {}
let seed = 0
, i = 0
, prev
/**
* Return a string representing the specified number.
*
* @param {Number} num The number to convert.
* @returns {String} The string representation of the number.
* @api public
*/
export function encode(num) {
let encoded = ''
do {
encoded = alphabet[num % length] + encoded
num = Math.floor(num / length)
} while (num > 0)
return encoded
}
/**
* Return the integer value specified by the given string.
*
* @param {String} str The string to convert.
* @returns {Number} The integer value represented by the string.
* @api public
*/
export function decode(str) {
let decoded = 0
for (i = 0; i < str.length; i++) {
decoded = decoded * length + map[str.charAt(i)]
}
return decoded
}
/**
* Yeast: A tiny growing id generator.
*
* @returns {String} A unique id.
* @api public
*/
export function yeast() {
const now = encode(+new Date())
if (now !== prev) return seed = 0, prev = now
return now + '.' + encode(seed++)
}
//
// Map each character to its index.
//
for (; i < length; i++) map[alphabet[i]] = i

View File

@@ -1,16 +1,10 @@
import { Socket } from './socket' import { Socket } from "./socket"
export default (uri, opts) => new Socket(uri, opts) export { Socket }
export { SocketOptions } from "./socket"
/** export const protocol = Socket.protocol
* Expose deps for legacy compatibility export { Transport } from "./transport"
* and standalone browser access. export { transports } from "./transports/index"
*/ export { installTimerFunctions } from "./util"
const protocol = Socket.protocol // this is an int export { parse } from "./contrib/parseuri"
export { Socket, protocol } export { nextTick } from "./transports/websocket-constructor"
// module.exports.Transport = require("./transport")
// module.exports.transports = require("./transports/index")
// module.exports.parser = require("../engine.io-parser")
export * from './transport'
export * from './transports/index'
export * from '../engine.io-parser'

View File

@@ -1,21 +1,295 @@
import transports from "./transports" // import { transports } from "./transports/index.js";
// const transports = require("./transports/index") import { transports } from "./transports"
const Emitter = require("component-emitter") import { installTimerFunctions, byteLength } from "./util"
const debug = (...args: any) => console.debug('engine.io-client:socket', ...args)//require("debug")("engine.io-client:socket") import { decode } from "./contrib/parseqs"
import parser from "../engine.io-parser" import { parse } from "./contrib/parseuri"
const parseuri = require("parseuri") // import debugModule from "debug"; // debug()
const parseqs = require("parseqs") import { Emitter } from "@socket.io/component-emitter"
import { installTimerFunctions } from "./util" // import { protocol } from "engine.io-parser";
import { protocol } from "../engine.io-parser"
import type { Packet, BinaryType, PacketType, RawData } from "../engine.io-parser"
import { CloseDetails, Transport } from "./transport"
// const debug = debugModule("engine.io-client:socket"); // debug()
const debug = require('../debug')('engine.io-client:socket')
export interface SocketOptions {
/**
* The host that we're connecting to. Set from the URI passed when connecting
*/
host: string
/**
* The hostname for our connection. Set from the URI passed when connecting
*/
hostname: string
/**
* If this is a secure connection. Set from the URI passed when connecting
*/
secure: boolean
/**
* The port for our connection. Set from the URI passed when connecting
*/
port: string | number
/**
* Any query parameters in our uri. Set from the URI passed when connecting
*/
query: { [key: string]: any }
/**
* `http.Agent` to use, defaults to `false` (NodeJS only)
*/
agent: string | boolean
/**
* Whether the client should try to upgrade the transport from
* long-polling to something better.
* @default true
*/
upgrade: boolean
/**
* Forces base 64 encoding for polling transport even when XHR2
* responseType is available and WebSocket even if the used standard
* supports binary.
*/
forceBase64: boolean
/**
* The param name to use as our timestamp key
* @default 't'
*/
timestampParam: string
/**
* Whether to add the timestamp with each transport request. Note: this
* is ignored if the browser is IE or Android, in which case requests
* are always stamped
* @default false
*/
timestampRequests: boolean
/**
* A list of transports to try (in order). Engine.io always attempts to
* connect directly with the first one, provided the feature detection test
* for it passes.
* @default ['polling','websocket']
*/
transports: string[]
/**
* The port the policy server listens on
* @default 843
*/
policyPost: number
/**
* If true and if the previous websocket connection to the server succeeded,
* the connection attempt will bypass the normal upgrade process and will
* initially try websocket. A connection attempt following a transport error
* will use the normal upgrade process. It is recommended you turn this on
* only when using SSL/TLS connections, or if you know that your network does
* not block websockets.
* @default false
*/
rememberUpgrade: boolean
/**
* Are we only interested in transports that support binary?
*/
onlyBinaryUpgrades: boolean
/**
* Timeout for xhr-polling requests in milliseconds (0) (only for polling transport)
*/
requestTimeout: number
/**
* Transport options for Node.js client (headers etc)
*/
transportOptions: Object
/**
* (SSL) Certificate, Private key and CA certificates to use for SSL.
* Can be used in Node.js client environment to manually specify
* certificate information.
*/
pfx: string
/**
* (SSL) Private key to use for SSL. Can be used in Node.js client
* environment to manually specify certificate information.
*/
key: string
/**
* (SSL) A string or passphrase for the private key or pfx. Can be
* used in Node.js client environment to manually specify certificate
* information.
*/
passphrase: string
/**
* (SSL) Public x509 certificate to use. Can be used in Node.js client
* environment to manually specify certificate information.
*/
cert: string
/**
* (SSL) An authority certificate or array of authority certificates to
* check the remote host against.. Can be used in Node.js client
* environment to manually specify certificate information.
*/
ca: string | string[]
/**
* (SSL) A string describing the ciphers to use or exclude. Consult the
* [cipher format list]
* (http://www.openssl.org/docs/apps/ciphers.html#CIPHER_LIST_FORMAT) for
* details on the format.. Can be used in Node.js client environment to
* manually specify certificate information.
*/
ciphers: string
/**
* (SSL) If true, the server certificate is verified against the list of
* supplied CAs. An 'error' event is emitted if verification fails.
* Verification happens at the connection level, before the HTTP request
* is sent. Can be used in Node.js client environment to manually specify
* certificate information.
*/
rejectUnauthorized: boolean
/**
* Headers that will be passed for each request to the server (via xhr-polling and via websockets).
* These values then can be used during handshake or for special proxies.
*/
extraHeaders?: { [header: string]: string }
/**
* Whether to include credentials (cookies, authorization headers, TLS
* client certificates, etc.) with cross-origin XHR polling requests
* @default false
*/
withCredentials: boolean
/**
* Whether to automatically close the connection whenever the beforeunload event is received.
* @default true
*/
closeOnBeforeunload: boolean
/**
* Whether to always use the native timeouts. This allows the client to
* reconnect when the native timeout functions are overridden, such as when
* mock clocks are installed.
* @default false
*/
useNativeTimers: boolean
/**
* weather we should unref the reconnect timer when it is
* create automatically
* @default false
*/
autoUnref: boolean
/**
* parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to false to disable.
* @default false
*/
perMessageDeflate: { threshold: number }
/**
* The path to get our client file from, in the case of the server
* serving it
* @default '/engine.io'
*/
path: string
/**
* Whether we should add a trailing slash to the request path.
* @default true
*/
addTrailingSlash: boolean
/**
* Either a single protocol string or an array of protocol strings. These strings are used to indicate sub-protocols,
* so that a single server can implement multiple WebSocket sub-protocols (for example, you might want one server to
* be able to handle different types of interactions depending on the specified protocol)
* @default []
*/
protocols: string | string[]
}
interface HandshakeData {
sid: string
upgrades: string[]
pingInterval: number
pingTimeout: number
maxPayload: number
}
interface SocketReservedEvents {
open: () => void
handshake: (data: HandshakeData) => void
packet: (packet: Packet) => void
packetCreate: (packet: Packet) => void
data: (data) => void
message: (data) => void
drain: () => void
flush: () => void
heartbeat: () => void
ping: () => void
pong: () => void
error: (err: string | Error) => void
upgrading: (transport) => void
upgrade: (transport) => void
upgradeError: (err: Error) => void
close: (reason: string, description?: CloseDetails | Error) => void
}
type SocketState = "opening" | "open" | "closing" | "closed"
export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
public id: string
public transport: Transport
public binaryType: BinaryType
public readyState: SocketState
public writeBuffer: Packet[] = [];
private prevBufferLen: number
private upgrades
private pingInterval: number
private pingTimeout: number
private pingTimeoutTimer: NodeJS.Timer
private setTimeoutFn: typeof setTimeout
private clearTimeoutFn: typeof clearTimeout
private readonly beforeunloadEventListener: () => void
private readonly offlineEventListener: () => void
private upgrading: boolean
private maxPayload?: number
private readonly opts: Partial<SocketOptions>
private readonly secure: boolean
private readonly hostname: string
private readonly port: string | number
private readonly transports: string[]
static priorWebsocketSuccess: boolean
static protocol = protocol;
export class Socket extends Emitter {
/** /**
* Socket constructor. * Socket constructor.
* *
* @param {String|Object} uri or options * @param {String|Object} uri or options
* @param {Object} options * @param {Object} opts - options
* @api public * @api public
*/ */
constructor(uri, opts: any = {}) { constructor(uri, opts: Partial<SocketOptions> = {}) {
super() super()
if (uri && "object" === typeof uri) { if (uri && "object" === typeof uri) {
@@ -24,13 +298,13 @@ export class Socket extends Emitter {
} }
if (uri) { if (uri) {
uri = parseuri(uri) uri = parse(uri)
opts.hostname = uri.host opts.hostname = uri.host
opts.secure = uri.protocol === "https" || uri.protocol === "wss" opts.secure = uri.protocol === "https" || uri.protocol === "wss"
opts.port = uri.port opts.port = uri.port
if (uri.query) opts.query = uri.query if (uri.query) opts.query = uri.query
} else if (opts.host) { } else if (opts.host) {
opts.hostname = parseuri(opts.host).host opts.hostname = parse(opts.host).host
} }
installTimerFunctions(this, opts) installTimerFunctions(this, opts)
@@ -53,11 +327,10 @@ export class Socket extends Emitter {
(typeof location !== "undefined" && location.port (typeof location !== "undefined" && location.port
? location.port ? location.port
: this.secure : this.secure
? 443 ? "443"
: 80) : "80")
this.transports = ["websocket"] this.transports = opts.transports || ["polling", "websocket"]
this.readyState = ""
this.writeBuffer = [] this.writeBuffer = []
this.prevBufferLen = 0 this.prevBufferLen = 0
@@ -67,9 +340,9 @@ export class Socket extends Emitter {
agent: false, agent: false,
withCredentials: false, withCredentials: false,
upgrade: true, upgrade: true,
jsonp: true,
timestampParam: "t", timestampParam: "t",
rememberUpgrade: false, rememberUpgrade: false,
addTrailingSlash: true,
rejectUnauthorized: true, rejectUnauthorized: true,
perMessageDeflate: { perMessageDeflate: {
threshold: 1024 threshold: 1024
@@ -80,10 +353,12 @@ export class Socket extends Emitter {
opts opts
) )
this.opts.path = this.opts.path.replace(/\/$/, "") + "/" this.opts.path =
this.opts.path.replace(/\/$/, "") +
(this.opts.addTrailingSlash ? "/" : "")
if (typeof this.opts.query === "string") { if (typeof this.opts.query === "string") {
this.opts.query = parseqs.decode(this.opts.query) this.opts.query = decode(this.opts.query)
} }
// set on handshake // set on handshake
@@ -100,21 +375,20 @@ export class Socket extends Emitter {
// Firefox closes the connection when the "beforeunload" event is emitted but not Chrome. This event listener // Firefox closes the connection when the "beforeunload" event is emitted but not Chrome. This event listener
// ensures every browser behaves the same (no "disconnect" event at the Socket.IO level when the page is // ensures every browser behaves the same (no "disconnect" event at the Socket.IO level when the page is
// closed/reloaded) // closed/reloaded)
addEventListener( this.beforeunloadEventListener = () => {
"beforeunload", if (this.transport) {
() => { // silently close the transport
if (this.transport) { this.transport.removeAllListeners()
// silently close the transport this.transport.close()
this.transport.removeAllListeners() }
this.transport.close() }
} addEventListener("beforeunload", this.beforeunloadEventListener, false)
},
false
)
} }
if (this.hostname !== "localhost") { if (this.hostname !== "localhost") {
this.offlineEventListener = () => { this.offlineEventListener = () => {
this.onClose("transport close") this.onClose("transport close", {
description: "network connection lost",
})
} }
addEventListener("offline", this.offlineEventListener, false) addEventListener("offline", this.offlineEventListener, false)
} }
@@ -126,19 +400,16 @@ export class Socket extends Emitter {
/** /**
* Creates transport of the given type. * Creates transport of the given type.
* *
* @param {String} transport name * @param {String} name - transport name
* @return {Transport} * @return {Transport}
* @api private * @private
*/ */
createTransport(name, opt?) { private createTransport(name) {
if (name != 'websocket') {
throw new Error('Only Support WebSocket in MiaoScript!')
}
debug('creating transport "%s"', name) debug('creating transport "%s"', name)
const query: any = clone(this.opts.query) const query: any = Object.assign({}, this.opts.query)
// append engine.io protocol identifier // append engine.io protocol identifier
query.EIO = parser.protocol query.EIO = protocol
// transport name // transport name
query.transport = name query.transport = name
@@ -155,21 +426,21 @@ export class Socket extends Emitter {
socket: this, socket: this,
hostname: this.hostname, hostname: this.hostname,
secure: this.secure, secure: this.secure,
port: this.port port: this.port,
} }
) )
debug("options: %j", JSON.stringify(opts)) debug("options: %j", opts)
debug("new func", transports[name])
return new transports[name](opts) return new transports[name](opts)
} }
/** /**
* Initializes transport to use and starts probe. * Initializes transport to use and starts probe.
* *
* @api private * @private
*/ */
open() { private open() {
let transport let transport
if ( if (
this.opts.rememberUpgrade && this.opts.rememberUpgrade &&
@@ -180,7 +451,7 @@ export class Socket extends Emitter {
} else if (0 === this.transports.length) { } else if (0 === this.transports.length) {
// Emit error on next tick so it can be listened to // Emit error on next tick so it can be listened to
this.setTimeoutFn(() => { this.setTimeoutFn(() => {
this.emit("error", "No transports available") this.emitReserved("error", "No transports available")
}, 0) }, 0)
return return
} else { } else {
@@ -191,8 +462,8 @@ export class Socket extends Emitter {
// Retry with the next transport if the transport is disabled (jsonp: false) // Retry with the next transport if the transport is disabled (jsonp: false)
try { try {
transport = this.createTransport(transport) transport = this.createTransport(transport)
} catch (error: any) { } catch (e) {
debug("error while creating transport: %s", error) debug("error while creating transport: %s", e)
this.transports.shift() this.transports.shift()
this.open() this.open()
return return
@@ -205,9 +476,9 @@ export class Socket extends Emitter {
/** /**
* Sets the current transport. Disables the existing one (if any). * Sets the current transport. Disables the existing one (if any).
* *
* @api private * @private
*/ */
setTransport(transport) { private setTransport(transport) {
debug("setting transport %s", transport.name) debug("setting transport %s", transport.name)
if (this.transport) { if (this.transport) {
@@ -223,9 +494,7 @@ export class Socket extends Emitter {
.on("drain", this.onDrain.bind(this)) .on("drain", this.onDrain.bind(this))
.on("packet", this.onPacket.bind(this)) .on("packet", this.onPacket.bind(this))
.on("error", this.onError.bind(this)) .on("error", this.onError.bind(this))
.on("close", () => { .on("close", (reason) => this.onClose("transport close", reason))
this.onClose("transport close")
})
} }
/** /**
@@ -234,9 +503,9 @@ export class Socket extends Emitter {
* @param {String} transport name * @param {String} transport name
* @api private * @api private
*/ */
probe(name) { private probe(name) {
debug('probing transport "%s"', name) debug('probing transport "%s"', name)
let transport = this.createTransport(name, { probe: 1 }) let transport = this.createTransport(name)
let failed = false let failed = false
Socket.priorWebsocketSuccess = false Socket.priorWebsocketSuccess = false
@@ -251,7 +520,7 @@ export class Socket extends Emitter {
if ("pong" === msg.type && "probe" === msg.data) { if ("pong" === msg.type && "probe" === msg.data) {
debug('probe transport "%s" pong', name) debug('probe transport "%s" pong', name)
this.upgrading = true this.upgrading = true
this.emit("upgrading", transport) this.emitReserved("upgrading", transport)
if (!transport) return if (!transport) return
Socket.priorWebsocketSuccess = "websocket" === transport.name Socket.priorWebsocketSuccess = "websocket" === transport.name
@@ -265,16 +534,17 @@ export class Socket extends Emitter {
this.setTransport(transport) this.setTransport(transport)
transport.send([{ type: "upgrade" }]) transport.send([{ type: "upgrade" }])
this.emit("upgrade", transport) this.emitReserved("upgrade", transport)
transport = null transport = null
this.upgrading = false this.upgrading = false
this.flush() this.flush()
}) })
} else { } else {
debug('probe transport "%s" failed', name) debug('probe transport "%s" failed', name)
const err: any = new Error("probe error") const err = new Error("probe error")
// @ts-ignore
err.transport = transport.name err.transport = transport.name
this.emit("upgradeError", err) this.emitReserved("upgradeError", err)
} }
}) })
} }
@@ -293,14 +563,15 @@ export class Socket extends Emitter {
// Handle any error that happens while probing // Handle any error that happens while probing
const onerror = err => { const onerror = err => {
const error: any = new Error("probe error: " + err) const error = new Error("probe error: " + err)
// @ts-ignore
error.transport = transport.name error.transport = transport.name
freezeTransport() freezeTransport()
debug('probe transport "%s" failed because of error: %s', name, err) debug('probe transport "%s" failed because of error: %s', name, err)
this.emit("upgradeError", error) this.emitReserved("upgradeError", error)
} }
function onTransportClose() { function onTransportClose() {
@@ -325,8 +596,8 @@ export class Socket extends Emitter {
transport.removeListener("open", onTransportOpen) transport.removeListener("open", onTransportOpen)
transport.removeListener("error", onerror) transport.removeListener("error", onerror)
transport.removeListener("close", onTransportClose) transport.removeListener("close", onTransportClose)
this.removeListener("close", onclose) this.off("close", onclose)
this.removeListener("upgrading", onupgrade) this.off("upgrading", onupgrade)
} }
transport.once("open", onTransportOpen) transport.once("open", onTransportOpen)
@@ -342,13 +613,13 @@ export class Socket extends Emitter {
/** /**
* Called when connection is deemed open. * Called when connection is deemed open.
* *
* @api public * @api private
*/ */
onOpen() { private onOpen() {
debug("socket open") debug("socket open")
this.readyState = "open" this.readyState = "open"
Socket.priorWebsocketSuccess = "websocket" === this.transport.name Socket.priorWebsocketSuccess = "websocket" === this.transport.name
this.emit("open") this.emitReserved("open")
this.flush() this.flush()
// we check for `readyState` in case an `open` // we check for `readyState` in case an `open`
@@ -372,7 +643,7 @@ export class Socket extends Emitter {
* *
* @api private * @api private
*/ */
onPacket(packet) { private onPacket(packet) {
if ( if (
"opening" === this.readyState || "opening" === this.readyState ||
"open" === this.readyState || "open" === this.readyState ||
@@ -380,10 +651,10 @@ export class Socket extends Emitter {
) { ) {
debug('socket receive: type "%s", data "%s"', packet.type, packet.data) debug('socket receive: type "%s", data "%s"', packet.type, packet.data)
this.emit("packet", packet) this.emitReserved("packet", packet)
// Socket is live - any packet counts // Socket is live - any packet counts
this.emit("heartbeat") this.emitReserved("heartbeat")
switch (packet.type) { switch (packet.type) {
case "open": case "open":
@@ -393,19 +664,20 @@ export class Socket extends Emitter {
case "ping": case "ping":
this.resetPingTimeout() this.resetPingTimeout()
this.sendPacket("pong") this.sendPacket("pong")
this.emit("ping") this.emitReserved("ping")
this.emit("pong") this.emitReserved("pong")
break break
case "error": case "error":
const err: any = new Error("server error") const err = new Error("server error")
// @ts-ignore
err.code = packet.data err.code = packet.data
this.onError(err) this.onError(err)
break break
case "message": case "message":
this.emit("data", packet.data) this.emitReserved("data", packet.data)
this.emit("message", packet.data) this.emitReserved("message", packet.data)
break break
} }
} else { } else {
@@ -416,16 +688,17 @@ export class Socket extends Emitter {
/** /**
* Called upon handshake completion. * Called upon handshake completion.
* *
* @param {Object} handshake obj * @param {Object} data - handshake obj
* @api private * @api private
*/ */
onHandshake(data) { private onHandshake(data) {
this.emit("handshake", data) this.emitReserved("handshake", data)
this.id = data.sid this.id = data.sid
this.transport.query.sid = data.sid this.transport.query.sid = data.sid
this.upgrades = this.filterUpgrades(data.upgrades) this.upgrades = this.filterUpgrades(data.upgrades)
this.pingInterval = data.pingInterval this.pingInterval = data.pingInterval
this.pingTimeout = data.pingTimeout this.pingTimeout = data.pingTimeout
this.maxPayload = data.maxPayload
this.onOpen() this.onOpen()
// In case open handler closes socket // In case open handler closes socket
if ("closed" === this.readyState) return if ("closed" === this.readyState) return
@@ -437,7 +710,7 @@ export class Socket extends Emitter {
* *
* @api private * @api private
*/ */
resetPingTimeout() { private resetPingTimeout() {
this.clearTimeoutFn(this.pingTimeoutTimer) this.clearTimeoutFn(this.pingTimeoutTimer)
this.pingTimeoutTimer = this.setTimeoutFn(() => { this.pingTimeoutTimer = this.setTimeoutFn(() => {
this.onClose("ping timeout") this.onClose("ping timeout")
@@ -452,7 +725,7 @@ export class Socket extends Emitter {
* *
* @api private * @api private
*/ */
onDrain() { private onDrain() {
this.writeBuffer.splice(0, this.prevBufferLen) this.writeBuffer.splice(0, this.prevBufferLen)
// setting prevBufferLen = 0 is very important // setting prevBufferLen = 0 is very important
@@ -461,7 +734,7 @@ export class Socket extends Emitter {
this.prevBufferLen = 0 this.prevBufferLen = 0
if (0 === this.writeBuffer.length) { if (0 === this.writeBuffer.length) {
this.emit("drain") this.emitReserved("drain")
} else { } else {
this.flush() this.flush()
} }
@@ -472,22 +745,53 @@ export class Socket extends Emitter {
* *
* @api private * @api private
*/ */
flush() { private flush() {
if ( if (
"closed" !== this.readyState && "closed" !== this.readyState &&
this.transport.writable && this.transport.writable &&
!this.upgrading && !this.upgrading &&
this.writeBuffer.length this.writeBuffer.length
) { ) {
debug("flushing %d packets in socket", this.writeBuffer.length) const packets = this.getWritablePackets()
this.transport.send(this.writeBuffer) debug("flushing %d packets in socket", packets.length)
this.transport.send(packets)
// keep track of current length of writeBuffer // keep track of current length of writeBuffer
// splice writeBuffer and callbackBuffer on `drain` // splice writeBuffer and callbackBuffer on `drain`
this.prevBufferLen = this.writeBuffer.length this.prevBufferLen = packets.length
this.emit("flush") this.emitReserved("flush")
} }
} }
/**
* Ensure the encoded size of the writeBuffer is below the maxPayload value sent by the server (only for HTTP
* long-polling)
*
* @private
*/
private getWritablePackets() {
const shouldCheckPayloadSize =
this.maxPayload &&
this.transport.name === "polling" &&
this.writeBuffer.length > 1
if (!shouldCheckPayloadSize) {
return this.writeBuffer
}
let payloadSize = 1 // first packet type
for (let i = 0; i < this.writeBuffer.length; i++) {
const data = this.writeBuffer[i].data
if (data) {
payloadSize += byteLength(data)
}
if (i > 0 && payloadSize > this.maxPayload) {
debug("only send %d out of %d packets", i, this.writeBuffer.length)
return this.writeBuffer.slice(0, i)
}
payloadSize += 2 // separator + packet type
}
debug("payload size is %d (max: %d)", payloadSize, this.maxPayload)
return this.writeBuffer
}
/** /**
* Sends a message. * Sends a message.
* *
@@ -497,12 +801,12 @@ export class Socket extends Emitter {
* @return {Socket} for chaining. * @return {Socket} for chaining.
* @api public * @api public
*/ */
write(msg, options, fn) { public write(msg, options, fn?) {
this.sendPacket("message", msg, options, fn) this.sendPacket("message", msg, options, fn)
return this return this
} }
send(msg, options, fn) { public send(msg, options, fn?) {
this.sendPacket("message", msg, options, fn) this.sendPacket("message", msg, options, fn)
return this return this
} }
@@ -516,7 +820,7 @@ export class Socket extends Emitter {
* @param {Function} callback function. * @param {Function} callback function.
* @api private * @api private
*/ */
sendPacket(type, data?, options?, fn?) { private sendPacket(type, data?, options?, fn?) {
if ("function" === typeof data) { if ("function" === typeof data) {
fn = data fn = data
data = undefined data = undefined
@@ -539,7 +843,7 @@ export class Socket extends Emitter {
data: data, data: data,
options: options options: options
} }
this.emit("packetCreate", packet) this.emitReserved("packetCreate", packet)
this.writeBuffer.push(packet) this.writeBuffer.push(packet)
if (fn) this.once("flush", fn) if (fn) this.once("flush", fn)
this.flush() this.flush()
@@ -548,9 +852,9 @@ export class Socket extends Emitter {
/** /**
* Closes the connection. * Closes the connection.
* *
* @api private * @api public
*/ */
close() { public close() {
const close = () => { const close = () => {
this.onClose("forced close") this.onClose("forced close")
debug("socket closing - telling transport to close") debug("socket closing - telling transport to close")
@@ -558,8 +862,8 @@ export class Socket extends Emitter {
} }
const cleanupAndClose = () => { const cleanupAndClose = () => {
this.removeListener("upgrade", cleanupAndClose) this.off("upgrade", cleanupAndClose)
this.removeListener("upgradeError", cleanupAndClose) this.off("upgradeError", cleanupAndClose)
close() close()
} }
@@ -595,10 +899,10 @@ export class Socket extends Emitter {
* *
* @api private * @api private
*/ */
onError(err) { private onError(err) {
debug("socket error %j", err) debug("socket error %j", err)
Socket.priorWebsocketSuccess = false Socket.priorWebsocketSuccess = false
this.emit("error", err) this.emitReserved("error", err)
this.onClose("transport error", err) this.onClose("transport error", err)
} }
@@ -607,7 +911,7 @@ export class Socket extends Emitter {
* *
* @api private * @api private
*/ */
onClose(reason, desc?) { private onClose(reason: string, description?: CloseDetails | Error) {
if ( if (
"opening" === this.readyState || "opening" === this.readyState ||
"open" === this.readyState || "open" === this.readyState ||
@@ -616,7 +920,6 @@ export class Socket extends Emitter {
debug('socket close with reason: "%s"', reason) debug('socket close with reason: "%s"', reason)
// clear timers // clear timers
this.clearTimeoutFn(this.pingIntervalTimer)
this.clearTimeoutFn(this.pingTimeoutTimer) this.clearTimeoutFn(this.pingTimeoutTimer)
// stop event from firing again for transport // stop event from firing again for transport
@@ -629,6 +932,11 @@ export class Socket extends Emitter {
this.transport.removeAllListeners() this.transport.removeAllListeners()
if (typeof removeEventListener === "function") { if (typeof removeEventListener === "function") {
removeEventListener(
"beforeunload",
this.beforeunloadEventListener,
false
)
removeEventListener("offline", this.offlineEventListener, false) removeEventListener("offline", this.offlineEventListener, false)
} }
@@ -639,7 +947,7 @@ export class Socket extends Emitter {
this.id = null this.id = null
// emit close event // emit close event
this.emit("close", reason, desc) this.emitReserved("close", reason, description)
// clean buffers after, so users can still // clean buffers after, so users can still
// grab the buffers on `close` event // grab the buffers on `close` event
@@ -655,7 +963,7 @@ export class Socket extends Emitter {
* @api private * @api private
* *
*/ */
filterUpgrades(upgrades) { private filterUpgrades(upgrades) {
const filteredUpgrades = [] const filteredUpgrades = []
let i = 0 let i = 0
const j = upgrades.length const j = upgrades.length
@@ -666,23 +974,3 @@ export class Socket extends Emitter {
return filteredUpgrades return filteredUpgrades
} }
} }
Socket.priorWebsocketSuccess = false
/**
* Protocol version.
*
* @api public
*/
Socket.protocol = parser.protocol // this is an int
function clone(obj) {
const o = {}
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
o[i] = obj[i]
}
}
return o
}

View File

@@ -1,61 +1,103 @@
import parser from "../engine.io-parser" // import { decodePacket, Packet, RawData } from "engine.io-parser"
const Emitter = require("component-emitter") import { decodePacket, Packet, RawData } from "../engine.io-parser"
import { Emitter } from "@socket.io/component-emitter"
import { installTimerFunctions } from "./util" import { installTimerFunctions } from "./util"
const debug = (...args: any) => console.debug('engine.io-client:transport', ...args)//require("debug")("engine.io-client:transport") // import debugModule from "debug"; // debug()
import { SocketOptions } from "./socket"
// const debug = debugModule("engine.io-client:transport"); // debug()
const debug = require('../debug')("engine.io-client:transport") // debug()
class TransportError extends Error {
public readonly type = "TransportError";
constructor(
reason: string,
readonly description: any,
readonly context: any
) {
super(reason)
}
}
export interface CloseDetails {
description: string
context?: unknown // context should be typed as CloseEvent | XMLHttpRequest, but these types are not available on non-browser platforms
}
interface TransportReservedEvents {
open: () => void
error: (err: TransportError) => void
packet: (packet: Packet) => void
close: (details?: CloseDetails) => void
poll: () => void
pollComplete: () => void
drain: () => void
}
type TransportState = "opening" | "open" | "closed" | "pausing" | "paused"
export abstract class Transport extends Emitter<
Record<never, never>,
Record<never, never>,
TransportReservedEvents
> {
public query: Record<string, string>
public writable: boolean = false;
protected opts: SocketOptions
protected supportsBinary: boolean
protected readyState: TransportState
protected socket: any
protected setTimeoutFn: typeof setTimeout
export class Transport extends Emitter {
/** /**
* Transport abstract constructor. * Transport abstract constructor.
* *
* @param {Object} options. * @param {Object} opts - options
* @api private * @protected
*/ */
constructor(opts) { constructor(opts) {
super() super()
installTimerFunctions(this, opts) installTimerFunctions(this, opts)
this.opts = opts this.opts = opts
this.query = opts.query this.query = opts.query
this.readyState = ""
this.socket = opts.socket this.socket = opts.socket
} }
/** /**
* Emits an error. * Emits an error.
* *
* @param {String} str * @param {String} reason
* @param description
* @param context - the error context
* @return {Transport} for chaining * @return {Transport} for chaining
* @api public * @protected
*/ */
onError(msg, desc) { protected onError(reason: string, description: any, context?: any) {
const err: any = new Error(msg) super.emitReserved(
err.type = "TransportError" "error",
err.description = desc new TransportError(reason, description, context)
this.emit("error", err) )
return this return this
} }
/** /**
* Opens the transport. * Opens the transport.
*
* @api public
*/ */
open() { public open() {
if ("closed" === this.readyState || "" === this.readyState) { this.readyState = "opening"
this.readyState = "opening" this.doOpen()
this.doOpen()
}
return this return this
} }
/** /**
* Closes the transport. * Closes the transport.
*
* @api private
*/ */
close() { public close() {
if ("opening" === this.readyState || "open" === this.readyState) { if (this.readyState === "opening" || this.readyState === "open") {
this.doClose() this.doClose()
this.onClose() this.onClose()
} }
@@ -67,10 +109,9 @@ export class Transport extends Emitter {
* Sends multiple packets. * Sends multiple packets.
* *
* @param {Array} packets * @param {Array} packets
* @api private
*/ */
send(packets) { public send(packets) {
if ("open" === this.readyState) { if (this.readyState === "open") {
this.write(packets) this.write(packets)
} else { } else {
// this might happen if the transport was silently closed in the beforeunload event handler // this might happen if the transport was silently closed in the beforeunload event handler
@@ -81,39 +122,58 @@ export class Transport extends Emitter {
/** /**
* Called upon open * Called upon open
* *
* @api private * @protected
*/ */
onOpen() { protected onOpen() {
this.readyState = "open" this.readyState = "open"
this.writable = true this.writable = true
this.emit("open") super.emitReserved("open")
} }
/** /**
* Called with data. * Called with data.
* *
* @param {String} data * @param {String} data
* @api private * @protected
*/ */
onData(data) { protected onData(data: RawData) {
const packet = parser.decodePacket(data, this.socket.binaryType) const packet = decodePacket(data, this.socket.binaryType)
this.onPacket(packet) this.onPacket(packet)
} }
/** /**
* Called with a decoded packet. * Called with a decoded packet.
*
* @protected
*/ */
onPacket(packet) { protected onPacket(packet: Packet) {
this.emit("packet", packet) super.emitReserved("packet", packet)
} }
/** /**
* Called upon close. * Called upon close.
* *
* @api private * @protected
*/ */
onClose() { protected onClose(details?: CloseDetails) {
this.readyState = "closed" this.readyState = "closed"
this.emit("close") super.emitReserved("close", details)
} }
/**
* The name of the transport
*/
public abstract get name(): string
/**
* Pauses the transport, in order not to lose packets during an upgrade.
*
* @param onPause
*/
public pause(onPause: () => void) { }
protected abstract doOpen()
protected abstract doClose()
protected abstract write(packets: Packet[])
} }

View File

@@ -1,4 +1,5 @@
import { WS } from "./websocket" import { WS } from "./websocket.js"
export default {
'websocket': WS export const transports = {
websocket: WS,
} }

View File

@@ -0,0 +1,6 @@
import { WebSocket as ws } from "../../client"
export const WebSocket = ws
export const usingBrowserWebSocket = false
export const defaultBinaryType = "nodebuffer"
export const nextTick = process.nextTick

View File

@@ -1,21 +1,18 @@
import { Transport } from '../transport' import { Transport } from "../transport"
// const Transport = require("../transport") import { encode } from "../contrib/parseqs"
import parser from '../../engine.io-parser' import { yeast } from "../contrib/yeast"
// const parser = require("../engine.io-parser") import { pick } from "../util"
const parseqs = require("parseqs") import {
const yeast = require("yeast") defaultBinaryType,
import { pick } from '../util' nextTick,
// const { pick } = require("../util") usingBrowserWebSocket,
import { WebSocket } from '../../client' WebSocket
const usingBrowserWebSocket = true } from "./websocket-constructor"
// const { // import debugModule from "debug" // debug()
// WebSocket, import { encodePacket } from "../../engine.io-parser"
// usingBrowserWebSocket,
// defaultBinaryType,
// nextTick
// } = require("./websocket-constructor")
const debug = (...args: any) => console.debug('engine.io-client:websocket', ...args)//require("debug")("engine.io-client:websocket") // const debug = debugModule("engine.io-client:websocket") // debug()
const debug = (...args: any) => console.debug('engine.io-client:websocket', ...args)
// detect ReactNative environment // detect ReactNative environment
const isReactNative = const isReactNative =
@@ -24,11 +21,13 @@ const isReactNative =
navigator.product.toLowerCase() === "reactnative" navigator.product.toLowerCase() === "reactnative"
export class WS extends Transport { export class WS extends Transport {
private ws: any
/** /**
* WebSocket transport constructor. * WebSocket transport constructor.
* *
* @api {Object} connection options * @param {Object} opts - connection options
* @api public * @protected
*/ */
constructor(opts) { constructor(opts) {
super(opts) super(opts)
@@ -36,21 +35,11 @@ export class WS extends Transport {
this.supportsBinary = !opts.forceBase64 this.supportsBinary = !opts.forceBase64
} }
/** override get name() {
* Transport name.
*
* @api public
*/
get name() {
return "websocket" return "websocket"
} }
/** override doOpen() {
* Opens socket.
*
* @api private
*/
doOpen() {
if (!this.check()) { if (!this.check()) {
// let probe timeout // let probe timeout
return return
@@ -86,17 +75,17 @@ export class WS extends Transport {
} }
try { try {
this.ws = new WebSocket(uri, protocols) this.ws =
// usingBrowserWebSocket && !isReactNative usingBrowserWebSocket && !isReactNative
// ? protocols ? protocols
// ? new WebSocket(uri, protocols) ? new WebSocket(uri, protocols)
// : new WebSocket(uri) : new WebSocket(uri)
// : new WebSocket(uri, protocols, opts) : new WebSocket(uri, protocols, opts)
} catch (err) { } catch (err: any) {
return this.emit("error", err) return this.emitReserved("error", err)
} }
this.ws.binaryType = this.socket.binaryType || 'arraybuffer' this.ws.binaryType = this.socket.binaryType || defaultBinaryType
this.addEventListeners() this.addEventListeners()
} }
@@ -104,27 +93,25 @@ export class WS extends Transport {
/** /**
* Adds event listeners to the socket * Adds event listeners to the socket
* *
* @api private * @private
*/ */
addEventListeners() { private addEventListeners() {
this.ws.onopen = () => { this.ws.onopen = () => {
if (this.opts.autoUnref) { if (this.opts.autoUnref) {
this.ws._socket.unref() this.ws._socket.unref()
} }
this.onOpen() this.onOpen()
} }
this.ws.onclose = this.onClose.bind(this) this.ws.onclose = (closeEvent) =>
this.ws.onmessage = ev => this.onData(ev.data) this.onClose({
this.ws.onerror = e => this.onError("websocket error", e) description: "websocket connection closed",
context: closeEvent,
})
this.ws.onmessage = (ev) => this.onData(ev.data)
this.ws.onerror = (e) => this.onError("websocket error", e)
} }
/** override write(packets) {
* Writes data to socket.
*
* @param {Array} array of packets.
* @api private
*/
write(packets) {
this.writable = false this.writable = false
// encodePacket efficient as it uses WS framing // encodePacket efficient as it uses WS framing
@@ -133,9 +120,9 @@ export class WS extends Transport {
const packet = packets[i] const packet = packets[i]
const lastPacket = i === packets.length - 1 const lastPacket = i === packets.length - 1
parser.encodePacket(packet, this.supportsBinary, data => { encodePacket(packet, this.supportsBinary, (data) => {
// always create a new object (GH-437) // always create a new object (GH-437)
const opts: any = {} const opts: { compress?: boolean } = {}
if (!usingBrowserWebSocket) { if (!usingBrowserWebSocket) {
if (packet.options) { if (packet.options) {
opts.compress = packet.options.compress opts.compress = packet.options.compress
@@ -143,6 +130,7 @@ export class WS extends Transport {
if (this.opts.perMessageDeflate) { if (this.opts.perMessageDeflate) {
const len = const len =
// @ts-ignore
"string" === typeof data ? Buffer.byteLength(data) : data.length "string" === typeof data ? Buffer.byteLength(data) : data.length
if (len < this.opts.perMessageDeflate.threshold) { if (len < this.opts.perMessageDeflate.threshold) {
opts.compress = false opts.compress = false
@@ -160,37 +148,23 @@ export class WS extends Transport {
} else { } else {
this.ws.send(data, opts) this.ws.send(data, opts)
} }
} catch (error: any) { } catch (e) {
debug("websocket closed before onclose event") debug("websocket closed before onclose event")
} }
if (lastPacket) { if (lastPacket) {
// fake drain // fake drain
// defer to next tick to allow Socket to clear writeBuffer // defer to next tick to allow Socket to clear writeBuffer
process.nextTick(() => { nextTick(() => {
this.writable = true this.writable = true
this.emit("drain") this.emitReserved("drain")
}, this.setTimeoutFn) }, this.setTimeoutFn)
} }
}) })
} }
} }
/** override doClose() {
* Called upon close
*
* @api private
*/
onClose() {
Transport.prototype.onClose.call(this)
}
/**
* Closes socket.
*
* @api private
*/
doClose() {
if (typeof this.ws !== "undefined") { if (typeof this.ws !== "undefined") {
this.ws.close() this.ws.close()
this.ws = null this.ws = null
@@ -200,10 +174,10 @@ export class WS extends Transport {
/** /**
* Generates uri for connection. * Generates uri for connection.
* *
* @api private * @private
*/ */
uri() { uri() {
let query = this.query || {} let query: { b64?: number } = this.query || {}
const schema = this.opts.secure ? "wss" : "ws" const schema = this.opts.secure ? "wss" : "ws"
let port = "" let port = ""
@@ -226,21 +200,16 @@ export class WS extends Transport {
query.b64 = 1 query.b64 = 1
} }
query = parseqs.encode(query) const encodedQuery = encode(query)
// prepend ? to query
if (query.length) {
query = "?" + query
}
const ipv6 = this.opts.hostname.indexOf(":") !== -1 const ipv6 = this.opts.hostname.indexOf(":") !== -1
return ( return (
schema + schema +
"://" + "://" +
(ipv6 ? "[" + this.opts.hostname + "]" : this.opts.hostname) + (ipv6 ? "[" + this.opts.hostname + "]" : this.opts.hostname) +
port + port +
this.opts.path + this.opts.path +
query (encodedQuery.length ? "?" + encodedQuery : "")
) )
} }
@@ -248,12 +217,9 @@ export class WS extends Transport {
* Feature detection for WebSocket. * Feature detection for WebSocket.
* *
* @return {Boolean} whether this transport is available. * @return {Boolean} whether this transport is available.
* @api public * @private
*/ */
check() { private check() {
return ( return !!WebSocket
!!WebSocket &&
!("__initialize" in WebSocket && this.name === WS.prototype.name)
)
} }
} }

View File

@@ -1,4 +1,6 @@
const pick = (obj, ...attr) => { // import { globalThisShim as globalThis } from "./globalThis.js"
export function pick(obj, ...attr) {
return attr.reduce((acc, k) => { return attr.reduce((acc, k) => {
if (obj.hasOwnProperty(k)) { if (obj.hasOwnProperty(k)) {
acc[k] = obj[k] acc[k] = obj[k]
@@ -8,16 +10,46 @@ const pick = (obj, ...attr) => {
} }
// Keep a reference to the real timeout functions so they can be used when overridden // Keep a reference to the real timeout functions so they can be used when overridden
const NATIVE_SET_TIMEOUT = setTimeout const NATIVE_SET_TIMEOUT = globalThis.setTimeout
const NATIVE_CLEAR_TIMEOUT = clearTimeout const NATIVE_CLEAR_TIMEOUT = globalThis.clearTimeout
const installTimerFunctions = (obj, opts) => { export function installTimerFunctions(obj, opts) {
if (opts.useNativeTimers) { if (opts.useNativeTimers) {
obj.setTimeoutFn = NATIVE_SET_TIMEOUT.bind(globalThis) obj.setTimeoutFn = NATIVE_SET_TIMEOUT.bind(globalThis)
obj.clearTimeoutFn = NATIVE_CLEAR_TIMEOUT.bind(globalThis) obj.clearTimeoutFn = NATIVE_CLEAR_TIMEOUT.bind(globalThis)
} else { } else {
obj.setTimeoutFn = setTimeout.bind(globalThis) obj.setTimeoutFn = globalThis.setTimeout.bind(globalThis)
obj.clearTimeoutFn = clearTimeout.bind(globalThis) obj.clearTimeoutFn = globalThis.clearTimeout.bind(globalThis)
} }
} }
export { pick, installTimerFunctions }
// base64 encoded buffers are about 33% bigger (https://en.wikipedia.org/wiki/Base64)
const BASE64_OVERHEAD = 1.33
// we could also have used `new Blob([obj]).size`, but it isn't supported in IE9
export function byteLength(obj) {
if (typeof obj === "string") {
return utf8Length(obj)
}
// arraybuffer or blob
return Math.ceil((obj.byteLength || obj.size) * BASE64_OVERHEAD)
}
function utf8Length(str) {
let c = 0,
length = 0
for (let i = 0, l = str.length; i < l; i++) {
c = str.charCodeAt(i)
if (c < 0x80) {
length += 1
} else if (c < 0x800) {
length += 2
} else if (c < 0xd800 || c >= 0xe000) {
length += 3
} else {
i++
length += 4
}
}
return length
}

View File

@@ -1,21 +1,39 @@
const PACKET_TYPES = Object.create(null) // no Map = no polyfill const PACKET_TYPES = Object.create(null); // no Map = no polyfill
PACKET_TYPES["open"] = "0" PACKET_TYPES["open"] = "0";
PACKET_TYPES["close"] = "1" PACKET_TYPES["close"] = "1";
PACKET_TYPES["ping"] = "2" PACKET_TYPES["ping"] = "2";
PACKET_TYPES["pong"] = "3" PACKET_TYPES["pong"] = "3";
PACKET_TYPES["message"] = "4" PACKET_TYPES["message"] = "4";
PACKET_TYPES["upgrade"] = "5" PACKET_TYPES["upgrade"] = "5";
PACKET_TYPES["noop"] = "6" PACKET_TYPES["noop"] = "6";
const PACKET_TYPES_REVERSE = Object.create(null) const PACKET_TYPES_REVERSE = Object.create(null);
Object.keys(PACKET_TYPES).forEach(key => { Object.keys(PACKET_TYPES).forEach(key => {
PACKET_TYPES_REVERSE[PACKET_TYPES[key]] = key PACKET_TYPES_REVERSE[PACKET_TYPES[key]] = key;
}) });
const ERROR_PACKET = { type: "error", data: "parser error" } const ERROR_PACKET: Packet = { type: "error", data: "parser error" };
export = { export { PACKET_TYPES, PACKET_TYPES_REVERSE, ERROR_PACKET };
PACKET_TYPES,
PACKET_TYPES_REVERSE, export type PacketType =
ERROR_PACKET | "open"
| "close"
| "ping"
| "pong"
| "message"
| "upgrade"
| "noop"
| "error";
// RawData should be "string | Buffer | ArrayBuffer | ArrayBufferView | Blob", but Blob does not exist in Node.js and
// requires to add the dom lib in tsconfig.json
export type RawData = any;
export interface Packet {
type: PacketType;
options?: { compress: boolean };
data?: RawData;
} }
export type BinaryType = "nodebuffer" | "arraybuffer" | "blob";

View File

@@ -1,48 +1,60 @@
const { PACKET_TYPES_REVERSE, ERROR_PACKET } = require("./commons") import {
ERROR_PACKET,
PACKET_TYPES_REVERSE,
Packet,
BinaryType,
RawData
} from "./commons.js";
export const decodePacket = (encodedPacket, binaryType) => { const decodePacket = (
if (typeof encodedPacket !== "string") { encodedPacket: RawData,
return { binaryType?: BinaryType
type: "message", ): Packet => {
data: mapBinary(encodedPacket, binaryType) if (typeof encodedPacket !== "string") {
} return {
} type: "message",
const type = encodedPacket.charAt(0) data: mapBinary(encodedPacket, binaryType)
if (type === "b") { };
const buffer = Buffer.from(encodedPacket.substring(1), "base64") }
return { const type = encodedPacket.charAt(0);
type: "message", if (type === "b") {
data: mapBinary(buffer, binaryType) const buffer = Buffer.from(encodedPacket.substring(1), "base64");
} return {
} type: "message",
if (!PACKET_TYPES_REVERSE[type]) { data: mapBinary(buffer, binaryType)
return ERROR_PACKET };
} }
return encodedPacket.length > 1 if (!PACKET_TYPES_REVERSE[type]) {
? { return ERROR_PACKET;
type: PACKET_TYPES_REVERSE[type], }
data: encodedPacket.substring(1) return encodedPacket.length > 1
} ? {
: { type: PACKET_TYPES_REVERSE[type],
type: PACKET_TYPES_REVERSE[type] data: encodedPacket.substring(1)
} }
} : {
type: PACKET_TYPES_REVERSE[type]
};
};
const mapBinary = (data, binaryType) => { const mapBinary = (data: RawData, binaryType?: BinaryType) => {
switch (binaryType) { const isBuffer = Buffer.isBuffer(data);
case "arraybuffer": switch (binaryType) {
return Buffer.isBuffer(data) ? toArrayBuffer(data) : data case "arraybuffer":
case "nodebuffer": return isBuffer ? toArrayBuffer(data) : data;
default: case "nodebuffer":
return data // assuming the data is already a Buffer default:
} return data; // assuming the data is already a Buffer
} }
};
const toArrayBuffer = buffer => { const toArrayBuffer = (buffer: Buffer): ArrayBuffer => {
const arrayBuffer = new ArrayBuffer(buffer.length) const arrayBuffer = new ArrayBuffer(buffer.length);
const view = new Uint8Array(arrayBuffer) const view = new Uint8Array(arrayBuffer);
for (let i = 0; i < buffer.length; i++) { for (let i = 0; i < buffer.length; i++) {
view[i] = buffer[i] view[i] = buffer[i];
} }
return arrayBuffer return arrayBuffer;
} };
export default decodePacket;

View File

@@ -1,26 +1,31 @@
const { PACKET_TYPES } = require("./commons") import { PACKET_TYPES, Packet, RawData } from "./commons.js";
export const encodePacket = ({ type, data }, supportsBinary, callback) => { const encodePacket = (
console.trace('encodePacket', type, JSON.stringify(data)) { type, data }: Packet,
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) { supportsBinary: boolean,
const buffer = toBuffer(data) callback: (encodedPacket: RawData) => void
return callback(encodeBuffer(buffer, supportsBinary)) ) => {
} if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
// plain string const buffer = toBuffer(data);
return callback(PACKET_TYPES[type] + (data || "")) return callback(encodeBuffer(buffer, supportsBinary));
} }
// plain string
return callback(PACKET_TYPES[type] + (data || ""));
};
const toBuffer = data => { const toBuffer = data => {
if (Buffer.isBuffer(data)) { if (Buffer.isBuffer(data)) {
return data return data;
} else if (data instanceof ArrayBuffer) { } else if (data instanceof ArrayBuffer) {
return Buffer.from(data) return Buffer.from(data);
} else { } else {
return Buffer.from(data.buffer, data.byteOffset, data.byteLength) return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
} }
} };
// only 'message' packets can contain binary, so the type prefix is not needed // only 'message' packets can contain binary, so the type prefix is not needed
const encodeBuffer = (data, supportsBinary) => { const encodeBuffer = (data: Buffer, supportsBinary: boolean): RawData => {
return supportsBinary ? data : "b" + data.toString("base64") return supportsBinary ? data : "b" + data.toString("base64");
} };
export default encodePacket;

View File

@@ -1,42 +1,53 @@
import { encodePacket } from "./encodePacket" import encodePacket from "./encodePacket.js";
import { decodePacket } from "./decodePacket" import decodePacket from "./decodePacket.js";
import { Packet, PacketType, RawData, BinaryType } from "./commons.js";
const SEPARATOR = String.fromCharCode(30) // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text const SEPARATOR = String.fromCharCode(30); // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text
const encodePayload = (packets, callback) => { const encodePayload = (
// some packets may be added to the array while encoding, so the initial length must be saved packets: Packet[],
const length = packets.length callback: (encodedPayload: string) => void
const encodedPackets = new Array(length) ) => {
let count = 0 // some packets may be added to the array while encoding, so the initial length must be saved
const length = packets.length;
const encodedPackets = new Array(length);
let count = 0;
packets.forEach((packet, i) => { packets.forEach((packet, i) => {
// force base64 encoding for binary packets // force base64 encoding for binary packets
encodePacket(packet, false, encodedPacket => { encodePacket(packet, false, encodedPacket => {
encodedPackets[i] = encodedPacket encodedPackets[i] = encodedPacket;
if (++count === length) { if (++count === length) {
callback(encodedPackets.join(SEPARATOR)) callback(encodedPackets.join(SEPARATOR));
} }
}) });
}) });
} };
const decodePayload = (encodedPayload, binaryType) => { const decodePayload = (
const encodedPackets = encodedPayload.split(SEPARATOR) encodedPayload: string,
const packets = [] binaryType?: BinaryType
for (let i = 0; i < encodedPackets.length; i++) { ): Packet[] => {
const decodedPacket = decodePacket(encodedPackets[i], binaryType) const encodedPackets = encodedPayload.split(SEPARATOR);
packets.push(decodedPacket) const packets = [];
if (decodedPacket.type === "error") { for (let i = 0; i < encodedPackets.length; i++) {
break const decodedPacket = decodePacket(encodedPackets[i], binaryType);
} packets.push(decodedPacket);
if (decodedPacket.type === "error") {
break;
} }
return packets }
} return packets;
};
export default { export const protocol = 4;
protocol: 4, export {
encodePacket, encodePacket,
encodePayload, encodePayload,
decodePacket, decodePacket,
decodePayload decodePayload,
} Packet,
PacketType,
RawData,
BinaryType
};

View File

@@ -1,10 +1,45 @@
/** // import { createServer } from "http"
* Module dependencies. import { Server, AttachOptions, ServerOptions } from "./server";
*/ import transports from "./transports/index";
import * as parser from "../engine.io-parser";
// const http = require("http") // export { Server, transports, listen, attach, parser }
// const Server = require("./server") export { Server, transports, attach, parser };
import { Server } from './server' export { AttachOptions, ServerOptions } from "./server";
// export { uServer } from "./userver";
export { Socket } from "./socket";
export { Transport } from "./transport";
export const protocol = parser.protocol;
// /**
// * Creates an http.Server exclusively used for WS upgrades.
// *
// * @param {Number} port
// * @param {Function} callback
// * @param {Object} options
// * @return {Server} websocket.io server
// * @api public
// */
//
// function listen(port, options: AttachOptions & ServerOptions, fn) {
// if ("function" === typeof options) {
// fn = options;
// options = {};
// }
// const server = createServer(function(req, res) {
// res.writeHead(501);
// res.end("Not Implemented");
// });
// // create engine server
// const engine = attach(server, options);
// engine.httpServer = server;
// server.listen(port, fn);
// return engine;
// }
/** /**
* Captures upgrade requests for a http.Server. * Captures upgrade requests for a http.Server.
@@ -15,12 +50,8 @@ import { Server } from './server'
* @api public * @api public
*/ */
function attach(srv, options) { function attach(server, options: AttachOptions & ServerOptions) {
const engine = new Server(options) const engine = new Server(options);
engine.attach(srv, options) engine.attach(server, options);
return engine return engine;
}
export = {
attach
} }

View File

@@ -0,0 +1,485 @@
// imported from https://github.com/socketio/engine.io-parser/tree/2.2.x
/**
* Module dependencies.
*/
var utf8 = require('./utf8');
/**
* Current protocol version.
*/
export const protocol = 3;
const hasBinary = (packets) => {
for (const packet of packets) {
if (packet.data instanceof ArrayBuffer || ArrayBuffer.isView(packet.data)) {
return true;
}
}
return false;
}
/**
* Packet types.
*/
export const packets = {
open: 0 // non-ws
, close: 1 // non-ws
, ping: 2
, pong: 3
, message: 4
, upgrade: 5
, noop: 6
};
var packetslist = Object.keys(packets);
/**
* Premade error packet.
*/
var err = { type: 'error', data: 'parser error' };
const EMPTY_BUFFER = Buffer.concat([]);
/**
* Encodes a packet.
*
* <packet type id> [ <data> ]
*
* Example:
*
* 5hello world
* 3
* 4
*
* Binary is encoded in an identical principle
*
* @api private
*/
export function encodePacket (packet, supportsBinary, utf8encode, callback) {
if (typeof supportsBinary === 'function') {
callback = supportsBinary;
supportsBinary = null;
}
if (typeof utf8encode === 'function') {
callback = utf8encode;
utf8encode = null;
}
if (Buffer.isBuffer(packet.data)) {
return encodeBuffer(packet, supportsBinary, callback);
} else if (packet.data && (packet.data.buffer || packet.data) instanceof ArrayBuffer) {
return encodeBuffer({ type: packet.type, data: arrayBufferToBuffer(packet.data) }, supportsBinary, callback);
}
// Sending data as a utf-8 string
var encoded = packets[packet.type];
// data fragment is optional
if (undefined !== packet.data) {
encoded += utf8encode ? utf8.encode(String(packet.data), { strict: false }) : String(packet.data);
}
return callback('' + encoded);
};
/**
* Encode Buffer data
*/
function encodeBuffer(packet, supportsBinary, callback) {
if (!supportsBinary) {
return encodeBase64Packet(packet, callback);
}
var data = packet.data;
var typeBuffer = Buffer.allocUnsafe(1);
typeBuffer[0] = packets[packet.type];
return callback(Buffer.concat([typeBuffer, data]));
}
/**
* Encodes a packet with binary data in a base64 string
*
* @param {Object} packet, has `type` and `data`
* @return {String} base64 encoded message
*/
export function encodeBase64Packet (packet, callback){
var data = Buffer.isBuffer(packet.data) ? packet.data : arrayBufferToBuffer(packet.data);
var message = 'b' + packets[packet.type];
message += data.toString('base64');
return callback(message);
};
/**
* Decodes a packet. Data also available as an ArrayBuffer if requested.
*
* @return {Object} with `type` and `data` (if any)
* @api private
*/
export function decodePacket (data, binaryType, utf8decode) {
if (data === undefined) {
return err;
}
var type;
// String data
if (typeof data === 'string') {
type = data.charAt(0);
if (type === 'b') {
return decodeBase64Packet(data.substr(1), binaryType);
}
if (utf8decode) {
data = tryDecode(data);
if (data === false) {
return err;
}
}
if (Number(type) != type || !packetslist[type]) {
return err;
}
if (data.length > 1) {
return { type: packetslist[type], data: data.substring(1) };
} else {
return { type: packetslist[type] };
}
}
// Binary data
if (binaryType === 'arraybuffer') {
// wrap Buffer/ArrayBuffer data into an Uint8Array
var intArray = new Uint8Array(data);
type = intArray[0];
return { type: packetslist[type], data: intArray.buffer.slice(1) };
}
if (data instanceof ArrayBuffer) {
data = arrayBufferToBuffer(data);
}
type = data[0];
return { type: packetslist[type], data: data.slice(1) };
};
function tryDecode(data) {
try {
data = utf8.decode(data, { strict: false });
} catch (e) {
return false;
}
return data;
}
/**
* Decodes a packet encoded in a base64 string.
*
* @param {String} base64 encoded message
* @return {Object} with `type` and `data` (if any)
*/
export function decodeBase64Packet (msg, binaryType) {
var type = packetslist[msg.charAt(0)];
var data = Buffer.from(msg.substr(1), 'base64');
if (binaryType === 'arraybuffer') {
var abv = new Uint8Array(data.length);
for (var i = 0; i < abv.length; i++){
abv[i] = data[i];
}
// @ts-ignore
data = abv.buffer;
}
return { type: type, data: data };
};
/**
* Encodes multiple messages (payload).
*
* <length>:data
*
* Example:
*
* 11:hello world2:hi
*
* If any contents are binary, they will be encoded as base64 strings. Base64
* encoded strings are marked with a b before the length specifier
*
* @param {Array} packets
* @api private
*/
export function encodePayload (packets, supportsBinary, callback) {
if (typeof supportsBinary === 'function') {
callback = supportsBinary;
supportsBinary = null;
}
if (supportsBinary && hasBinary(packets)) {
return encodePayloadAsBinary(packets, callback);
}
if (!packets.length) {
return callback('0:');
}
function encodeOne(packet, doneCallback) {
encodePacket(packet, supportsBinary, false, function(message) {
doneCallback(null, setLengthHeader(message));
});
}
map(packets, encodeOne, function(err, results) {
return callback(results.join(''));
});
};
function setLengthHeader(message) {
return message.length + ':' + message;
}
/**
* Async array map using after
*/
function map(ary, each, done) {
const results = new Array(ary.length);
let count = 0;
for (let i = 0; i < ary.length; i++) {
each(ary[i], (error, msg) => {
results[i] = msg;
if (++count === ary.length) {
done(null, results);
}
});
}
}
/*
* Decodes data when a payload is maybe expected. Possible binary contents are
* decoded from their base64 representation
*
* @param {String} data, callback method
* @api public
*/
export function decodePayload (data, binaryType, callback) {
if (typeof data !== 'string') {
return decodePayloadAsBinary(data, binaryType, callback);
}
if (typeof binaryType === 'function') {
callback = binaryType;
binaryType = null;
}
if (data === '') {
// parser error - ignoring payload
return callback(err, 0, 1);
}
var length = '', n, msg, packet;
for (var i = 0, l = data.length; i < l; i++) {
var chr = data.charAt(i);
if (chr !== ':') {
length += chr;
continue;
}
// @ts-ignore
if (length === '' || (length != (n = Number(length)))) {
// parser error - ignoring payload
return callback(err, 0, 1);
}
msg = data.substr(i + 1, n);
if (length != msg.length) {
// parser error - ignoring payload
return callback(err, 0, 1);
}
if (msg.length) {
packet = decodePacket(msg, binaryType, false);
if (err.type === packet.type && err.data === packet.data) {
// parser error in individual packet - ignoring payload
return callback(err, 0, 1);
}
var more = callback(packet, i + n, l);
if (false === more) return;
}
// advance cursor
i += n;
length = '';
}
if (length !== '') {
// parser error - ignoring payload
return callback(err, 0, 1);
}
};
/**
*
* Converts a buffer to a utf8.js encoded string
*
* @api private
*/
function bufferToString(buffer) {
var str = '';
for (var i = 0, l = buffer.length; i < l; i++) {
str += String.fromCharCode(buffer[i]);
}
return str;
}
/**
*
* Converts a utf8.js encoded string to a buffer
*
* @api private
*/
function stringToBuffer(string) {
var buf = Buffer.allocUnsafe(string.length);
for (var i = 0, l = string.length; i < l; i++) {
buf.writeUInt8(string.charCodeAt(i), i);
}
return buf;
}
/**
*
* Converts an ArrayBuffer to a Buffer
*
* @api private
*/
function arrayBufferToBuffer(data) {
// data is either an ArrayBuffer or ArrayBufferView.
var length = data.byteLength || data.length;
var offset = data.byteOffset || 0;
return Buffer.from(data.buffer || data, offset, length);
}
/**
* Encodes multiple messages (payload) as binary.
*
* <1 = binary, 0 = string><number from 0-9><number from 0-9>[...]<number
* 255><data>
*
* Example:
* 1 3 255 1 2 3, if the binary contents are interpreted as 8 bit integers
*
* @param {Array} packets
* @return {Buffer} encoded payload
* @api private
*/
export function encodePayloadAsBinary (packets, callback) {
if (!packets.length) {
return callback(EMPTY_BUFFER);
}
map(packets, encodeOneBinaryPacket, function(err, results) {
return callback(Buffer.concat(results));
});
};
function encodeOneBinaryPacket(p, doneCallback) {
function onBinaryPacketEncode(packet) {
var encodingLength = '' + packet.length;
var sizeBuffer;
if (typeof packet === 'string') {
sizeBuffer = Buffer.allocUnsafe(encodingLength.length + 2);
sizeBuffer[0] = 0; // is a string (not true binary = 0)
for (var i = 0; i < encodingLength.length; i++) {
sizeBuffer[i + 1] = parseInt(encodingLength[i], 10);
}
sizeBuffer[sizeBuffer.length - 1] = 255;
return doneCallback(null, Buffer.concat([sizeBuffer, stringToBuffer(packet)]));
}
sizeBuffer = Buffer.allocUnsafe(encodingLength.length + 2);
sizeBuffer[0] = 1; // is binary (true binary = 1)
for (var i = 0; i < encodingLength.length; i++) {
sizeBuffer[i + 1] = parseInt(encodingLength[i], 10);
}
sizeBuffer[sizeBuffer.length - 1] = 255;
doneCallback(null, Buffer.concat([sizeBuffer, packet]));
}
encodePacket(p, true, true, onBinaryPacketEncode);
}
/*
* Decodes data when a payload is maybe expected. Strings are decoded by
* interpreting each byte as a key code for entries marked to start with 0. See
* description of encodePayloadAsBinary
* @param {Buffer} data, callback method
* @api public
*/
export function decodePayloadAsBinary (data, binaryType, callback) {
if (typeof binaryType === 'function') {
callback = binaryType;
binaryType = null;
}
var bufferTail = data;
var buffers = [];
var i;
while (bufferTail.length > 0) {
var strLen = '';
var isString = bufferTail[0] === 0;
for (i = 1; ; i++) {
if (bufferTail[i] === 255) break;
// 310 = char length of Number.MAX_VALUE
if (strLen.length > 310) {
return callback(err, 0, 1);
}
strLen += '' + bufferTail[i];
}
bufferTail = bufferTail.slice(strLen.length + 1);
var msgLength = parseInt(strLen, 10);
var msg = bufferTail.slice(1, msgLength + 1);
if (isString) msg = bufferToString(msg);
buffers.push(msg);
bufferTail = bufferTail.slice(msgLength + 1);
}
var total = buffers.length;
for (i = 0; i < total; i++) {
var buffer = buffers[i];
callback(decodePacket(buffer, binaryType, true), i, total);
}
};

View File

@@ -0,0 +1,210 @@
/*! https://mths.be/utf8js v2.1.2 by @mathias */
var stringFromCharCode = String.fromCharCode;
// Taken from https://mths.be/punycode
function ucs2decode(string) {
var output = [];
var counter = 0;
var length = string.length;
var value;
var extra;
while (counter < length) {
value = string.charCodeAt(counter++);
if (value >= 0xD800 && value <= 0xDBFF && counter < length) {
// high surrogate, and there is a next character
extra = string.charCodeAt(counter++);
if ((extra & 0xFC00) == 0xDC00) { // low surrogate
output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000);
} else {
// unmatched surrogate; only append this code unit, in case the next
// code unit is the high surrogate of a surrogate pair
output.push(value);
counter--;
}
} else {
output.push(value);
}
}
return output;
}
// Taken from https://mths.be/punycode
function ucs2encode(array) {
var length = array.length;
var index = -1;
var value;
var output = '';
while (++index < length) {
value = array[index];
if (value > 0xFFFF) {
value -= 0x10000;
output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800);
value = 0xDC00 | value & 0x3FF;
}
output += stringFromCharCode(value);
}
return output;
}
function checkScalarValue(codePoint, strict) {
if (codePoint >= 0xD800 && codePoint <= 0xDFFF) {
if (strict) {
throw Error(
'Lone surrogate U+' + codePoint.toString(16).toUpperCase() +
' is not a scalar value'
);
}
return false;
}
return true;
}
/*--------------------------------------------------------------------------*/
function createByte(codePoint, shift) {
return stringFromCharCode(((codePoint >> shift) & 0x3F) | 0x80);
}
function encodeCodePoint(codePoint, strict) {
if ((codePoint & 0xFFFFFF80) == 0) { // 1-byte sequence
return stringFromCharCode(codePoint);
}
var symbol = '';
if ((codePoint & 0xFFFFF800) == 0) { // 2-byte sequence
symbol = stringFromCharCode(((codePoint >> 6) & 0x1F) | 0xC0);
}
else if ((codePoint & 0xFFFF0000) == 0) { // 3-byte sequence
if (!checkScalarValue(codePoint, strict)) {
codePoint = 0xFFFD;
}
symbol = stringFromCharCode(((codePoint >> 12) & 0x0F) | 0xE0);
symbol += createByte(codePoint, 6);
}
else if ((codePoint & 0xFFE00000) == 0) { // 4-byte sequence
symbol = stringFromCharCode(((codePoint >> 18) & 0x07) | 0xF0);
symbol += createByte(codePoint, 12);
symbol += createByte(codePoint, 6);
}
symbol += stringFromCharCode((codePoint & 0x3F) | 0x80);
return symbol;
}
function utf8encode(string, opts) {
opts = opts || {};
var strict = false !== opts.strict;
var codePoints = ucs2decode(string);
var length = codePoints.length;
var index = -1;
var codePoint;
var byteString = '';
while (++index < length) {
codePoint = codePoints[index];
byteString += encodeCodePoint(codePoint, strict);
}
return byteString;
}
/*--------------------------------------------------------------------------*/
function readContinuationByte() {
if (byteIndex >= byteCount) {
throw Error('Invalid byte index');
}
var continuationByte = byteArray[byteIndex] & 0xFF;
byteIndex++;
if ((continuationByte & 0xC0) == 0x80) {
return continuationByte & 0x3F;
}
// If we end up here, its not a continuation byte
throw Error('Invalid continuation byte');
}
function decodeSymbol(strict) {
var byte1;
var byte2;
var byte3;
var byte4;
var codePoint;
if (byteIndex > byteCount) {
throw Error('Invalid byte index');
}
if (byteIndex == byteCount) {
return false;
}
// Read first byte
byte1 = byteArray[byteIndex] & 0xFF;
byteIndex++;
// 1-byte sequence (no continuation bytes)
if ((byte1 & 0x80) == 0) {
return byte1;
}
// 2-byte sequence
if ((byte1 & 0xE0) == 0xC0) {
byte2 = readContinuationByte();
codePoint = ((byte1 & 0x1F) << 6) | byte2;
if (codePoint >= 0x80) {
return codePoint;
} else {
throw Error('Invalid continuation byte');
}
}
// 3-byte sequence (may include unpaired surrogates)
if ((byte1 & 0xF0) == 0xE0) {
byte2 = readContinuationByte();
byte3 = readContinuationByte();
codePoint = ((byte1 & 0x0F) << 12) | (byte2 << 6) | byte3;
if (codePoint >= 0x0800) {
return checkScalarValue(codePoint, strict) ? codePoint : 0xFFFD;
} else {
throw Error('Invalid continuation byte');
}
}
// 4-byte sequence
if ((byte1 & 0xF8) == 0xF0) {
byte2 = readContinuationByte();
byte3 = readContinuationByte();
byte4 = readContinuationByte();
codePoint = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0C) |
(byte3 << 0x06) | byte4;
if (codePoint >= 0x010000 && codePoint <= 0x10FFFF) {
return codePoint;
}
}
throw Error('Invalid UTF-8 detected');
}
var byteArray;
var byteCount;
var byteIndex;
function utf8decode(byteString, opts) {
opts = opts || {};
var strict = false !== opts.strict;
byteArray = ucs2decode(byteString);
byteCount = byteArray.length;
byteIndex = 0;
var codePoints = [];
var tmp;
while ((tmp = decodeSymbol(strict)) !== false) {
codePoints.push(tmp);
}
return ucs2encode(codePoints);
}
module.exports = {
version: '2.1.2',
encode: utf8encode,
decode: utf8decode
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,121 +1,145 @@
import { EventEmitter } from 'events' import { EventEmitter } from "events";
import parser_v4 from "../engine.io-parser" import * as parser_v4 from "../engine.io-parser";
import type { WebSocketClient } from '../server/client' import * as parser_v3 from "./parser-v3";
// import debugModule from "debug"
// import { IncomingMessage } from "http"
import { Packet } from "../engine.io-parser";
// const debug = debugModule("engine:transport")
const debug = require("../debug")("engine:transport");
/** /**
* Noop function. * Noop function.
* *
* @api private * @api private
*/ */
function noop() { } function noop() {}
export abstract class Transport extends EventEmitter { export abstract class Transport extends EventEmitter {
public sid: string public sid: string;
public req /**http.IncomingMessage */ public writable: boolean;
public socket: WebSocketClient public protocol: number;
public writable: boolean
public readyState: string
public discarded: boolean
public protocol: Number
public parser: any
public perMessageDeflate: any
public supportsBinary: boolean = false
/** protected _readyState: string;
* Transport constructor. protected discarded: boolean;
* protected parser: any;
* @param {http.IncomingMessage} request // protected req: IncomingMessage & { cleanup: Function }
* @api public protected req: { cleanup: Function };
*/ protected supportsBinary: boolean;
constructor(req) {
super() get readyState() {
this.readyState = "open" return this._readyState;
this.discarded = false }
this.protocol = req._query.EIO === "4" ? 4 : 3 // 3rd revision by default
this.parser = parser_v4//= this.protocol === 4 ? parser_v4 : parser_v3 set readyState(state) {
debug(
"readyState updated from %s to %s (%s)",
this._readyState,
state,
this.name
);
this._readyState = state;
}
/**
* Transport constructor.
*
* @param {http.IncomingMessage} request
* @api public
*/
constructor(req) {
super();
this.readyState = "open";
this.discarded = false;
this.protocol = req._query.EIO === "4" ? 4 : 3; // 3rd revision by default
this.parser = this.protocol === 4 ? parser_v4 : parser_v3;
}
/**
* Flags the transport as discarded.
*
* @api private
*/
discard() {
this.discarded = true;
}
/**
* Called with an incoming HTTP request.
*
* @param {http.IncomingMessage} request
* @api protected
*/
protected onRequest(req) {
debug("setting request");
this.req = req;
}
/**
* Closes the transport.
*
* @api private
*/
close(fn?) {
if ("closed" === this.readyState || "closing" === this.readyState) return;
this.readyState = "closing";
this.doClose(fn || noop);
}
/**
* Called with a transport error.
*
* @param {String} message error
* @param {Object} error description
* @api protected
*/
protected onError(msg: string, desc?) {
if (this.listeners("error").length) {
const err = new Error(msg);
// @ts-ignore
err.type = "TransportError";
// @ts-ignore
err.description = desc;
this.emit("error", err);
} else {
debug("ignored transport error %s (%s)", msg, desc);
} }
}
/** /**
* Flags the transport as discarded. * Called with parsed out a packets from the data stream.
* *
* @api private * @param {Object} packet
*/ * @api protected
discard() { */
this.discarded = true protected onPacket(packet: Packet) {
} this.emit("packet", packet);
}
/** /**
* Called with an incoming HTTP request. * Called with the encoded packet data.
* *
* @param {http.IncomingMessage} request * @param {String} data
* @api private * @api protected
*/ */
onRequest(req) { protected onData(data) {
console.debug(`engine.io transport ${this.socket.id} setting request`, JSON.stringify(req)) this.onPacket(this.parser.decodePacket(data));
this.req = req }
}
/** /**
* Closes the transport. * Called upon transport close.
* *
* @api private * @api protected
*/ */
close(fn?) { protected onClose() {
if ("closed" === this.readyState || "closing" === this.readyState) return this.readyState = "closed";
this.emit("close");
}
this.readyState = "closing" abstract get supportsFraming();
this.doClose(fn || noop) abstract get name();
} abstract send(packets);
abstract doClose(fn?);
/**
* Called with a transport error.
*
* @param {String} message error
* @param {Object} error description
* @api private
*/
onError(msg: string, desc?: string) {
if (this.listeners("error").length) {
const err: any = new Error(msg)
err.type = "TransportError"
err.description = desc
this.emit("error", err)
} else {
console.debug(`ignored transport error ${msg} (${desc})`)
}
}
/**
* Called with parsed out a packets from the data stream.
*
* @param {Object} packet
* @api private
*/
onPacket(packet) {
this.emit("packet", packet)
}
/**
* Called with the encoded packet data.
*
* @param {String} data
* @api private
*/
onData(data) {
this.onPacket(this.parser.decodePacket(data))
}
/**
* Called upon transport close.
*
* @api private
*/
onClose() {
this.readyState = "closed"
this.emit("close")
}
abstract get supportsFraming()
abstract get name()
abstract send(...args: any[])
abstract doClose(d: Function)
} }

View File

@@ -1,3 +1,24 @@
// import { Polling as XHR } from "./polling"
// import { JSONP } from "./polling-jsonp"
import { WebSocket } from "./websocket";
export default { export default {
websocket: require("./websocket").WebSocket // polling: polling,
} websocket: WebSocket,
};
// /**
// * Polling polymorphic constructor.
// *
// * @api private
// */
// function polling(req) {
// if ("string" === typeof req._query.j) {
// return new JSONP(req)
// } else {
// return new XHR(req)
// }
// }
// polling.upgradesTo = ["websocket"]

View File

@@ -1,116 +1,111 @@
import { Transport } from '../transport' import { Transport } from "../transport";
// const debug = require("debug")("engine:ws") // import debugModule from "debug";
const debug = require("../../debug")("engine:ws");
export class WebSocket extends Transport { export class WebSocket extends Transport {
public perMessageDeflate: any protected perMessageDeflate: any;
private socket: any;
/** /**
* WebSocket transport * WebSocket transport
* *
* @param {http.IncomingMessage} * @param {http.IncomingMessage}
* @api public * @api public
*/ */
constructor(req) { constructor(req) {
super(req) super(req);
this.socket = req.websocket this.socket = req.websocket;
this.socket.on("message", this.onData.bind(this)) this.socket.on("message", (data, isBinary) => {
this.socket.once("close", this.onClose.bind(this)) const message = isBinary ? data : data.toString();
this.socket.on("error", this.onError.bind(this)) debug('received "%s"', message);
this.writable = true super.onData(message);
this.perMessageDeflate = null });
this.socket.once("close", this.onClose.bind(this));
this.socket.on("error", this.onError.bind(this));
this.writable = true;
this.perMessageDeflate = null;
}
/**
* Transport name
*
* @api public
*/
get name() {
return "websocket";
}
/**
* Advertise upgrade support.
*
* @api public
*/
get handlesUpgrades() {
return true;
}
/**
* Advertise framing support.
*
* @api public
*/
get supportsFraming() {
return true;
}
/**
* Writes a packet payload.
*
* @param {Array} packets
* @api private
*/
send(packets) {
const packet = packets.shift();
if (typeof packet === "undefined") {
this.writable = true;
this.emit("drain");
return;
} }
/** // always creates a new object since ws modifies it
* Transport name const opts: { compress?: boolean } = {};
* if (packet.options) {
* @api public opts.compress = packet.options.compress;
*/
get name() {
return "websocket"
} }
/** const send = data => {
* Advertise upgrade support. if (this.perMessageDeflate) {
* const len =
* @api public "string" === typeof data ? Buffer.byteLength(data) : data.length;
*/ if (len < this.perMessageDeflate.threshold) {
get handlesUpgrades() { opts.compress = false;
return true
}
/**
* Advertise framing support.
*
* @api public
*/
get supportsFraming() {
return true
}
/**
* Processes the incoming data.
*
* @param {String} encoded packet
* @api private
*/
onData(data) {
// debug('received "%s"', data)
super.onData(data)
}
/**
* Writes a packet payload.
*
* @param {Array} packets
* @api private
*/
send(packets) {
// console.log('WebSocket send packets', JSON.stringify(packets))
const packet = packets.shift()
if (typeof packet === "undefined") {
this.writable = true
this.emit("drain")
return
} }
}
debug('writing "%s"', data);
this.writable = false;
// always creates a new object since ws modifies it this.socket.send(data, opts, err => {
const opts: any = {} if (err) return this.onError("write error", err.stack);
if (packet.options) { this.send(packets);
opts.compress = packet.options.compress });
} };
const send = data => { if (packet.options && typeof packet.options.wsPreEncoded === "string") {
if (this.perMessageDeflate) { send(packet.options.wsPreEncoded);
const len = } else {
"string" === typeof data ? Buffer.byteLength(data) : data.length this.parser.encodePacket(packet, this.supportsBinary, send);
if (len < this.perMessageDeflate.threshold) {
opts.compress = false
}
}
console.trace('writing', data)
this.writable = false
this.socket.send(data, opts, err => {
if (err) return this.onError("write error", err.stack)
this.send(packets)
})
}
if (packet.options && typeof packet.options.wsPreEncoded === "string") {
send(packet.options.wsPreEncoded)
} else {
this.parser.encodePacket(packet, this.supportsBinary, send)
}
} }
}
/** /**
* Closes the transport. * Closes the transport.
* *
* @api private * @api private
*/ */
doClose(fn) { doClose(fn) {
// debug("closing") debug("closing");
this.socket.close() this.socket.close();
fn && fn() fn && fn();
} }
} }

View File

@@ -0,0 +1,65 @@
// imported from https://github.com/unshiftio/yeast
"use strict";
const alphabet =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_".split(
""
),
length = 64,
map = {};
let seed = 0,
i = 0,
prev;
/**
* Return a string representing the specified number.
*
* @param {Number} num The number to convert.
* @returns {String} The string representation of the number.
* @api public
*/
export function encode(num) {
let encoded = "";
do {
encoded = alphabet[num % length] + encoded;
num = Math.floor(num / length);
} while (num > 0);
return encoded;
}
/**
* Return the integer value specified by the given string.
*
* @param {String} str The string to convert.
* @returns {Number} The integer value represented by the string.
* @api public
*/
export function decode(str) {
let decoded = 0;
for (i = 0; i < str.length; i++) {
decoded = decoded * length + map[str.charAt(i)];
}
return decoded;
}
/**
* Yeast: A tiny growing id generator.
*
* @returns {String} A unique id.
* @api public
*/
export function yeast() {
const now = encode(+new Date());
if (now !== prev) return (seed = 0), (prev = now);
return now + "." + encode(seed++);
}
//
// Map each character to its index.
//
for (; i < length; i++) map[alphabet[i]] = i;

View File

@@ -1,279 +1,507 @@
import { EventEmitter } from "events" import { EventEmitter } from "events";
import { yeast } from "./contrib/yeast";
// import WebSocket = require("ws");
export type SocketId = string // const canPreComputeFrame = typeof WebSocket?.Sender?.frame === "function";
export type Room = string
/**
* A public ID, sent by the server at the beginning of the Socket.IO session and which can be used for private messaging
*/
export type SocketId = string;
/**
* A private ID, sent by the server at the beginning of the Socket.IO session and used for connection state recovery
* upon reconnection
*/
export type PrivateSessionId = string;
// we could extend the Room type to "string | number", but that would be a breaking change
// related: https://github.com/socketio/socket.io-redis-adapter/issues/418
export type Room = string;
export interface BroadcastFlags { export interface BroadcastFlags {
volatile?: boolean volatile?: boolean;
compress?: boolean compress?: boolean;
local?: boolean local?: boolean;
broadcast?: boolean broadcast?: boolean;
binary?: boolean binary?: boolean;
timeout?: number;
} }
export interface BroadcastOptions { export interface BroadcastOptions {
rooms: Set<Room> rooms: Set<Room>;
except?: Set<SocketId> except?: Set<Room>;
flags?: BroadcastFlags flags?: BroadcastFlags;
} }
interface SessionToPersist {
sid: SocketId;
pid: PrivateSessionId;
rooms: Room[];
data: unknown;
}
export type Session = SessionToPersist & { missedPackets: unknown[][] };
export class Adapter extends EventEmitter { export class Adapter extends EventEmitter {
public rooms: Map<Room, Set<SocketId>> = new Map(); public rooms: Map<Room, Set<SocketId>> = new Map();
public sids: Map<SocketId, Set<Room>> = new Map(); public sids: Map<SocketId, Set<Room>> = new Map();
private readonly encoder private readonly encoder;
/** /**
* In-memory adapter constructor. * In-memory adapter constructor.
* *
* @param {Namespace} nsp * @param {Namespace} nsp
*/ */
constructor(readonly nsp: any) { constructor(readonly nsp: any) {
super() super();
this.encoder = nsp.server.encoder this.encoder = nsp.server.encoder;
}
/**
* To be overridden
*/
public init(): Promise<void> | void {}
/**
* To be overridden
*/
public close(): Promise<void> | void {}
/**
* Returns the number of Socket.IO servers in the cluster
*
* @public
*/
public serverCount(): Promise<number> {
return Promise.resolve(1);
}
/**
* Adds a socket to a list of room.
*
* @param {SocketId} id the socket id
* @param {Set<Room>} rooms a set of rooms
* @public
*/
public addAll(id: SocketId, rooms: Set<Room>): Promise<void> | void {
if (!this.sids.has(id)) {
this.sids.set(id, new Set());
} }
/** for (const room of rooms) {
* To be overridden this.sids.get(id).add(room);
*/
public init(): Promise<void> | void { }
/** if (!this.rooms.has(room)) {
* To be overridden this.rooms.set(room, new Set());
*/ this.emit("create-room", room);
public close(): Promise<void> | void { } }
if (!this.rooms.get(room).has(id)) {
this.rooms.get(room).add(id);
this.emit("join-room", room, id);
}
}
}
/** /**
* Adds a socket to a list of room. * Removes a socket from a room.
* *
* @param {SocketId} id the socket id * @param {SocketId} id the socket id
* @param {Set<Room>} rooms a set of rooms * @param {Room} room the room name
* @public */
*/ public del(id: SocketId, room: Room): Promise<void> | void {
public addAll(id: SocketId, rooms: Set<Room>): Promise<void> | void { if (this.sids.has(id)) {
if (!this.sids.has(id)) { this.sids.get(id).delete(room);
this.sids.set(id, new Set())
}
for (const room of rooms) {
this.sids.get(id).add(room)
if (!this.rooms.has(room)) {
this.rooms.set(room, new Set())
this.emit("create-room", room)
}
if (!this.rooms.get(room).has(id)) {
this.rooms.get(room).add(id)
this.emit("join-room", room, id)
}
}
} }
/** this._del(room, id);
* Removes a socket from a room. }
*
* @param {SocketId} id the socket id
* @param {Room} room the room name
*/
public del(id: SocketId, room: Room): Promise<void> | void {
if (this.sids.has(id)) {
this.sids.get(id).delete(room)
}
this._del(room, id) private _del(room: Room, id: SocketId) {
const _room = this.rooms.get(room);
if (_room != null) {
const deleted = _room.delete(id);
if (deleted) {
this.emit("leave-room", room, id);
}
if (_room.size === 0 && this.rooms.delete(room)) {
this.emit("delete-room", room);
}
}
}
/**
* Removes a socket from all rooms it's joined.
*
* @param {SocketId} id the socket id
*/
public delAll(id: SocketId): void {
if (!this.sids.has(id)) {
return;
} }
private _del(room, id) { for (const room of this.sids.get(id)) {
this._del(room, id);
}
this.sids.delete(id);
}
/**
* Broadcasts a packet.
*
* Options:
* - `flags` {Object} flags for this packet
* - `except` {Array} sids that should be excluded
* - `rooms` {Array} list of rooms to broadcast to
*
* @param {Object} packet the packet object
* @param {Object} opts the options
* @public
*/
public broadcast(packet: any, opts: BroadcastOptions): void {
const flags = opts.flags || {};
const packetOpts = {
preEncoded: true,
volatile: flags.volatile,
compress: flags.compress,
};
packet.nsp = this.nsp.name;
const encodedPackets = this._encode(packet, packetOpts);
this.apply(opts, (socket) => {
if (typeof socket.notifyOutgoingListeners === "function") {
socket.notifyOutgoingListeners(packet);
}
socket.client.writeToEngine(encodedPackets, packetOpts);
});
}
/**
* Broadcasts a packet and expects multiple acknowledgements.
*
* Options:
* - `flags` {Object} flags for this packet
* - `except` {Array} sids that should be excluded
* - `rooms` {Array} list of rooms to broadcast to
*
* @param {Object} packet the packet object
* @param {Object} opts the options
* @param clientCountCallback - the number of clients that received the packet
* @param ack - the callback that will be called for each client response
*
* @public
*/
public broadcastWithAck(
packet: any,
opts: BroadcastOptions,
clientCountCallback: (clientCount: number) => void,
ack: (...args: any[]) => void
) {
const flags = opts.flags || {};
const packetOpts = {
preEncoded: true,
volatile: flags.volatile,
compress: flags.compress,
};
packet.nsp = this.nsp.name;
// we can use the same id for each packet, since the _ids counter is common (no duplicate)
packet.id = this.nsp._ids++;
const encodedPackets = this._encode(packet, packetOpts);
let clientCount = 0;
this.apply(opts, (socket) => {
// track the total number of acknowledgements that are expected
clientCount++;
// call the ack callback for each client response
socket.acks.set(packet.id, ack);
if (typeof socket.notifyOutgoingListeners === "function") {
socket.notifyOutgoingListeners(packet);
}
socket.client.writeToEngine(encodedPackets, packetOpts);
});
clientCountCallback(clientCount);
}
private _encode(packet: unknown, packetOpts: Record<string, unknown>) {
const encodedPackets = this.encoder.encode(packet);
// if (
// canPreComputeFrame &&
// encodedPackets.length === 1 &&
// typeof encodedPackets[0] === "string"
// ) {
// // "4" being the "message" packet type in the Engine.IO protocol
// const data = Buffer.from("4" + encodedPackets[0]);
// // see https://github.com/websockets/ws/issues/617#issuecomment-283002469
// packetOpts.wsPreEncodedFrame = WebSocket.Sender.frame(data, {
// readOnly: false,
// mask: false,
// rsv1: false,
// opcode: 1,
// fin: true,
// });
// }
return encodedPackets;
}
/**
* Gets a list of sockets by sid.
*
* @param {Set<Room>} rooms the explicit set of rooms to check.
*/
public sockets(rooms: Set<Room>): Promise<Set<SocketId>> {
const sids = new Set<SocketId>();
this.apply({ rooms }, (socket) => {
sids.add(socket.id);
});
return Promise.resolve(sids);
}
/**
* Gets the list of rooms a given socket has joined.
*
* @param {SocketId} id the socket id
*/
public socketRooms(id: SocketId): Set<Room> | undefined {
return this.sids.get(id);
}
/**
* Returns the matching socket instances
*
* @param opts - the filters to apply
*/
public fetchSockets(opts: BroadcastOptions): Promise<any[]> {
const sockets = [];
this.apply(opts, (socket) => {
sockets.push(socket);
});
return Promise.resolve(sockets);
}
/**
* Makes the matching socket instances join the specified rooms
*
* @param opts - the filters to apply
* @param rooms - the rooms to join
*/
public addSockets(opts: BroadcastOptions, rooms: Room[]): void {
this.apply(opts, (socket) => {
socket.join(rooms);
});
}
/**
* Makes the matching socket instances leave the specified rooms
*
* @param opts - the filters to apply
* @param rooms - the rooms to leave
*/
public delSockets(opts: BroadcastOptions, rooms: Room[]): void {
this.apply(opts, (socket) => {
rooms.forEach((room) => socket.leave(room));
});
}
/**
* Makes the matching socket instances disconnect
*
* @param opts - the filters to apply
* @param close - whether to close the underlying connection
*/
public disconnectSockets(opts: BroadcastOptions, close: boolean): void {
this.apply(opts, (socket) => {
socket.disconnect(close);
});
}
private apply(opts: BroadcastOptions, callback: (socket) => void): void {
const rooms = opts.rooms;
const except = this.computeExceptSids(opts.except);
if (rooms.size) {
const ids = new Set();
for (const room of rooms) {
if (!this.rooms.has(room)) continue;
for (const id of this.rooms.get(room)) {
if (ids.has(id) || except.has(id)) continue;
const socket = this.nsp.sockets.get(id);
if (socket) {
callback(socket);
ids.add(id);
}
}
}
} else {
for (const [id] of this.sids) {
if (except.has(id)) continue;
const socket = this.nsp.sockets.get(id);
if (socket) callback(socket);
}
}
}
private computeExceptSids(exceptRooms?: Set<Room>) {
const exceptSids = new Set();
if (exceptRooms && exceptRooms.size > 0) {
for (const room of exceptRooms) {
if (this.rooms.has(room)) { if (this.rooms.has(room)) {
const deleted = this.rooms.get(room).delete(id) this.rooms.get(room).forEach((sid) => exceptSids.add(sid));
if (deleted) {
this.emit("leave-room", room, id)
}
if (this.rooms.get(room).size === 0) {
this.rooms.delete(room)
this.emit("delete-room", room)
}
} }
}
} }
return exceptSids;
}
/** /**
* Removes a socket from all rooms it's joined. * Send a packet to the other Socket.IO servers in the cluster
* * @param packet - an array of arguments, which may include an acknowledgement callback at the end
* @param {SocketId} id the socket id */
*/ public serverSideEmit(packet: any[]): void {
public delAll(id: SocketId): void { console.warn(
if (!this.sids.has(id)) { "this adapter does not support the serverSideEmit() functionality"
return );
} }
for (const room of this.sids.get(id)) { /**
this._del(room, id) * Save the client session in order to restore it upon reconnection.
} */
public persistSession(session: SessionToPersist) {}
this.sids.delete(id) /**
} * Restore the session and find the packets that were missed by the client.
* @param pid
/** * @param offset
* Broadcasts a packet. */
* public restoreSession(
* Options: pid: PrivateSessionId,
* - `flags` {Object} flags for this packet offset: string
* - `except` {Array} sids that should be excluded ): Session {
* - `rooms` {Array} list of rooms to broadcast to return null;
* }
* @param {Object} packet the packet object }
* @param {Object} opts the options
* @public interface PersistedPacket {
*/ id: string;
public broadcast(packet: any, opts: BroadcastOptions): void { emittedAt: number;
const flags = opts.flags || {} data: unknown[];
const basePacketOpts = { opts: BroadcastOptions;
preEncoded: true, }
volatile: flags.volatile,
compress: flags.compress type SessionWithTimestamp = SessionToPersist & { disconnectedAt: number };
}
export class SessionAwareAdapter extends Adapter {
packet.nsp = this.nsp.name private readonly maxDisconnectionDuration: number;
const encodedPackets = this.encoder.encode(packet)
private sessions: Map<PrivateSessionId, SessionWithTimestamp> = new Map();
const packetOpts = encodedPackets.map(encodedPacket => { private packets: PersistedPacket[] = [];
if (typeof encodedPacket === "string") {
return { constructor(readonly nsp: any) {
...basePacketOpts, super(nsp);
wsPreEncoded: "4" + encodedPacket // "4" being the "message" packet type in Engine.IO this.maxDisconnectionDuration =
} nsp.server.opts.connectionStateRecovery.maxDisconnectionDuration;
} else {
return basePacketOpts const timer = setInterval(() => {
} const threshold = Date.now() - this.maxDisconnectionDuration;
}) this.sessions.forEach((session, sessionId) => {
const hasExpired = session.disconnectedAt < threshold;
this.apply(opts, socket => { if (hasExpired) {
for (let i = 0; i < encodedPackets.length; i++) { this.sessions.delete(sessionId);
socket.client.writeToEngine(encodedPackets[i], packetOpts[i]) }
} });
}) for (let i = this.packets.length - 1; i >= 0; i--) {
} const hasExpired = this.packets[i].emittedAt < threshold;
if (hasExpired) {
/** this.packets.splice(0, i + 1);
* Gets a list of sockets by sid. break;
* }
* @param {Set<Room>} rooms the explicit set of rooms to check. }
*/ }, 60 * 1000);
public sockets(rooms: Set<Room>): Promise<Set<SocketId>> { // prevents the timer from keeping the process alive
const sids = new Set<SocketId>() timer.unref();
}
this.apply({ rooms }, socket => {
sids.add(socket.id) override persistSession(session: SessionToPersist) {
}) (session as SessionWithTimestamp).disconnectedAt = Date.now();
this.sessions.set(session.pid, session as SessionWithTimestamp);
return Promise.resolve(sids) }
}
override restoreSession(
/** pid: PrivateSessionId,
* Gets the list of rooms a given socket has joined. offset: string
* ): Session {
* @param {SocketId} id the socket id const session = this.sessions.get(pid);
*/ if (!session) {
public socketRooms(id: SocketId): Set<Room> | undefined { // the session may have expired
return this.sids.get(id) return null;
} }
const hasExpired =
/** session.disconnectedAt + this.maxDisconnectionDuration < Date.now();
* Returns the matching socket instances if (hasExpired) {
* // the session has expired
* @param opts - the filters to apply this.sessions.delete(pid);
*/ return null;
public fetchSockets(opts: BroadcastOptions): Promise<any[]> { }
const sockets = [] const index = this.packets.findIndex((packet) => packet.id === offset);
if (index === -1) {
this.apply(opts, socket => { // the offset may be too old
sockets.push(socket) return null;
}) }
const missedPackets = [];
return Promise.resolve(sockets) for (let i = index + 1; i < this.packets.length; i++) {
} const packet = this.packets[i];
if (shouldIncludePacket(session.rooms, packet.opts)) {
/** missedPackets.push(packet.data);
* Makes the matching socket instances join the specified rooms }
* }
* @param opts - the filters to apply return {
* @param rooms - the rooms to join ...session,
*/ missedPackets,
public addSockets(opts: BroadcastOptions, rooms: Room[]): void { };
this.apply(opts, socket => { }
socket.join(rooms)
}) override broadcast(packet: any, opts: BroadcastOptions) {
} const isEventPacket = packet.type === 2;
// packets with acknowledgement are not stored because the acknowledgement function cannot be serialized and
/** // restored on another server upon reconnection
* Makes the matching socket instances leave the specified rooms const withoutAcknowledgement = packet.id === undefined;
* const notVolatile = opts.flags?.volatile === undefined;
* @param opts - the filters to apply if (isEventPacket && withoutAcknowledgement && notVolatile) {
* @param rooms - the rooms to leave const id = yeast();
*/ // the offset is stored at the end of the data array, so the client knows the ID of the last packet it has
public delSockets(opts: BroadcastOptions, rooms: Room[]): void { // processed (and the format is backward-compatible)
this.apply(opts, socket => { packet.data.push(id);
rooms.forEach(room => socket.leave(room)) this.packets.push({
}) id,
} opts,
data: packet.data,
/** emittedAt: Date.now(),
* Makes the matching socket instances disconnect });
* }
* @param opts - the filters to apply super.broadcast(packet, opts);
* @param close - whether to close the underlying connection }
*/ }
public disconnectSockets(opts: BroadcastOptions, close: boolean): void {
this.apply(opts, socket => { function shouldIncludePacket(
socket.disconnect(close) sessionRooms: Room[],
}) opts: BroadcastOptions
} ): boolean {
const included =
private apply(opts: BroadcastOptions, callback: (socket) => void): void { opts.rooms.size === 0 || sessionRooms.some((room) => opts.rooms.has(room));
const rooms = opts.rooms const notExcluded = sessionRooms.every((room) => !opts.except.has(room));
const except = this.computeExceptSids(opts.except) return included && notExcluded;
if (rooms.size) {
const ids = new Set()
for (const room of rooms) {
if (!this.rooms.has(room)) continue
for (const id of this.rooms.get(room)) {
if (ids.has(id) || except.has(id)) continue
const socket = this.nsp.sockets.get(id)
if (socket) {
callback(socket)
ids.add(id)
}
}
}
} else {
for (const [id] of this.sids) {
if (except.has(id)) continue
const socket = this.nsp.sockets.get(id)
if (socket) callback(socket)
}
}
}
private computeExceptSids(exceptRooms?: Set<Room>) {
const exceptSids = new Set()
if (exceptRooms && exceptRooms.size > 0) {
for (const room of exceptRooms) {
if (this.rooms.has(room)) {
this.rooms.get(room).forEach(sid => exceptSids.add(sid))
}
}
}
return exceptSids
}
/**
* Send a packet to the other Socket.IO servers in the cluster
* @param packet - an array of arguments, which may include an acknowledgement callback at the end
*/
public serverSideEmit(packet: any[]): void {
throw new Error(
"this adapter does not support the serverSideEmit() functionality"
)
}
} }

View File

@@ -0,0 +1,77 @@
/**
* Initialize backoff timer with `opts`.
*
* - `min` initial timeout in milliseconds [100]
* - `max` max timeout [10000]
* - `jitter` [0]
* - `factor` [2]
*
* @param {Object} opts
* @api public
*/
export function Backoff(this: any, opts) {
opts = opts || {}
this.ms = opts.min || 100
this.max = opts.max || 10000
this.factor = opts.factor || 2
this.jitter = opts.jitter > 0 && opts.jitter <= 1 ? opts.jitter : 0
this.attempts = 0
}
/**
* Return the backoff duration.
*
* @return {Number}
* @api public
*/
Backoff.prototype.duration = function () {
var ms = this.ms * Math.pow(this.factor, this.attempts++)
if (this.jitter) {
var rand = Math.random()
var deviation = Math.floor(rand * this.jitter * ms)
ms = (Math.floor(rand * 10) & 1) == 0 ? ms - deviation : ms + deviation
}
return Math.min(ms, this.max) | 0
}
/**
* Reset the number of attempts.
*
* @api public
*/
Backoff.prototype.reset = function () {
this.attempts = 0
}
/**
* Set the minimum duration
*
* @api public
*/
Backoff.prototype.setMin = function (min) {
this.ms = min
}
/**
* Set the maximum duration
*
* @api public
*/
Backoff.prototype.setMax = function (max) {
this.max = max
}
/**
* Set the jitter
*
* @api public
*/
Backoff.prototype.setJitter = function (jitter) {
this.jitter = jitter
}

View File

@@ -1,19 +1,14 @@
import { url } from "./url" import { url } from "./url"
import { Manager, ManagerOptions } from "./manager" import { Manager, ManagerOptions } from "./manager"
import { Socket, SocketOptions } from "./socket" import { Socket, SocketOptions } from "./socket"
// import debugModule from "debug"; // debug()
const debug = require("../debug")("socket.io-client") const debug = require("../debug")("socket.io-client")
/**
* Module exports.
*/
module.exports = exports = lookup
/** /**
* Managers cache. * Managers cache.
*/ */
const cache: Record<string, Manager> = (exports.managers = {}) const cache: Record<string, Manager> = {}
/** /**
* Looks up an existing `Manager` for multiplexing. * Looks up an existing `Manager` for multiplexing.
@@ -76,6 +71,15 @@ function lookup(
return io.socket(parsed.path, opts) return io.socket(parsed.path, opts)
} }
// so that "lookup" can be used both as a function (e.g. `io(...)`) and as a
// namespace (e.g. `io.connect(...)`), for backward compatibility
Object.assign(lookup, {
Manager,
Socket,
io: lookup,
connect: lookup,
})
/** /**
* Protocol version. * Protocol version.
* *
@@ -84,22 +88,18 @@ function lookup(
export { protocol } from "../socket.io-parser" export { protocol } from "../socket.io-parser"
/**
* `connect`.
*
* @param {String} uri
* @public
*/
exports.connect = lookup
/** /**
* Expose constructors for standalone build. * Expose constructors for standalone build.
* *
* @public * @public
*/ */
export { Manager, ManagerOptions } from "./manager" export {
export { Socket } from "./socket" Manager,
export { lookup as io, SocketOptions } ManagerOptions,
export default lookup Socket,
SocketOptions,
lookup as io,
lookup as connect,
lookup as default,
}

View File

@@ -1,210 +1,26 @@
import eio from "../engine.io-client" import {
import { Socket, SocketOptions } from "./socket" Socket as Engine,
SocketOptions as EngineOptions,
installTimerFunctions,
nextTick,
} from "../engine.io-client"
import { Socket, SocketOptions, DisconnectDescription } from "./socket.js"
// import * as parser from "socket.io-parser"
import * as parser from "../socket.io-parser" import * as parser from "../socket.io-parser"
// import { Decoder, Encoder, Packet } from "socket.io-parser"
import { Decoder, Encoder, Packet } from "../socket.io-parser" import { Decoder, Encoder, Packet } from "../socket.io-parser"
import { on } from "./on" import { on } from "./on.js"
import * as Backoff from "backo2" import { Backoff } from "./contrib/backo2.js"
import { import {
DefaultEventsMap, DefaultEventsMap,
EventsMap, EventsMap,
StrictEventEmitter, Emitter,
} from "./typed-events" } from "@socket.io/component-emitter"
// import debugModule from "debug" // debug()
// const debug = debugModule("socket.io-client:manager") // debug()
const debug = require("../debug")("socket.io-client") const debug = require("../debug")("socket.io-client")
interface EngineOptions {
/**
* The host that we're connecting to. Set from the URI passed when connecting
*/
host: string
/**
* The hostname for our connection. Set from the URI passed when connecting
*/
hostname: string
/**
* If this is a secure connection. Set from the URI passed when connecting
*/
secure: boolean
/**
* The port for our connection. Set from the URI passed when connecting
*/
port: string
/**
* Any query parameters in our uri. Set from the URI passed when connecting
*/
query: { [key: string]: string }
/**
* `http.Agent` to use, defaults to `false` (NodeJS only)
*/
agent: string | boolean
/**
* Whether the client should try to upgrade the transport from
* long-polling to something better.
* @default true
*/
upgrade: boolean
/**
* Forces JSONP for polling transport.
*/
forceJSONP: boolean
/**
* Determines whether to use JSONP when necessary for polling. If
* disabled (by settings to false) an error will be emitted (saying
* "No transports available") if no other transports are available.
* If another transport is available for opening a connection (e.g.
* WebSocket) that transport will be used instead.
* @default true
*/
jsonp: boolean
/**
* Forces base 64 encoding for polling transport even when XHR2
* responseType is available and WebSocket even if the used standard
* supports binary.
*/
forceBase64: boolean
/**
* Enables XDomainRequest for IE8 to avoid loading bar flashing with
* click sound. default to `false` because XDomainRequest has a flaw
* of not sending cookie.
* @default false
*/
enablesXDR: boolean
/**
* The param name to use as our timestamp key
* @default 't'
*/
timestampParam: string
/**
* Whether to add the timestamp with each transport request. Note: this
* is ignored if the browser is IE or Android, in which case requests
* are always stamped
* @default false
*/
timestampRequests: boolean
/**
* A list of transports to try (in order). Engine.io always attempts to
* connect directly with the first one, provided the feature detection test
* for it passes.
* @default ['polling','websocket']
*/
transports: string[]
/**
* The port the policy server listens on
* @default 843
*/
policyPost: number
/**
* If true and if the previous websocket connection to the server succeeded,
* the connection attempt will bypass the normal upgrade process and will
* initially try websocket. A connection attempt following a transport error
* will use the normal upgrade process. It is recommended you turn this on
* only when using SSL/TLS connections, or if you know that your network does
* not block websockets.
* @default false
*/
rememberUpgrade: boolean
/**
* Are we only interested in transports that support binary?
*/
onlyBinaryUpgrades: boolean
/**
* Timeout for xhr-polling requests in milliseconds (0) (only for polling transport)
*/
requestTimeout: number
/**
* Transport options for Node.js client (headers etc)
*/
transportOptions: Object
/**
* (SSL) Certificate, Private key and CA certificates to use for SSL.
* Can be used in Node.js client environment to manually specify
* certificate information.
*/
pfx: string
/**
* (SSL) Private key to use for SSL. Can be used in Node.js client
* environment to manually specify certificate information.
*/
key: string
/**
* (SSL) A string or passphrase for the private key or pfx. Can be
* used in Node.js client environment to manually specify certificate
* information.
*/
passphrase: string
/**
* (SSL) Public x509 certificate to use. Can be used in Node.js client
* environment to manually specify certificate information.
*/
cert: string
/**
* (SSL) An authority certificate or array of authority certificates to
* check the remote host against.. Can be used in Node.js client
* environment to manually specify certificate information.
*/
ca: string | string[]
/**
* (SSL) A string describing the ciphers to use or exclude. Consult the
* [cipher format list]
* (http://www.openssl.org/docs/apps/ciphers.html#CIPHER_LIST_FORMAT) for
* details on the format.. Can be used in Node.js client environment to
* manually specify certificate information.
*/
ciphers: string
/**
* (SSL) If true, the server certificate is verified against the list of
* supplied CAs. An 'error' event is emitted if verification fails.
* Verification happens at the connection level, before the HTTP request
* is sent. Can be used in Node.js client environment to manually specify
* certificate information.
*/
rejectUnauthorized: boolean
/**
* Headers that will be passed for each request to the server (via xhr-polling and via websockets).
* These values then can be used during handshake or for special proxies.
*/
extraHeaders?: { [header: string]: string }
/**
* Whether to include credentials (cookies, authorization headers, TLS
* client certificates, etc.) with cross-origin XHR polling requests
* @default false
*/
withCredentials: boolean
/**
* Whether to automatically close the connection whenever the beforeunload event is received.
* @default true
*/
closeOnBeforeunload: boolean
}
export interface ManagerOptions extends EngineOptions { export interface ManagerOptions extends EngineOptions {
/** /**
* Should we force a new Manager for this connection? * Should we force a new Manager for this connection?
@@ -267,13 +83,6 @@ export interface ManagerOptions extends EngineOptions {
*/ */
autoConnect: boolean autoConnect: boolean
/**
* weather we should unref the reconnect timer when it is
* create automatically
* @default false
*/
autoUnref: boolean
/** /**
* the parser to use. Defaults to an instance of the Parser that ships with socket.io. * the parser to use. Defaults to an instance of the Parser that ships with socket.io.
*/ */
@@ -285,7 +94,7 @@ interface ManagerReservedEvents {
error: (err: Error) => void error: (err: Error) => void
ping: () => void ping: () => void
packet: (packet: Packet) => void packet: (packet: Packet) => void
close: (reason: string) => void close: (reason: string, description?: DisconnectDescription) => void
reconnect_failed: () => void reconnect_failed: () => void
reconnect_attempt: (attempt: number) => void reconnect_attempt: (attempt: number) => void
reconnect_error: (err: Error) => void reconnect_error: (err: Error) => void
@@ -295,13 +104,13 @@ interface ManagerReservedEvents {
export class Manager< export class Manager<
ListenEvents extends EventsMap = DefaultEventsMap, ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents EmitEvents extends EventsMap = ListenEvents
> extends StrictEventEmitter<{}, {}, ManagerReservedEvents> { > extends Emitter<{}, {}, ManagerReservedEvents> {
/** /**
* The Engine.IO client instance * The Engine.IO client instance
* *
* @public * @public
*/ */
public engine: any public engine: Engine
/** /**
* @private * @private
*/ */
@@ -320,7 +129,9 @@ export class Manager<
private nsps: Record<string, Socket> = {}; private nsps: Record<string, Socket> = {};
private subs: Array<ReturnType<typeof on>> = []; private subs: Array<ReturnType<typeof on>> = [];
// @ts-ignore
private backoff: Backoff private backoff: Backoff
private setTimeoutFn: typeof setTimeout
private _reconnection: boolean private _reconnection: boolean
private _reconnectionAttempts: number private _reconnectionAttempts: number
private _reconnectionDelay: number private _reconnectionDelay: number
@@ -358,6 +169,7 @@ export class Manager<
opts.path = opts.path || "/socket.io" opts.path = opts.path || "/socket.io"
this.opts = opts this.opts = opts
installTimerFunctions(this, opts)
this.reconnection(opts.reconnection !== false) this.reconnection(opts.reconnection !== false)
this.reconnectionAttempts(opts.reconnectionAttempts || Infinity) this.reconnectionAttempts(opts.reconnectionAttempts || Infinity)
this.reconnectionDelay(opts.reconnectionDelay || 1000) this.reconnectionDelay(opts.reconnectionDelay || 1000)
@@ -507,8 +319,7 @@ export class Manager<
if (~this._readyState.indexOf("open")) return this if (~this._readyState.indexOf("open")) return this
debug("opening %s", this.uri) debug("opening %s", this.uri)
// @ts-ignore this.engine = new Engine(this.uri, this.opts)
this.engine = eio(this.uri, this.opts)
const socket = this.engine const socket = this.engine
const self = this const self = this
this._readyState = "opening" this._readyState = "opening"
@@ -543,10 +354,11 @@ export class Manager<
} }
// set timer // set timer
const timer = setTimeout(() => { const timer = this.setTimeoutFn(() => {
debug("connect attempt timed out after %d", timeout) debug("connect attempt timed out after %d", timeout)
openSubDestroy() openSubDestroy()
socket.close() socket.close()
// @ts-ignore
socket.emit("error", new Error("timeout")) socket.emit("error", new Error("timeout"))
}, timeout) }, timeout)
@@ -616,7 +428,11 @@ export class Manager<
* @private * @private
*/ */
private ondata(data): void { private ondata(data): void {
this.decoder.add(data) try {
this.decoder.add(data)
} catch (e) {
this.onclose("parse error", e as Error)
}
} }
/** /**
@@ -625,7 +441,10 @@ export class Manager<
* @private * @private
*/ */
private ondecoded(packet): void { private ondecoded(packet): void {
this.emitReserved("packet", packet) // the nextTick call prevents an exception in a user-provided event listener from triggering a disconnection due to a "parse error"
nextTick(() => {
this.emitReserved("packet", packet)
}, this.setTimeoutFn)
} }
/** /**
@@ -651,6 +470,10 @@ export class Manager<
this.nsps[nsp] = socket this.nsps[nsp] = socket
} }
if (this._autoConnect) {
socket.connect()
}
return socket return socket
} }
@@ -713,13 +536,7 @@ export class Manager<
debug("disconnect") debug("disconnect")
this.skipReconnect = true this.skipReconnect = true
this._reconnecting = false this._reconnecting = false
if ("opening" === this._readyState) { this.onclose("forced close")
// `onclose` will not fire because
// an open event never happened
this.cleanup()
}
this.backoff.reset()
this._readyState = "closed"
if (this.engine) this.engine.close() if (this.engine) this.engine.close()
} }
@@ -737,13 +554,13 @@ export class Manager<
* *
* @private * @private
*/ */
private onclose(reason: string): void { private onclose(reason: string, description?: DisconnectDescription): void {
debug("onclose") debug("closed due to %s", reason)
this.cleanup() this.cleanup()
this.backoff.reset() this.backoff.reset()
this._readyState = "closed" this._readyState = "closed"
this.emitReserved("close", reason) this.emitReserved("close", reason, description)
if (this._reconnection && !this.skipReconnect) { if (this._reconnection && !this.skipReconnect) {
this.reconnect() this.reconnect()
@@ -770,7 +587,7 @@ export class Manager<
debug("will wait %dms before reconnect attempt", delay) debug("will wait %dms before reconnect attempt", delay)
this._reconnecting = true this._reconnecting = true
const timer = setTimeout(() => { const timer = this.setTimeoutFn(() => {
if (self.skipReconnect) return if (self.skipReconnect) return
debug("attempting reconnect") debug("attempting reconnect")

View File

@@ -1,9 +1,7 @@
// import type * as Emitter from "component-emitter"; import { Emitter } from "@socket.io/component-emitter"
import { EventEmitter } from "events"
import { StrictEventEmitter } from "./typed-events"
export function on( export function on(
obj: EventEmitter | StrictEventEmitter<any, any>, obj: Emitter<any, any>,
ev: string, ev: string,
fn: (err?: any) => any fn: (err?: any) => any
): VoidFunction { ): VoidFunction {

View File

@@ -1,21 +1,82 @@
// import { Packet, PacketType } from "socket.io-parser"
import { Packet, PacketType } from "../socket.io-parser" import { Packet, PacketType } from "../socket.io-parser"
import { on } from "./on" import { on } from "./on.js"
import { Manager } from "./manager" import { Manager } from "./manager.js"
import { import {
DefaultEventsMap, DefaultEventsMap,
EventNames, EventNames,
EventParams, EventParams,
EventsMap, EventsMap,
StrictEventEmitter, Emitter,
} from "./typed-events" } from "@socket.io/component-emitter"
// import debugModule from "debug" // debug()
// const debug = debugModule("socket.io-client:socket") // debug()
const debug = require("../debug")("socket.io-client") const debug = require("../debug")("socket.io-client")
type PrependTimeoutError<T extends any[]> = {
[K in keyof T]: T[K] extends (...args: infer Params) => infer Result
? (err: Error, ...args: Params) => Result
: T[K]
}
/**
* Utility type to decorate the acknowledgement callbacks with a timeout error.
*
* This is needed because the timeout() flag breaks the symmetry between the sender and the receiver:
*
* @example
* interface Events {
* "my-event": (val: string) => void;
* }
*
* socket.on("my-event", (cb) => {
* cb("123"); // one single argument here
* });
*
* socket.timeout(1000).emit("my-event", (err, val) => {
* // two arguments there (the "err" argument is not properly typed)
* });
*
*/
export type DecorateAcknowledgements<E> = {
[K in keyof E]: E[K] extends (...args: infer Params) => infer Result
? (...args: PrependTimeoutError<Params>) => Result
: E[K]
}
export type Last<T extends any[]> = T extends [...infer H, infer L] ? L : any
export type AllButLast<T extends any[]> = T extends [...infer H, infer L]
? H
: any[]
export type FirstArg<T> = T extends (arg: infer Param) => infer Result
? Param
: any
export interface SocketOptions { export interface SocketOptions {
/** /**
* the authentication payload sent when connecting to the Namespace * the authentication payload sent when connecting to the Namespace
*/ */
auth: { [key: string]: any } | ((cb: (data: object) => void) => void) auth?: { [key: string]: any } | ((cb: (data: object) => void) => void)
/**
* The maximum number of retries. Above the limit, the packet will be discarded.
*
* Using `Infinity` means the delivery guarantee is "at-least-once" (instead of "at-most-once" by default), but a
* smaller value like 10 should be sufficient in practice.
*/
retries?: number
/**
* The default timeout in milliseconds used when waiting for an acknowledgement.
*/
ackTimeout?: number
}
type QueuedPacket = {
id: number
args: unknown[]
flags: Flags
pending: boolean
tryCount: number
} }
/** /**
@@ -35,58 +96,180 @@ const RESERVED_EVENTS = Object.freeze({
interface Flags { interface Flags {
compress?: boolean compress?: boolean
volatile?: boolean volatile?: boolean
timeout?: number
fromQueue?: boolean
} }
export type DisconnectDescription =
| Error
| {
description: string
context?: unknown // context should be typed as CloseEvent | XMLHttpRequest, but these types are not available on non-browser platforms
}
interface SocketReservedEvents { interface SocketReservedEvents {
connect: () => void connect: () => void
connect_error: (err: Error) => void connect_error: (err: Error) => void
disconnect: (reason: Socket.DisconnectReason) => void disconnect: (
reason: Socket.DisconnectReason,
description?: DisconnectDescription
) => void
} }
/**
* A Socket is the fundamental class for interacting with the server.
*
* A Socket belongs to a certain Namespace (by default /) and uses an underlying {@link Manager} to communicate.
*
* @example
* const socket = io();
*
* socket.on("connect", () => {
* console.log("connected");
* });
*
* // send an event to the server
* socket.emit("foo", "bar");
*
* socket.on("foobar", () => {
* // an event was received from the server
* });
*
* // upon disconnection
* socket.on("disconnect", (reason) => {
* console.log(`disconnected due to ${reason}`);
* });
*/
export class Socket< export class Socket<
ListenEvents extends EventsMap = DefaultEventsMap, ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents EmitEvents extends EventsMap = ListenEvents
> extends StrictEventEmitter<ListenEvents, EmitEvents, SocketReservedEvents> { > extends Emitter<ListenEvents, EmitEvents, SocketReservedEvents> {
public readonly io: Manager<ListenEvents, EmitEvents> public readonly io: Manager<ListenEvents, EmitEvents>
/**
* A unique identifier for the session.
*
* @example
* const socket = io();
*
* console.log(socket.id); // undefined
*
* socket.on("connect", () => {
* console.log(socket.id); // "G5p5..."
* });
*/
public id: string public id: string
public connected: boolean
public disconnected: boolean
/**
* The session ID used for connection state recovery, which must not be shared (unlike {@link id}).
*
* @private
*/
private _pid: string
/**
* The offset of the last received packet, which will be sent upon reconnection to allow for the recovery of the connection state.
*
* @private
*/
private _lastOffset: string
/**
* Whether the socket is currently connected to the server.
*
* @example
* const socket = io();
*
* socket.on("connect", () => {
* console.log(socket.connected); // true
* });
*
* socket.on("disconnect", () => {
* console.log(socket.connected); // false
* });
*/
public connected: boolean = false;
/**
* Whether the connection state was recovered after a temporary disconnection. In that case, any missed packets will
* be transmitted by the server.
*/
public recovered: boolean = false;
/**
* Credentials that are sent when accessing a namespace.
*
* @example
* const socket = io({
* auth: {
* token: "abcd"
* }
* });
*
* // or with a function
* const socket = io({
* auth: (cb) => {
* cb({ token: localStorage.token })
* }
* });
*/
public auth: { [key: string]: any } | ((cb: (data: object) => void) => void) public auth: { [key: string]: any } | ((cb: (data: object) => void) => void)
/**
* Buffer for packets received before the CONNECT packet
*/
public receiveBuffer: Array<ReadonlyArray<any>> = []; public receiveBuffer: Array<ReadonlyArray<any>> = [];
/**
* Buffer for packets that will be sent once the socket is connected
*/
public sendBuffer: Array<Packet> = []; public sendBuffer: Array<Packet> = [];
/**
* The queue of packets to be sent with retry in case of failure.
*
* Packets are sent one by one, each waiting for the server acknowledgement, in order to guarantee the delivery order.
* @private
*/
private _queue: Array<QueuedPacket> = [];
private readonly nsp: string private readonly nsp: string
private readonly _opts: SocketOptions
private ids: number = 0; private ids: number = 0;
private acks: object = {}; private acks: object = {};
private flags: Flags = {}; private flags: Flags = {};
private subs?: Array<VoidFunction> private subs?: Array<VoidFunction>
private _anyListeners: Array<(...args: any[]) => void> private _anyListeners: Array<(...args: any[]) => void>
private _anyOutgoingListeners: Array<(...args: any[]) => void>
/** /**
* `Socket` constructor. * `Socket` constructor.
*
* @public
*/ */
constructor(io: Manager, nsp: string, opts?: Partial<SocketOptions>) { constructor(io: Manager, nsp: string, opts?: Partial<SocketOptions>) {
super() super()
this.io = io this.io = io
this.nsp = nsp this.nsp = nsp
this.ids = 0
this.acks = {}
this.receiveBuffer = []
this.sendBuffer = []
this.connected = false
this.disconnected = true
this.flags = {}
if (opts && opts.auth) { if (opts && opts.auth) {
this.auth = opts.auth this.auth = opts.auth
} }
this._opts = Object.assign({}, opts)
if (this.io._autoConnect) this.open() if (this.io._autoConnect) this.open()
} }
/**
* Whether the socket is currently disconnected
*
* @example
* const socket = io();
*
* socket.on("connect", () => {
* console.log(socket.disconnected); // false
* });
*
* socket.on("disconnect", () => {
* console.log(socket.disconnected); // true
* });
*/
public get disconnected(): boolean {
return !this.connected
}
/** /**
* Subscribe to open, close and packet events * Subscribe to open, close and packet events
* *
@@ -105,7 +288,21 @@ export class Socket<
} }
/** /**
* Whether the Socket will try to reconnect when its Manager connects or reconnects * Whether the Socket will try to reconnect when its Manager connects or reconnects.
*
* @example
* const socket = io();
*
* console.log(socket.active); // true
*
* socket.on("disconnect", (reason) => {
* if (reason === "io server disconnect") {
* // the disconnection was initiated by the server, you need to manually reconnect
* console.log(socket.active); // false
* }
* // else the socket will automatically try to reconnect
* console.log(socket.active); // true
* });
*/ */
public get active(): boolean { public get active(): boolean {
return !!this.subs return !!this.subs
@@ -114,7 +311,12 @@ export class Socket<
/** /**
* "Opens" the socket. * "Opens" the socket.
* *
* @public * @example
* const socket = io({
* autoConnect: false
* });
*
* socket.connect();
*/ */
public connect(): this { public connect(): this {
if (this.connected) return this if (this.connected) return this
@@ -126,7 +328,7 @@ export class Socket<
} }
/** /**
* Alias for connect() * Alias for {@link connect()}.
*/ */
public open(): this { public open(): this {
return this.connect() return this.connect()
@@ -135,8 +337,17 @@ export class Socket<
/** /**
* Sends a `message` event. * Sends a `message` event.
* *
* This method mimics the WebSocket.send() method.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
*
* @example
* socket.send("hello");
*
* // this is equivalent to
* socket.emit("message", "hello");
*
* @return self * @return self
* @public
*/ */
public send(...args: any[]): this { public send(...args: any[]): this {
args.unshift("message") args.unshift("message")
@@ -149,18 +360,34 @@ export class Socket<
* Override `emit`. * Override `emit`.
* If the event is in `events`, it's emitted normally. * If the event is in `events`, it's emitted normally.
* *
* @example
* socket.emit("hello", "world");
*
* // all serializable datastructures are supported (no need to call JSON.stringify)
* socket.emit("hello", 1, "2", { 3: ["4"], 5: Uint8Array.from([6]) });
*
* // with an acknowledgement from the server
* socket.emit("hello", "world", (val) => {
* // ...
* });
*
* @return self * @return self
* @public
*/ */
public emit<Ev extends EventNames<EmitEvents>>( public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev, ev: Ev,
...args: EventParams<EmitEvents, Ev> ...args: EventParams<EmitEvents, Ev>
): this { ): this {
if (RESERVED_EVENTS.hasOwnProperty(ev)) { if (RESERVED_EVENTS.hasOwnProperty(ev)) {
throw new Error('"' + ev + '" is a reserved event name') throw new Error('"' + ev.toString() + '" is a reserved event name')
} }
args.unshift(ev) args.unshift(ev)
if (this._opts.retries && !this.flags.fromQueue && !this.flags.volatile) {
this._addToQueue(args)
return this
}
const packet: any = { const packet: any = {
type: PacketType.EVENT, type: PacketType.EVENT,
data: args, data: args,
@@ -171,9 +398,12 @@ export class Socket<
// event ack callback // event ack callback
if ("function" === typeof args[args.length - 1]) { if ("function" === typeof args[args.length - 1]) {
debug("emitting packet with ack id %d", this.ids) const id = this.ids++
this.acks[this.ids] = args.pop() debug("emitting packet with ack id %d", id)
packet.id = this.ids++
const ack = args.pop() as Function
this._registerAckCallback(id, ack)
packet.id = id
} }
const isTransportWritable = const isTransportWritable =
@@ -186,6 +416,7 @@ export class Socket<
if (discardPacket) { if (discardPacket) {
debug("discard packet as the transport is not currently writable") debug("discard packet as the transport is not currently writable")
} else if (this.connected) { } else if (this.connected) {
this.notifyOutgoingListeners(packet)
this.packet(packet) this.packet(packet)
} else { } else {
this.sendBuffer.push(packet) this.sendBuffer.push(packet)
@@ -196,6 +427,152 @@ export class Socket<
return this return this
} }
/**
* @private
*/
private _registerAckCallback(id: number, ack: Function) {
const timeout = this.flags.timeout ?? this._opts.ackTimeout
if (timeout === undefined) {
this.acks[id] = ack
return
}
// @ts-ignore
const timer = this.io.setTimeoutFn(() => {
delete this.acks[id]
for (let i = 0; i < this.sendBuffer.length; i++) {
if (this.sendBuffer[i].id === id) {
debug("removing packet with ack id %d from the buffer", id)
this.sendBuffer.splice(i, 1)
}
}
debug("event with ack id %d has timed out after %d ms", id, timeout)
ack.call(this, new Error("operation has timed out"))
}, timeout)
this.acks[id] = (...args) => {
// @ts-ignore
this.io.clearTimeoutFn(timer)
ack.apply(this, [null, ...args])
}
}
/**
* Emits an event and waits for an acknowledgement
*
* @example
* // without timeout
* const response = await socket.emitWithAck("hello", "world");
*
* // with a specific timeout
* try {
* const response = await socket.timeout(1000).emitWithAck("hello", "world");
* } catch (err) {
* // the server did not acknowledge the event in the given delay
* }
*
* @return a Promise that will be fulfilled when the server acknowledges the event
*/
public emitWithAck<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: AllButLast<EventParams<EmitEvents, Ev>>
): Promise<FirstArg<Last<EventParams<EmitEvents, Ev>>>> {
// the timeout flag is optional
const withErr =
this.flags.timeout !== undefined || this._opts.ackTimeout !== undefined
return new Promise((resolve, reject) => {
args.push((arg1, arg2) => {
if (withErr) {
return arg1 ? reject(arg1) : resolve(arg2)
} else {
return resolve(arg1)
}
})
this.emit(ev, ...(args as any[] as EventParams<EmitEvents, Ev>))
})
}
/**
* Add the packet to the queue.
* @param args
* @private
*/
private _addToQueue(args: unknown[]) {
let ack
if (typeof args[args.length - 1] === "function") {
ack = args.pop()
}
const packet = {
id: this.ids++,
tryCount: 0,
pending: false,
args,
flags: Object.assign({ fromQueue: true }, this.flags),
}
args.push((err, ...responseArgs) => {
if (packet !== this._queue[0]) {
// the packet has already been acknowledged
return
}
const hasError = err !== null
if (hasError) {
if (packet.tryCount > this._opts.retries) {
debug(
"packet [%d] is discarded after %d tries",
packet.id,
packet.tryCount
)
this._queue.shift()
if (ack) {
ack(err)
}
}
} else {
debug("packet [%d] was successfully sent", packet.id)
this._queue.shift()
if (ack) {
ack(null, ...responseArgs)
}
}
packet.pending = false
return this._drainQueue()
})
this._queue.push(packet)
this._drainQueue()
}
/**
* Send the first packet of the queue, and wait for an acknowledgement from the server.
* @private
*/
private _drainQueue() {
debug("draining queue")
if (this._queue.length === 0) {
return
}
const packet = this._queue[0]
if (packet.pending) {
debug(
"packet [%d] has already been sent and is waiting for an ack",
packet.id
)
return
}
packet.pending = true
packet.tryCount++
debug("sending packet [%d] (try n°%d)", packet.id, packet.tryCount)
const currentId = this.ids
this.ids = packet.id // the same id is reused for consecutive retries, in order to allow deduplication on the server side
this.flags = packet.flags
// @ts-ignore
this.emit.apply(this, packet.args)
this.ids = currentId // restore offset
}
/** /**
* Sends a packet. * Sends a packet.
* *
@@ -216,13 +593,28 @@ export class Socket<
debug("transport is open - connecting") debug("transport is open - connecting")
if (typeof this.auth == "function") { if (typeof this.auth == "function") {
this.auth((data) => { this.auth((data) => {
this.packet({ type: PacketType.CONNECT, data }) this._sendConnectPacket(data as Record<string, unknown>)
}) })
} else { } else {
this.packet({ type: PacketType.CONNECT, data: this.auth }) this._sendConnectPacket(this.auth)
} }
} }
/**
* Sends a CONNECT packet to initiate the Socket.IO session.
*
* @param data
* @private
*/
private _sendConnectPacket(data: Record<string, unknown>) {
this.packet({
type: PacketType.CONNECT,
data: this._pid
? Object.assign({ pid: this._pid, offset: this._lastOffset }, data)
: data,
})
}
/** /**
* Called upon engine or manager `error`. * Called upon engine or manager `error`.
* *
@@ -239,14 +631,17 @@ export class Socket<
* Called upon engine `close`. * Called upon engine `close`.
* *
* @param reason * @param reason
* @param description
* @private * @private
*/ */
private onclose(reason: Socket.DisconnectReason): void { private onclose(
reason: Socket.DisconnectReason,
description?: DisconnectDescription
): void {
debug("close (%s)", reason) debug("close (%s)", reason)
this.connected = false this.connected = false
this.disconnected = true
delete this.id delete this.id
this.emitReserved("disconnect", reason) this.emitReserved("disconnect", reason, description)
} }
/** /**
@@ -263,8 +658,7 @@ export class Socket<
switch (packet.type) { switch (packet.type) {
case PacketType.CONNECT: case PacketType.CONNECT:
if (packet.data && packet.data.sid) { if (packet.data && packet.data.sid) {
const id = packet.data.sid this.onconnect(packet.data.sid, packet.data.pid)
this.onconnect(id)
} else { } else {
this.emitReserved( this.emitReserved(
"connect_error", "connect_error",
@@ -276,17 +670,11 @@ export class Socket<
break break
case PacketType.EVENT: case PacketType.EVENT:
this.onevent(packet)
break
case PacketType.BINARY_EVENT: case PacketType.BINARY_EVENT:
this.onevent(packet) this.onevent(packet)
break break
case PacketType.ACK: case PacketType.ACK:
this.onack(packet)
break
case PacketType.BINARY_ACK: case PacketType.BINARY_ACK:
this.onack(packet) this.onack(packet)
break break
@@ -296,6 +684,7 @@ export class Socket<
break break
case PacketType.CONNECT_ERROR: case PacketType.CONNECT_ERROR:
this.destroy()
const err = new Error(packet.data.message) const err = new Error(packet.data.message)
// @ts-ignore // @ts-ignore
err.data = packet.data.data err.data = packet.data.data
@@ -336,6 +725,9 @@ export class Socket<
} }
// @ts-ignore // @ts-ignore
super.emit.apply(this, args) super.emit.apply(this, args)
if (this._pid && args.length && typeof args[args.length - 1] === "string") {
this._lastOffset = args[args.length - 1]
}
} }
/** /**
@@ -382,11 +774,12 @@ export class Socket<
* *
* @private * @private
*/ */
private onconnect(id: string): void { private onconnect(id: string, pid: string) {
debug("socket connected with id %s", id) debug("socket connected with id %s", id)
this.id = id this.id = id
this.recovered = pid && this._pid === pid
this._pid = pid // defined only if connection state recovery is enabled
this.connected = true this.connected = true
this.disconnected = false
this.emitBuffered() this.emitBuffered()
this.emitReserved("connect") this.emitReserved("connect")
} }
@@ -400,7 +793,10 @@ export class Socket<
this.receiveBuffer.forEach((args) => this.emitEvent(args)) this.receiveBuffer.forEach((args) => this.emitEvent(args))
this.receiveBuffer = [] this.receiveBuffer = []
this.sendBuffer.forEach((packet) => this.packet(packet)) this.sendBuffer.forEach((packet) => {
this.notifyOutgoingListeners(packet)
this.packet(packet)
})
this.sendBuffer = [] this.sendBuffer = []
} }
@@ -432,10 +828,20 @@ export class Socket<
} }
/** /**
* Disconnects the socket manually. * Disconnects the socket manually. In that case, the socket will not try to reconnect.
*
* If this is the last active Socket instance of the {@link Manager}, the low-level connection will be closed.
*
* @example
* const socket = io();
*
* socket.on("disconnect", (reason) => {
* // console.log(reason); prints "io client disconnect"
* });
*
* socket.disconnect();
* *
* @return self * @return self
* @public
*/ */
public disconnect(): this { public disconnect(): this {
if (this.connected) { if (this.connected) {
@@ -454,10 +860,9 @@ export class Socket<
} }
/** /**
* Alias for disconnect() * Alias for {@link disconnect()}.
* *
* @return self * @return self
* @public
*/ */
public close(): this { public close(): this {
return this.disconnect() return this.disconnect()
@@ -466,9 +871,11 @@ export class Socket<
/** /**
* Sets the compress flag. * Sets the compress flag.
* *
* @example
* socket.compress(false).emit("hello");
*
* @param compress - if `true`, compresses the sending data * @param compress - if `true`, compresses the sending data
* @return self * @return self
* @public
*/ */
public compress(compress: boolean): this { public compress(compress: boolean): this {
this.flags.compress = compress this.flags.compress = compress
@@ -479,20 +886,46 @@ export class Socket<
* Sets a modifier for a subsequent event emission that the event message will be dropped when this socket is not * Sets a modifier for a subsequent event emission that the event message will be dropped when this socket is not
* ready to send messages. * ready to send messages.
* *
* @example
* socket.volatile.emit("hello"); // the server may or may not receive it
*
* @returns self * @returns self
* @public
*/ */
public get volatile(): this { public get volatile(): this {
this.flags.volatile = true this.flags.volatile = true
return this return this
} }
/**
* Sets a modifier for a subsequent event emission that the callback will be called with an error when the
* given number of milliseconds have elapsed without an acknowledgement from the server:
*
* @example
* socket.timeout(5000).emit("my-event", (err) => {
* if (err) {
* // the server did not acknowledge the event in the given delay
* }
* });
*
* @returns self
*/
public timeout(
timeout: number
): Socket<ListenEvents, DecorateAcknowledgements<EmitEvents>> {
this.flags.timeout = timeout
return this
}
/** /**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback. * callback.
* *
* @example
* socket.onAny((event, ...args) => {
* console.log(`got ${event}`);
* });
*
* @param listener * @param listener
* @public
*/ */
public onAny(listener: (...args: any[]) => void): this { public onAny(listener: (...args: any[]) => void): this {
this._anyListeners = this._anyListeners || [] this._anyListeners = this._anyListeners || []
@@ -504,8 +937,12 @@ export class Socket<
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback. The listener is added to the beginning of the listeners array. * callback. The listener is added to the beginning of the listeners array.
* *
* @example
* socket.prependAny((event, ...args) => {
* console.log(`got event ${event}`);
* });
*
* @param listener * @param listener
* @public
*/ */
public prependAny(listener: (...args: any[]) => void): this { public prependAny(listener: (...args: any[]) => void): this {
this._anyListeners = this._anyListeners || [] this._anyListeners = this._anyListeners || []
@@ -516,8 +953,20 @@ export class Socket<
/** /**
* Removes the listener that will be fired when any event is emitted. * Removes the listener that will be fired when any event is emitted.
* *
* @example
* const catchAllListener = (event, ...args) => {
* console.log(`got event ${event}`);
* }
*
* socket.onAny(catchAllListener);
*
* // remove a specific listener
* socket.offAny(catchAllListener);
*
* // or remove all listeners
* socket.offAny();
*
* @param listener * @param listener
* @public
*/ */
public offAny(listener?: (...args: any[]) => void): this { public offAny(listener?: (...args: any[]) => void): this {
if (!this._anyListeners) { if (!this._anyListeners) {
@@ -540,12 +989,108 @@ export class Socket<
/** /**
* Returns an array of listeners that are listening for any event that is specified. This array can be manipulated, * Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,
* e.g. to remove listeners. * e.g. to remove listeners.
*
* @public
*/ */
public listenersAny() { public listenersAny() {
return this._anyListeners || [] return this._anyListeners || []
} }
/**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback.
*
* Note: acknowledgements sent to the server are not included.
*
* @example
* socket.onAnyOutgoing((event, ...args) => {
* console.log(`sent event ${event}`);
* });
*
* @param listener
*/
public onAnyOutgoing(listener: (...args: any[]) => void): this {
this._anyOutgoingListeners = this._anyOutgoingListeners || []
this._anyOutgoingListeners.push(listener)
return this
}
/**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback. The listener is added to the beginning of the listeners array.
*
* Note: acknowledgements sent to the server are not included.
*
* @example
* socket.prependAnyOutgoing((event, ...args) => {
* console.log(`sent event ${event}`);
* });
*
* @param listener
*/
public prependAnyOutgoing(listener: (...args: any[]) => void): this {
this._anyOutgoingListeners = this._anyOutgoingListeners || []
this._anyOutgoingListeners.unshift(listener)
return this
}
/**
* Removes the listener that will be fired when any event is emitted.
*
* @example
* const catchAllListener = (event, ...args) => {
* console.log(`sent event ${event}`);
* }
*
* socket.onAnyOutgoing(catchAllListener);
*
* // remove a specific listener
* socket.offAnyOutgoing(catchAllListener);
*
* // or remove all listeners
* socket.offAnyOutgoing();
*
* @param [listener] - the catch-all listener (optional)
*/
public offAnyOutgoing(listener?: (...args: any[]) => void): this {
if (!this._anyOutgoingListeners) {
return this
}
if (listener) {
const listeners = this._anyOutgoingListeners
for (let i = 0; i < listeners.length; i++) {
if (listener === listeners[i]) {
listeners.splice(i, 1)
return this
}
}
} else {
this._anyOutgoingListeners = []
}
return this
}
/**
* Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,
* e.g. to remove listeners.
*/
public listenersAnyOutgoing() {
return this._anyOutgoingListeners || []
}
/**
* Notify the listeners for each packet sent
*
* @param packet
*
* @private
*/
private notifyOutgoingListeners(packet: Packet) {
if (this._anyOutgoingListeners && this._anyOutgoingListeners.length) {
const listeners = this._anyOutgoingListeners.slice()
for (const listener of listeners) {
listener.apply(this, packet.data)
}
}
}
} }
export namespace Socket { export namespace Socket {
@@ -555,4 +1100,5 @@ export namespace Socket {
| "ping timeout" | "ping timeout"
| "transport close" | "transport close"
| "transport error" | "transport error"
| "parse error"
} }

View File

@@ -1,157 +0,0 @@
import { EventEmitter } from "events"
/**
* An events map is an interface that maps event names to their value, which
* represents the type of the `on` listener.
*/
export interface EventsMap {
[event: string]: any
}
/**
* The default events map, used if no EventsMap is given. Using this EventsMap
* is equivalent to accepting all event names, and any data.
*/
export interface DefaultEventsMap {
[event: string]: (...args: any[]) => void
}
/**
* Returns a union type containing all the keys of an event map.
*/
export type EventNames<Map extends EventsMap> = keyof Map & (string | symbol)
/** The tuple type representing the parameters of an event listener */
export type EventParams<
Map extends EventsMap,
Ev extends EventNames<Map>
> = Parameters<Map[Ev]>
/**
* The event names that are either in ReservedEvents or in UserEvents
*/
export type ReservedOrUserEventNames<
ReservedEventsMap extends EventsMap,
UserEvents extends EventsMap
> = EventNames<ReservedEventsMap> | EventNames<UserEvents>
/**
* Type of a listener of a user event or a reserved event. If `Ev` is in
* `ReservedEvents`, the reserved event listener is returned.
*/
export type ReservedOrUserListener<
ReservedEvents extends EventsMap,
UserEvents extends EventsMap,
Ev extends ReservedOrUserEventNames<ReservedEvents, UserEvents>
> = FallbackToUntypedListener<
Ev extends EventNames<ReservedEvents>
? ReservedEvents[Ev]
: Ev extends EventNames<UserEvents>
? UserEvents[Ev]
: never
>
/**
* Returns an untyped listener type if `T` is `never`; otherwise, returns `T`.
*
* This is a hack to mitigate https://github.com/socketio/socket.io/issues/3833.
* Needed because of https://github.com/microsoft/TypeScript/issues/41778
*/
type FallbackToUntypedListener<T> = [T] extends [never]
? (...args: any[]) => void
: T
/**
* Strictly typed version of an `EventEmitter`. A `TypedEventEmitter` takes type
* parameters for mappings of event names to event data types, and strictly
* types method calls to the `EventEmitter` according to these event maps.
*
* @typeParam ListenEvents - `EventsMap` of user-defined events that can be
* listened to with `on` or `once`
* @typeParam EmitEvents - `EventsMap` of user-defined events that can be
* emitted with `emit`
* @typeParam ReservedEvents - `EventsMap` of reserved events, that can be
* emitted by socket.io with `emitReserved`, and can be listened to with
* `listen`.
*/
export abstract class StrictEventEmitter<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap,
ReservedEvents extends EventsMap = {}
> extends EventEmitter {
/**
* Adds the `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
on<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this {
super.on(ev as string, listener)
return this
}
/**
* Adds a one-time `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
once<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this {
super.once(ev as string, listener)
return this
}
/**
* Emits an event.
*
* @param ev Name of the event
* @param args Values to send to listeners of this event
*/
// @ts-ignore
emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): this {
super.emit(ev as string, ...args)
return this
}
/**
* Emits a reserved event.
*
* This method is `protected`, so that only a class extending
* `StrictEventEmitter` can emit its own reserved events.
*
* @param ev Reserved event name
* @param args Arguments to emit along with the event
*/
protected emitReserved<Ev extends EventNames<ReservedEvents>>(
ev: Ev,
...args: EventParams<ReservedEvents, Ev>
): this {
super.emit(ev as string, ...args)
return this
}
/**
* Returns the listeners listening to an event.
*
* @param event Event name
* @returns Array of listeners subscribed to `event`
*/
listeners<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
event: Ev
): ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>[] {
return super.listeners(event as string) as ReservedOrUserListener<
ReservedEvents,
ListenEvents,
Ev
>[]
}
}

View File

@@ -1,5 +1,8 @@
import * as parseuri from "parseuri" // import { parse } from "engine.io-client"
import { parse } from "../engine.io-client"
// import debugModule from "debug" // debug()
// const debug = debugModule("socket.io-client:url"); // debug()
const debug = require("../debug")("socket.io-client") const debug = require("../debug")("socket.io-client")
type ParsedUrl = { type ParsedUrl = {
@@ -67,7 +70,7 @@ export function url(
// parse // parse
debug("parse %s", uri) debug("parse %s", uri)
obj = parseuri(uri) as ParsedUrl obj = parse(uri) as ParsedUrl
} }
// make sure we treat `localhost:80` and `localhost` equally // make sure we treat `localhost:80` and `localhost` equally

View File

@@ -1,4 +1,4 @@
import { isBinary } from "./is-binary" import { isBinary } from "./is-binary.js";
/** /**
* Replaces every Buffer | ArrayBuffer | Blob | File in packet with a numbered placeholder. * Replaces every Buffer | ArrayBuffer | Blob | File in packet with a numbered placeholder.
@@ -9,37 +9,37 @@ import { isBinary } from "./is-binary"
*/ */
export function deconstructPacket(packet) { export function deconstructPacket(packet) {
const buffers = [] const buffers = [];
const packetData = packet.data const packetData = packet.data;
const pack = packet const pack = packet;
pack.data = _deconstructPacket(packetData, buffers) pack.data = _deconstructPacket(packetData, buffers);
pack.attachments = buffers.length // number of binary 'attachments' pack.attachments = buffers.length; // number of binary 'attachments'
return { packet: pack, buffers: buffers } return { packet: pack, buffers: buffers };
} }
function _deconstructPacket(data, buffers) { function _deconstructPacket(data, buffers) {
if (!data) return data if (!data) return data;
if (isBinary(data)) { if (isBinary(data)) {
const placeholder = { _placeholder: true, num: buffers.length } const placeholder = { _placeholder: true, num: buffers.length };
buffers.push(data) buffers.push(data);
return placeholder return placeholder;
} else if (Array.isArray(data)) { } else if (Array.isArray(data)) {
const newData = new Array(data.length) const newData = new Array(data.length);
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
newData[i] = _deconstructPacket(data[i], buffers) newData[i] = _deconstructPacket(data[i], buffers);
}
return newData
} else if (typeof data === "object" && !(data instanceof Date)) {
const newData = {}
for (const key in data) {
if (data.hasOwnProperty(key)) {
newData[key] = _deconstructPacket(data[key], buffers)
}
}
return newData
} }
return data return newData;
} else if (typeof data === "object" && !(data instanceof Date)) {
const newData = {};
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
newData[key] = _deconstructPacket(data[key], buffers);
}
}
return newData;
}
return data;
} }
/** /**
@@ -52,27 +52,35 @@ function _deconstructPacket(data, buffers) {
*/ */
export function reconstructPacket(packet, buffers) { export function reconstructPacket(packet, buffers) {
packet.data = _reconstructPacket(packet.data, buffers) packet.data = _reconstructPacket(packet.data, buffers);
packet.attachments = undefined // no longer useful delete packet.attachments; // no longer useful
return packet return packet;
} }
function _reconstructPacket(data, buffers) { function _reconstructPacket(data, buffers) {
if (!data) return data if (!data) return data;
if (data && data._placeholder) { if (data && data._placeholder === true) {
return buffers[data.num] // appropriate buffer (should be natural order anyway) const isIndexValid =
} else if (Array.isArray(data)) { typeof data.num === "number" &&
for (let i = 0; i < data.length; i++) { data.num >= 0 &&
data[i] = _reconstructPacket(data[i], buffers) data.num < buffers.length;
} if (isIndexValid) {
} else if (typeof data === "object") { return buffers[data.num]; // appropriate buffer (should be natural order anyway)
for (const key in data) { } else {
if (data.hasOwnProperty(key)) { throw new Error("illegal attachments");
data[key] = _reconstructPacket(data[key], buffers)
}
}
} }
} else if (Array.isArray(data)) {
for (let i = 0; i < data.length; i++) {
data[i] = _reconstructPacket(data[i], buffers);
}
} else if (typeof data === "object") {
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
data[key] = _reconstructPacket(data[key], buffers);
}
}
}
return data return data;
} }

View File

@@ -1,8 +1,9 @@
import EventEmitter = require("events") import { Emitter } from "@socket.io/component-emitter";
import { deconstructPacket, reconstructPacket } from "./binary" import { deconstructPacket, reconstructPacket } from "./binary.js";
import { isBinary, hasBinary } from "./is-binary" import { isBinary, hasBinary } from "./is-binary.js";
// import debugModule from "debug" // debug()
// const debug = require("debug")("socket.io-parser") const debug = require("../debug")("socket.io-parser");
/** /**
* Protocol version. * Protocol version.
@@ -10,24 +11,24 @@ import { isBinary, hasBinary } from "./is-binary"
* @public * @public
*/ */
export const protocol: number = 5 export const protocol: number = 5;
export enum PacketType { export enum PacketType {
CONNECT, CONNECT,
DISCONNECT, DISCONNECT,
EVENT, EVENT,
ACK, ACK,
CONNECT_ERROR, CONNECT_ERROR,
BINARY_EVENT, BINARY_EVENT,
BINARY_ACK, BINARY_ACK,
} }
export interface Packet { export interface Packet {
type: PacketType type: PacketType;
nsp: string nsp: string;
data?: any data?: any;
id?: number id?: number;
attachments?: number attachments?: number;
} }
/** /**
@@ -35,77 +36,91 @@ export interface Packet {
*/ */
export class Encoder { export class Encoder {
/** /**
* Encode a packet as a single string if non-binary, or as a * Encoder constructor
* buffer sequence, depending on packet type. *
* * @param {function} replacer - custom replacer to pass down to JSON.parse
* @param {Object} obj - packet object */
*/ constructor(private replacer?: (this: any, key: string, value: any) => any) {}
public encode(obj: Packet) { /**
console.trace("encoding packet", JSON.stringify(obj)) * Encode a packet as a single string if non-binary, or as a
* buffer sequence, depending on packet type.
*
* @param {Object} obj - packet object
*/
public encode(obj: Packet) {
debug("encoding packet %j", obj);
if (obj.type === PacketType.EVENT || obj.type === PacketType.ACK) { if (obj.type === PacketType.EVENT || obj.type === PacketType.ACK) {
if (hasBinary(obj)) { if (hasBinary(obj)) {
obj.type = return this.encodeAsBinary({
obj.type === PacketType.EVENT type:
? PacketType.BINARY_EVENT obj.type === PacketType.EVENT
: PacketType.BINARY_ACK ? PacketType.BINARY_EVENT
return this.encodeAsBinary(obj) : PacketType.BINARY_ACK,
} nsp: obj.nsp,
} data: obj.data,
return [this.encodeAsString(obj)] id: obj.id,
});
}
}
return [this.encodeAsString(obj)];
}
/**
* Encode packet as string.
*/
private encodeAsString(obj: Packet) {
// first is type
let str = "" + obj.type;
// attachments if we have them
if (
obj.type === PacketType.BINARY_EVENT ||
obj.type === PacketType.BINARY_ACK
) {
str += obj.attachments + "-";
} }
/** // if we have a namespace other than `/`
* Encode packet as string. // we append it followed by a comma `,`
*/ if (obj.nsp && "/" !== obj.nsp) {
str += obj.nsp + ",";
private encodeAsString(obj: Packet) {
// first is type
let str = "" + obj.type
// attachments if we have them
if (
obj.type === PacketType.BINARY_EVENT ||
obj.type === PacketType.BINARY_ACK
) {
str += obj.attachments + "-"
}
// if we have a namespace other than `/`
// we append it followed by a comma `,`
if (obj.nsp && "/" !== obj.nsp) {
str += obj.nsp + ","
}
// immediately followed by the id
if (null != obj.id) {
str += obj.id
}
// json data
if (null != obj.data) {
str += JSON.stringify(obj.data)
}
console.trace("encoded", JSON.stringify(obj), "as", str)
return str
} }
/** // immediately followed by the id
* Encode packet as 'buffer sequence' by removing blobs, and if (null != obj.id) {
* deconstructing packet into object with placeholders and str += obj.id;
* a list of buffers.
*/
private encodeAsBinary(obj: Packet) {
const deconstruction = deconstructPacket(obj)
const pack = this.encodeAsString(deconstruction.packet)
const buffers = deconstruction.buffers
buffers.unshift(pack) // add packet info to beginning of data list
return buffers // write all the buffers
} }
// json data
if (null != obj.data) {
str += JSON.stringify(obj.data, this.replacer);
}
debug("encoded %j as %s", obj, str);
return str;
}
/**
* Encode packet as 'buffer sequence' by removing blobs, and
* deconstructing packet into object with placeholders and
* a list of buffers.
*/
private encodeAsBinary(obj: Packet) {
const deconstruction = deconstructPacket(obj);
const pack = this.encodeAsString(deconstruction.packet);
const buffers = deconstruction.buffers;
buffers.unshift(pack); // add packet info to beginning of data list
return buffers; // write all the buffers
}
}
interface DecoderReservedEvents {
decoded: (packet: Packet) => void;
} }
/** /**
@@ -113,161 +128,169 @@ export class Encoder {
* *
* @return {Object} decoder * @return {Object} decoder
*/ */
export class Decoder extends EventEmitter { export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
private reconstructor: BinaryReconstructor private reconstructor: BinaryReconstructor;
constructor() { /**
super() * Decoder constructor
*
* @param {function} reviver - custom reviver to pass down to JSON.stringify
*/
constructor(private reviver?: (this: any, key: string, value: any) => any) {
super();
}
/**
* Decodes an encoded packet string into packet JSON.
*
* @param {String} obj - encoded packet
*/
public add(obj: any) {
let packet;
if (typeof obj === "string") {
if (this.reconstructor) {
throw new Error("got plaintext data when reconstructing a packet");
}
packet = this.decodeString(obj);
const isBinaryEvent = packet.type === PacketType.BINARY_EVENT;
if (isBinaryEvent || packet.type === PacketType.BINARY_ACK) {
packet.type = isBinaryEvent ? PacketType.EVENT : PacketType.ACK;
// binary packet's json
this.reconstructor = new BinaryReconstructor(packet);
// no attachments, labeled binary but no binary data to follow
if (packet.attachments === 0) {
super.emitReserved("decoded", packet);
}
} else {
// non-binary full packet
super.emitReserved("decoded", packet);
}
} else if (isBinary(obj) || obj.base64) {
// raw binary data
if (!this.reconstructor) {
throw new Error("got binary data when not reconstructing a packet");
} else {
packet = this.reconstructor.takeBinaryData(obj);
if (packet) {
// received final buffer
this.reconstructor = null;
super.emitReserved("decoded", packet);
}
}
} else {
throw new Error("Unknown type: " + obj);
}
}
/**
* Decode a packet String (JSON data)
*
* @param {String} str
* @return {Object} packet
*/
private decodeString(str): Packet {
let i = 0;
// look up type
const p: any = {
type: Number(str.charAt(0)),
};
if (PacketType[p.type] === undefined) {
throw new Error("unknown packet type " + p.type);
} }
/** // look up attachments if type binary
* Decodes an encoded packet string into packet JSON. if (
* p.type === PacketType.BINARY_EVENT ||
* @param {String} obj - encoded packet p.type === PacketType.BINARY_ACK
*/ ) {
const start = i + 1;
public add(obj: any) { while (str.charAt(++i) !== "-" && i != str.length) {}
let packet const buf = str.substring(start, i);
if (typeof obj === "string") { if (buf != Number(buf) || str.charAt(i) !== "-") {
packet = this.decodeString(obj) throw new Error("Illegal attachments");
if ( }
packet.type === PacketType.BINARY_EVENT || p.attachments = Number(buf);
packet.type === PacketType.BINARY_ACK
) {
// binary packet's json
this.reconstructor = new BinaryReconstructor(packet)
// no attachments, labeled binary but no binary data to follow
if (packet.attachments === 0) {
super.emit("decoded", packet)
}
} else {
// non-binary full packet
super.emit("decoded", packet)
}
} else if (isBinary(obj) || obj.base64) {
// raw binary data
if (!this.reconstructor) {
throw new Error("got binary data when not reconstructing a packet")
} else {
packet = this.reconstructor.takeBinaryData(obj)
if (packet) {
// received final buffer
this.reconstructor = null
super.emit("decoded", packet)
}
}
} else {
throw new Error("Unknown type: " + obj)
}
} }
/** // look up namespace (if any)
* Decode a packet String (JSON data) if ("/" === str.charAt(i + 1)) {
* const start = i + 1;
* @param {String} str while (++i) {
* @return {Object} packet const c = str.charAt(i);
*/ if ("," === c) break;
private decodeString(str): Packet { if (i === str.length) break;
let i = 0 }
// look up type p.nsp = str.substring(start, i);
const p: any = { } else {
type: Number(str.charAt(0)), p.nsp = "/";
}
if (PacketType[p.type] === undefined) {
throw new Error("unknown packet type " + p.type)
}
// look up attachments if type binary
if (
p.type === PacketType.BINARY_EVENT ||
p.type === PacketType.BINARY_ACK
) {
const start = i + 1
while (str.charAt(++i) !== "-" && i != str.length) { }
const buf = str.substring(start, i)
if (buf != Number(buf) || str.charAt(i) !== "-") {
throw new Error("Illegal attachments")
}
p.attachments = Number(buf)
}
// look up namespace (if any)
if ("/" === str.charAt(i + 1)) {
const start = i + 1
while (++i) {
const c = str.charAt(i)
if ("," === c) break
if (i === str.length) break
}
p.nsp = str.substring(start, i)
} else {
p.nsp = "/"
}
// look up id
const next = str.charAt(i + 1)
if ("" !== next && Number(next) == next) {
const start = i + 1
while (++i) {
const c = str.charAt(i)
if (null == c || Number(c) != c) {
--i
break
}
if (i === str.length) break
}
p.id = Number(str.substring(start, i + 1))
}
// look up json data
if (str.charAt(++i)) {
const payload = tryParse(str.substr(i))
if (Decoder.isPayloadValid(p.type, payload)) {
p.data = payload
} else {
throw new Error("invalid payload")
}
}
console.trace("decoded", str, "as", p)
return p
} }
private static isPayloadValid(type: PacketType, payload: any): boolean { // look up id
switch (type) { const next = str.charAt(i + 1);
case PacketType.CONNECT: if ("" !== next && Number(next) == next) {
return typeof payload === "object" const start = i + 1;
case PacketType.DISCONNECT: while (++i) {
return payload === undefined const c = str.charAt(i);
case PacketType.CONNECT_ERROR: if (null == c || Number(c) != c) {
return typeof payload === "string" || typeof payload === "object" --i;
case PacketType.EVENT: break;
case PacketType.BINARY_EVENT:
return Array.isArray(payload) && payload.length > 0
case PacketType.ACK:
case PacketType.BINARY_ACK:
return Array.isArray(payload)
} }
if (i === str.length) break;
}
p.id = Number(str.substring(start, i + 1));
} }
/** // look up json data
* Deallocates a parser's resources if (str.charAt(++i)) {
*/ const payload = this.tryParse(str.substr(i));
public destroy() { if (Decoder.isPayloadValid(p.type, payload)) {
if (this.reconstructor) { p.data = payload;
this.reconstructor.finishedReconstruction() } else {
} throw new Error("invalid payload");
}
} }
}
function tryParse(str) { debug("decoded %s as %j", str, p);
return p;
}
private tryParse(str) {
try { try {
return JSON.parse(str) return JSON.parse(str, this.reviver);
} catch (error: any) { } catch (e) {
return false return false;
} }
}
private static isPayloadValid(type: PacketType, payload: any): boolean {
switch (type) {
case PacketType.CONNECT:
return typeof payload === "object";
case PacketType.DISCONNECT:
return payload === undefined;
case PacketType.CONNECT_ERROR:
return typeof payload === "string" || typeof payload === "object";
case PacketType.EVENT:
case PacketType.BINARY_EVENT:
return Array.isArray(payload) && payload.length > 0;
case PacketType.ACK:
case PacketType.BINARY_ACK:
return Array.isArray(payload);
}
}
/**
* Deallocates a parser's resources
*/
public destroy() {
if (this.reconstructor) {
this.reconstructor.finishedReconstruction();
this.reconstructor = null;
}
}
} }
/** /**
@@ -280,37 +303,37 @@ function tryParse(str) {
*/ */
class BinaryReconstructor { class BinaryReconstructor {
private reconPack private reconPack;
private buffers: Array<Buffer | ArrayBuffer> = []; private buffers: Array<Buffer | ArrayBuffer> = [];
constructor(readonly packet: Packet) { constructor(readonly packet: Packet) {
this.reconPack = packet this.reconPack = packet;
} }
/** /**
* Method to be called when binary data received from connection * Method to be called when binary data received from connection
* after a BINARY_EVENT packet. * after a BINARY_EVENT packet.
* *
* @param {Buffer | ArrayBuffer} binData - the raw binary data received * @param {Buffer | ArrayBuffer} binData - the raw binary data received
* @return {null | Object} returns null if more binary data is expected or * @return {null | Object} returns null if more binary data is expected or
* a reconstructed packet object if all buffers have been received. * a reconstructed packet object if all buffers have been received.
*/ */
public takeBinaryData(binData) { public takeBinaryData(binData) {
this.buffers.push(binData) this.buffers.push(binData);
if (this.buffers.length === this.reconPack.attachments) { if (this.buffers.length === this.reconPack.attachments) {
// done with buffer list // done with buffer list
const packet = reconstructPacket(this.reconPack, this.buffers) const packet = reconstructPacket(this.reconPack, this.buffers);
this.finishedReconstruction() this.finishedReconstruction();
return packet return packet;
}
return null
} }
return null;
}
/** /**
* Cleans up binary packet reconstruction variables. * Cleans up binary packet reconstruction variables.
*/ */
public finishedReconstruction() { public finishedReconstruction() {
this.reconPack = null this.reconPack = null;
this.buffers = [] this.buffers = [];
} }
} }

View File

@@ -1,20 +1,20 @@
const withNativeArrayBuffer: boolean = typeof ArrayBuffer === "function" const withNativeArrayBuffer: boolean = typeof ArrayBuffer === "function";
const isView = (obj: any) => { const isView = (obj: any) => {
return typeof ArrayBuffer.isView === "function" return typeof ArrayBuffer.isView === "function"
? ArrayBuffer.isView(obj) ? ArrayBuffer.isView(obj)
: obj.buffer instanceof ArrayBuffer : obj.buffer instanceof ArrayBuffer;
} };
const toString = Object.prototype.toString const toString = Object.prototype.toString;
const withNativeBlob = false const withNativeBlob =
// typeof Blob === "function" || typeof Blob === "function" ||
// (typeof Blob !== "undefined" && (typeof Blob !== "undefined" &&
// toString.call(Blob) === "[object BlobConstructor]") toString.call(Blob) === "[object BlobConstructor]");
const withNativeFile = false const withNativeFile =
// typeof File === "function" || typeof File === "function" ||
// (typeof File !== "undefined" && (typeof File !== "undefined" &&
// toString.call(File) === "[object FileConstructor]") toString.call(File) === "[object FileConstructor]");
/** /**
* Returns true if obj is a Buffer, an ArrayBuffer, a Blob or a File. * Returns true if obj is a Buffer, an ArrayBuffer, a Blob or a File.
@@ -23,43 +23,44 @@ const withNativeFile = false
*/ */
export function isBinary(obj: any) { export function isBinary(obj: any) {
return ( return (
(withNativeArrayBuffer && (obj instanceof ArrayBuffer || isView(obj))) (withNativeArrayBuffer && (obj instanceof ArrayBuffer || isView(obj))) ||
// || (withNativeBlob && obj instanceof Blob) || (withNativeFile && obj instanceof File) (withNativeBlob && obj instanceof Blob) ||
) (withNativeFile && obj instanceof File)
);
} }
export function hasBinary(obj: any, toJSON?: boolean) { export function hasBinary(obj: any, toJSON?: boolean) {
if (!obj || typeof obj !== "object") { if (!obj || typeof obj !== "object") {
return false return false;
} }
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
for (let i = 0, l = obj.length; i < l; i++) { for (let i = 0, l = obj.length; i < l; i++) {
if (hasBinary(obj[i])) { if (hasBinary(obj[i])) {
return true return true;
} }
}
return false
} }
return false;
}
if (isBinary(obj)) { if (isBinary(obj)) {
return true return true;
}
if (
obj.toJSON &&
typeof obj.toJSON === "function" &&
arguments.length === 1
) {
return hasBinary(obj.toJSON(), true);
}
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key) && hasBinary(obj[key])) {
return true;
} }
}
if ( return false;
obj.toJSON &&
typeof obj.toJSON === "function" &&
arguments.length === 1
) {
return hasBinary(obj.toJSON(), true)
}
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key) && hasBinary(obj[key])) {
return true
}
}
return false
} }

View File

@@ -1,320 +1,561 @@
// import type { BroadcastFlags, Room, SocketId } from "socket.io-adapter" import type { BroadcastFlags, Room, SocketId } from "../socket.io-adapter";
import type { BroadcastFlags, Room, SocketId } from "../socket.io-adapter" import { Handshake, RESERVED_EVENTS, Socket } from "./socket";
import { Handshake, RESERVED_EVENTS, Socket } from "./socket" import { PacketType } from "../socket.io-parser";
// import { PacketType } from "socket.io-parser" import type { Adapter } from "../socket.io-adapter";
import { PacketType } from "../socket.io-parser"
// import type { Adapter } from "socket.io-adapter"
import type { Adapter } from "../socket.io-adapter"
import type { import type {
EventParams, EventParams,
EventNames, EventNames,
EventsMap, EventsMap,
TypedEventBroadcaster, TypedEventBroadcaster,
} from "./typed-events" DecorateAcknowledgements,
DecorateAcknowledgementsWithTimeoutAndMultipleResponses,
AllButLast,
Last,
SecondArg,
} from "./typed-events";
export class BroadcastOperator<EmitEvents extends EventsMap> export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
implements TypedEventBroadcaster<EmitEvents> implements TypedEventBroadcaster<EmitEvents>
{ {
constructor( constructor(
private readonly adapter: Adapter, private readonly adapter: Adapter,
private readonly rooms: Set<Room> = new Set<Room>(), private readonly rooms: Set<Room> = new Set<Room>(),
private readonly exceptRooms: Set<Room> = new Set<Room>(), private readonly exceptRooms: Set<Room> = new Set<Room>(),
private readonly flags: BroadcastFlags = {} private readonly flags: BroadcastFlags & {
) { } expectSingleResponse?: boolean;
} = {}
) {}
/** /**
* Targets a room when emitting. * Targets a room when emitting.
* *
* @param room * @example
* @return a new BroadcastOperator instance * // the “foo” event will be broadcast to all connected clients in the “room-101” room
* @public * io.to("room-101").emit("foo", "bar");
*/ *
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> { * // with an array of rooms (a client will be notified at most once)
const rooms = new Set(this.rooms) * io.to(["room-101", "room-102"]).emit("foo", "bar");
if (Array.isArray(room)) { *
room.forEach((r) => rooms.add(r)) * // with multiple chained calls
* io.to("room-101").to("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public to(room: Room | Room[]) {
const rooms = new Set(this.rooms);
if (Array.isArray(room)) {
room.forEach((r) => rooms.add(r));
} else {
rooms.add(room);
}
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
rooms,
this.exceptRooms,
this.flags
);
}
/**
* Targets a room when emitting. Similar to `to()`, but might feel clearer in some cases:
*
* @example
* // disconnect all clients in the "room-101" room
* io.in("room-101").disconnectSockets();
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public in(room: Room | Room[]) {
return this.to(room);
}
/**
* Excludes a room when emitting.
*
* @example
* // the "foo" event will be broadcast to all connected clients, except the ones that are in the "room-101" room
* io.except("room-101").emit("foo", "bar");
*
* // with an array of rooms
* io.except(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* io.except("room-101").except("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public except(room: Room | Room[]) {
const exceptRooms = new Set(this.exceptRooms);
if (Array.isArray(room)) {
room.forEach((r) => exceptRooms.add(r));
} else {
exceptRooms.add(room);
}
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
exceptRooms,
this.flags
);
}
/**
* Sets the compress flag.
*
* @example
* io.compress(false).emit("hello");
*
* @param compress - if `true`, compresses the sending data
* @return a new BroadcastOperator instance
*/
public compress(compress: boolean) {
const flags = Object.assign({}, this.flags, { compress });
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
this.exceptRooms,
flags
);
}
/**
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @example
* io.volatile.emit("hello"); // the clients may or may not receive it
*
* @return a new BroadcastOperator instance
*/
public get volatile() {
const flags = Object.assign({}, this.flags, { volatile: true });
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
this.exceptRooms,
flags
);
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @example
* // the “foo” event will be broadcast to all connected clients on this node
* io.local.emit("foo", "bar");
*
* @return a new {@link BroadcastOperator} instance for chaining
*/
public get local() {
const flags = Object.assign({}, this.flags, { local: true });
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
this.exceptRooms,
flags
);
}
/**
* Adds a timeout in milliseconds for the next operation
*
* @example
* io.timeout(1000).emit("some-event", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* @param timeout
*/
public timeout(timeout: number) {
const flags = Object.assign({}, this.flags, { timeout });
return new BroadcastOperator<
DecorateAcknowledgementsWithTimeoutAndMultipleResponses<EmitEvents>,
SocketData
>(this.adapter, this.rooms, this.exceptRooms, flags);
}
/**
* Emits to all clients.
*
* @example
* // the “foo” event will be broadcast to all connected clients
* io.emit("foo", "bar");
*
* // the “foo” event will be broadcast to all connected clients in the “room-101” room
* io.to("room-101").emit("foo", "bar");
*
* // with an acknowledgement expected from all connected clients
* io.timeout(1000).emit("some-event", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* @return Always true
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${String(ev)}" is a reserved event name`);
}
// set up packet object
const data = [ev, ...args];
const packet = {
type: PacketType.EVENT,
data: data,
};
const withAck = typeof data[data.length - 1] === "function";
if (!withAck) {
this.adapter.broadcast(packet, {
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
});
return true;
}
const ack = data.pop() as (...args: any[]) => void;
let timedOut = false;
let responses: any[] = [];
const timer = setTimeout(() => {
timedOut = true;
ack.apply(this, [
new Error("operation has timed out"),
this.flags.expectSingleResponse ? null : responses,
]);
}, this.flags.timeout);
let expectedServerCount = -1;
let actualServerCount = 0;
let expectedClientCount = 0;
const checkCompleteness = () => {
if (
!timedOut &&
expectedServerCount === actualServerCount &&
responses.length === expectedClientCount
) {
clearTimeout(timer);
ack.apply(this, [
null,
this.flags.expectSingleResponse ? null : responses,
]);
}
};
this.adapter.broadcastWithAck(
packet,
{
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
},
(clientCount) => {
// each Socket.IO server in the cluster sends the number of clients that were notified
expectedClientCount += clientCount;
actualServerCount++;
checkCompleteness();
},
(clientResponse) => {
// each client sends an acknowledgement
responses.push(clientResponse);
checkCompleteness();
}
);
this.adapter.serverCount().then((serverCount) => {
expectedServerCount = serverCount;
checkCompleteness();
});
return true;
}
/**
* Emits an event and waits for an acknowledgement from all clients.
*
* @example
* try {
* const responses = await io.timeout(1000).emitWithAck("some-event");
* console.log(responses); // one response per client
* } catch (e) {
* // some clients did not acknowledge the event in the given delay
* }
*
* @return a Promise that will be fulfilled when all clients have acknowledged the event
*/
public emitWithAck<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: AllButLast<EventParams<EmitEvents, Ev>>
): Promise<SecondArg<Last<EventParams<EmitEvents, Ev>>>> {
return new Promise((resolve, reject) => {
args.push((err, responses) => {
if (err) {
err.responses = responses;
return reject(err);
} else { } else {
rooms.add(room) return resolve(responses);
} }
return new BroadcastOperator( });
this.adapter, this.emit(ev, ...(args as any[] as EventParams<EmitEvents, Ev>));
rooms, });
this.exceptRooms, }
this.flags
) /**
* Gets a list of clients.
*
* @deprecated this method will be removed in the next major release, please use {@link Server#serverSideEmit} or
* {@link fetchSockets} instead.
*/
public allSockets(): Promise<Set<SocketId>> {
if (!this.adapter) {
throw new Error(
"No adapter for this namespace, are you trying to get the list of clients of a dynamic namespace?"
);
} }
return this.adapter.sockets(this.rooms);
}
/** /**
* Targets a room when emitting. * Returns the matching socket instances. This method works across a cluster of several Socket.IO servers.
* *
* @param room * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
* @return a new BroadcastOperator instance *
* @public * @example
*/ * // return all Socket instances
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> { * const sockets = await io.fetchSockets();
return this.to(room) *
} * // return all Socket instances in the "room1" room
* const sockets = await io.in("room1").fetchSockets();
*
* for (const socket of sockets) {
* console.log(socket.id);
* console.log(socket.handshake);
* console.log(socket.rooms);
* console.log(socket.data);
*
* socket.emit("hello");
* socket.join("room1");
* socket.leave("room2");
* socket.disconnect();
* }
*/
public fetchSockets(): Promise<RemoteSocket<EmitEvents, SocketData>[]> {
return this.adapter
.fetchSockets({
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
})
.then((sockets) => {
return sockets.map((socket) => {
if (socket instanceof Socket) {
// FIXME the TypeScript compiler complains about missing private properties
return socket as unknown as RemoteSocket<EmitEvents, SocketData>;
} else {
return new RemoteSocket(
this.adapter,
socket as SocketDetails<SocketData>
);
}
});
});
}
/** /**
* Excludes a room when emitting. * Makes the matching socket instances join the specified rooms.
* *
* @param room * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
* @return a new BroadcastOperator instance *
* @public * @example
*/ *
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> { * // make all socket instances join the "room1" room
const exceptRooms = new Set(this.exceptRooms) * io.socketsJoin("room1");
if (Array.isArray(room)) { *
room.forEach((r) => exceptRooms.add(r)) * // make all socket instances in the "room1" room join the "room2" and "room3" rooms
} else { * io.in("room1").socketsJoin(["room2", "room3"]);
exceptRooms.add(room) *
} * @param room - a room, or an array of rooms
return new BroadcastOperator( */
this.adapter, public socketsJoin(room: Room | Room[]): void {
this.rooms, this.adapter.addSockets(
exceptRooms, {
this.flags rooms: this.rooms,
) except: this.exceptRooms,
} flags: this.flags,
},
Array.isArray(room) ? room : [room]
);
}
/** /**
* Sets the compress flag. * Makes the matching socket instances leave the specified rooms.
* *
* @param compress - if `true`, compresses the sending data * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
* @return a new BroadcastOperator instance *
* @public * @example
*/ * // make all socket instances leave the "room1" room
public compress(compress: boolean): BroadcastOperator<EmitEvents> { * io.socketsLeave("room1");
const flags = Object.assign({}, this.flags, { compress }) *
return new BroadcastOperator( * // make all socket instances in the "room1" room leave the "room2" and "room3" rooms
this.adapter, * io.in("room1").socketsLeave(["room2", "room3"]);
this.rooms, *
this.exceptRooms, * @param room - a room, or an array of rooms
flags */
) public socketsLeave(room: Room | Room[]): void {
} this.adapter.delSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
},
Array.isArray(room) ? room : [room]
);
}
/** /**
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to * Makes the matching socket instances disconnect.
* receive messages (because of network slowness or other issues, or because theyre connected through long polling *
* and is in the middle of a request-response cycle). * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
* *
* @return a new BroadcastOperator instance * @example
* @public * // make all socket instances disconnect (the connections might be kept alive for other namespaces)
*/ * io.disconnectSockets();
public get volatile(): BroadcastOperator<EmitEvents> { *
const flags = Object.assign({}, this.flags, { volatile: true }) * // make all socket instances in the "room1" room disconnect and close the underlying connections
return new BroadcastOperator( * io.in("room1").disconnectSockets(true);
this.adapter, *
this.rooms, * @param close - whether to close the underlying connection
this.exceptRooms, */
flags public disconnectSockets(close: boolean = false): void {
) this.adapter.disconnectSockets(
} {
rooms: this.rooms,
/** except: this.exceptRooms,
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node. flags: this.flags,
* },
* @return a new BroadcastOperator instance close
* @public );
*/ }
public get local(): BroadcastOperator<EmitEvents> {
const flags = Object.assign({}, this.flags, { local: true })
return new BroadcastOperator(
this.adapter,
this.rooms,
this.exceptRooms,
flags
)
}
/**
* Emits to all clients.
*
* @return Always true
* @public
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${ev}" is a reserved event name`)
}
// set up packet object
const data = [ev, ...args]
const packet = {
type: PacketType.EVENT,
data: data,
}
if ("function" == typeof data[data.length - 1]) {
throw new Error("Callbacks are not supported when broadcasting")
}
this.adapter.broadcast(packet, {
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
})
return true
}
/**
* Gets a list of clients.
*
* @public
*/
public allSockets(): Promise<Set<SocketId>> {
if (!this.adapter) {
throw new Error(
"No adapter for this namespace, are you trying to get the list of clients of a dynamic namespace?"
)
}
return this.adapter.sockets(this.rooms)
}
/**
* Returns the matching socket instances
*
* @public
*/
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
return this.adapter
.fetchSockets({
rooms: this.rooms,
except: this.exceptRooms,
})
.then((sockets) => {
return sockets.map((socket) => {
if (socket instanceof Socket) {
// FIXME the TypeScript compiler complains about missing private properties
return socket as unknown as RemoteSocket<EmitEvents>
} else {
return new RemoteSocket(this.adapter, socket as SocketDetails)
}
})
})
}
/**
* Makes the matching socket instances join the specified rooms
*
* @param room
* @public
*/
public socketsJoin(room: Room | Room[]): void {
this.adapter.addSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
},
Array.isArray(room) ? room : [room]
)
}
/**
* Makes the matching socket instances leave the specified rooms
*
* @param room
* @public
*/
public socketsLeave(room: Room | Room[]): void {
this.adapter.delSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
},
Array.isArray(room) ? room : [room]
)
}
/**
* Makes the matching socket instances disconnect
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
this.adapter.disconnectSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
},
close
)
}
} }
/** /**
* Format of the data when the Socket instance exists on another Socket.IO server * Format of the data when the Socket instance exists on another Socket.IO server
*/ */
interface SocketDetails { interface SocketDetails<SocketData> {
id: SocketId id: SocketId;
handshake: Handshake handshake: Handshake;
rooms: Room[] rooms: Room[];
data: any data: SocketData;
} }
/** /**
* Expose of subset of the attributes and methods of the Socket class * Expose of subset of the attributes and methods of the Socket class
*/ */
export class RemoteSocket<EmitEvents extends EventsMap> export class RemoteSocket<EmitEvents extends EventsMap, SocketData>
implements TypedEventBroadcaster<EmitEvents> implements TypedEventBroadcaster<EmitEvents>
{ {
public readonly id: SocketId public readonly id: SocketId;
public readonly handshake: Handshake public readonly handshake: Handshake;
public readonly rooms: Set<Room> public readonly rooms: Set<Room>;
public readonly data: any public readonly data: SocketData;
private readonly operator: BroadcastOperator<EmitEvents> private readonly operator: BroadcastOperator<EmitEvents, SocketData>;
constructor(adapter: Adapter, details: SocketDetails) { constructor(adapter: Adapter, details: SocketDetails<SocketData>) {
this.id = details.id this.id = details.id;
this.handshake = details.handshake this.handshake = details.handshake;
this.rooms = new Set(details.rooms) this.rooms = new Set(details.rooms);
this.data = details.data this.data = details.data;
this.operator = new BroadcastOperator(adapter, new Set([this.id])) this.operator = new BroadcastOperator<EmitEvents, SocketData>(
} adapter,
new Set([this.id]),
new Set(),
{
expectSingleResponse: true, // so that remoteSocket.emit() with acknowledgement behaves like socket.emit()
}
);
}
public emit<Ev extends EventNames<EmitEvents>>( /**
ev: Ev, * Adds a timeout in milliseconds for the next operation.
...args: EventParams<EmitEvents, Ev> *
): boolean { * @example
return this.operator.emit(ev, ...args) * const sockets = await io.fetchSockets();
} *
* for (const socket of sockets) {
* if (someCondition) {
* socket.timeout(1000).emit("some-event", (err) => {
* if (err) {
* // the client did not acknowledge the event in the given delay
* }
* });
* }
* }
*
* // note: if possible, using a room instead of looping over all sockets is preferable
* io.timeout(1000).to(someConditionRoom).emit("some-event", (err, responses) => {
* // ...
* });
*
* @param timeout
*/
public timeout(timeout: number) {
return this.operator.timeout(timeout) as BroadcastOperator<
DecorateAcknowledgements<EmitEvents>,
SocketData
>;
}
/** public emit<Ev extends EventNames<EmitEvents>>(
* Joins a room. ev: Ev,
* ...args: EventParams<EmitEvents, Ev>
* @param {String|Array} room - room or array of rooms ): boolean {
* @public return this.operator.emit(ev, ...args);
*/ }
public join(room: Room | Room[]): void {
return this.operator.socketsJoin(room)
}
/** /**
* Leaves a room. * Joins a room.
* *
* @param {String} room * @param {String|Array} room - room or array of rooms
* @public */
*/ public join(room: Room | Room[]): void {
public leave(room: Room): void { return this.operator.socketsJoin(room);
return this.operator.socketsLeave(room) }
}
/** /**
* Disconnects this client. * Leaves a room.
* *
* @param {Boolean} close - if `true`, closes the underlying connection * @param {String} room
* @return {Socket} self */
* public leave(room: Room): void {
* @public return this.operator.socketsLeave(room);
*/ }
public disconnect(close = false): this {
this.operator.disconnectSockets(close) /**
return this * Disconnects this client.
} *
* @param {Boolean} close - if `true`, closes the underlying connection
* @return {Socket} self
*/
public disconnect(close = false): this {
this.operator.disconnectSockets(close);
return this;
}
} }

View File

@@ -1,331 +1,353 @@
// import { Decoder, Encoder, Packet, PacketType } from "socket.io-parser" import { Decoder, Encoder, Packet, PacketType } from "../socket.io-parser";
import { Decoder, Encoder, Packet, PacketType } from "../socket.io-parser"
// import debugModule = require("debug") // import debugModule = require("debug")
import url = require("url") import url = require("url");
// import type { IncomingMessage } from "http" // import type { IncomingMessage } from "http"
import type { Server } from "./index" import type { Server } from "./index";
import type { Namespace } from "./namespace" import type { Namespace } from "./namespace";
import type { EventsMap } from "./typed-events" import type { EventsMap } from "./typed-events";
import type { Socket } from "./socket" import type { Socket } from "./socket";
// import type { SocketId } from "socket.io-adapter" import type { SocketId } from "../socket.io-adapter";
import type { SocketId } from "../socket.io-adapter" import type { Socket as RawSocket } from "../engine.io/socket";
import type { Socket as EngineIOSocket } from '../engine.io/socket'
// const debug = debugModule("socket.io:client"); const debug = require("../debug")("socket.io:client");
interface WriteOptions { interface WriteOptions {
compress?: boolean compress?: boolean;
volatile?: boolean volatile?: boolean;
preEncoded?: boolean preEncoded?: boolean;
wsPreEncoded?: string wsPreEncoded?: string;
} }
type CloseReason =
| "transport error"
| "transport close"
| "forced close"
| "ping timeout"
| "parse error";
export class Client< export class Client<
ListenEvents extends EventsMap, ListenEvents extends EventsMap,
EmitEvents extends EventsMap, EmitEvents extends EventsMap,
ServerSideEvents extends EventsMap ServerSideEvents extends EventsMap,
> { SocketData = any
public readonly conn: EngineIOSocket > {
/** public readonly conn: RawSocket;
* @private
*/
readonly id: string
private readonly server: Server<ListenEvents, EmitEvents, ServerSideEvents>
private readonly encoder: Encoder
private readonly decoder: any
private sockets: Map<
SocketId,
Socket<ListenEvents, EmitEvents, ServerSideEvents>
> = new Map()
private nsps: Map<
string,
Socket<ListenEvents, EmitEvents, ServerSideEvents>
> = new Map()
private connectTimeout: NodeJS.Timeout
/** private readonly id: string;
* Client constructor. private readonly server: Server<
* ListenEvents,
* @param server instance EmitEvents,
* @param conn ServerSideEvents,
* @package SocketData
*/ >;
constructor( private readonly encoder: Encoder;
server: Server<ListenEvents, EmitEvents, ServerSideEvents>, private readonly decoder: Decoder;
conn: EngineIOSocket private sockets: Map<
) { SocketId,
this.server = server Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
this.conn = conn > = new Map();
this.encoder = server.encoder private nsps: Map<
this.decoder = new server._parser.Decoder() string,
this.id = conn.id Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
this.setup() > = new Map();
private connectTimeout?: NodeJS.Timeout;
/**
* Client constructor.
*
* @param server instance
* @param conn
* @package
*/
constructor(
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
conn: any
) {
this.server = server;
this.conn = conn;
this.encoder = server.encoder;
this.decoder = new server._parser.Decoder();
this.id = conn.id;
this.setup();
}
/**
* @return the reference to the request that originated the Engine.IO connection
*
* @public
*/
// public get request(): IncomingMessage {
public get request(): any {
return this.conn.request;
}
/**
* Sets up event listeners.
*
* @private
*/
private setup() {
this.onclose = this.onclose.bind(this);
this.ondata = this.ondata.bind(this);
this.onerror = this.onerror.bind(this);
this.ondecoded = this.ondecoded.bind(this);
// @ts-ignore
this.decoder.on("decoded", this.ondecoded);
this.conn.on("data", this.ondata);
this.conn.on("error", this.onerror);
this.conn.on("close", this.onclose);
this.connectTimeout = setTimeout(() => {
if (this.nsps.size === 0) {
debug("no namespace joined yet, close the client");
this.close();
} else {
debug("the client has already joined a namespace, nothing to do");
}
}, this.server._connectTimeout);
}
/**
* Connects a client to a namespace.
*
* @param {String} name - the namespace
* @param {Object} auth - the auth parameters
* @private
*/
private connect(name: string, auth: Record<string, unknown> = {}): void {
if (this.server._nsps.has(name)) {
debug("connecting to namespace %s", name);
return this.doConnect(name, auth);
} }
/** this.server._checkNamespace(
* @return the reference to the request that originated the Engine.IO connection name,
* auth,
* @public (
*/ dynamicNspName:
public get request(): any/** IncomingMessage */ { | Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
return this.conn.request | false
} ) => {
if (dynamicNspName) {
/** this.doConnect(name, auth);
* Sets up event listeners.
*
* @private
*/
private setup() {
console.debug(`socket.io client setup conn ${this.conn.id}`)
this.onclose = this.onclose.bind(this)
this.ondata = this.ondata.bind(this)
this.onerror = this.onerror.bind(this)
this.ondecoded = this.ondecoded.bind(this)
// @ts-ignore
this.decoder.on("decoded", this.ondecoded)
this.conn.on("data", this.ondata)
this.conn.on("error", this.onerror)
this.conn.on("close", this.onclose)
this.connectTimeout = setTimeout(() => {
if (this.nsps.size === 0) {
console.debug(`no namespace joined yet, close the client ${this.id}`)
this.close()
} else {
console.debug(`the client ${this.id} has already joined a namespace, nothing to do`)
}
}, this.server._connectTimeout)
}
/**
* Connects a client to a namespace.
*
* @param {String} name - the namespace
* @param {Object} auth - the auth parameters
* @private
*/
private connect(name: string, auth: object = {}): void {
if (this.server._nsps.has(name)) {
console.debug(`socket.io client ${this.id} connecting to namespace ${name}`)
return this.doConnect(name, auth)
}
this.server._checkNamespace(
name,
auth,
(
dynamicNspName:
| Namespace<ListenEvents, EmitEvents, ServerSideEvents>
| false
) => {
if (dynamicNspName) {
console.debug(`dynamic namespace ${dynamicNspName} was created`)
this.doConnect(name, auth)
} else {
console.debug(`creation of namespace ${name} was denied`)
this._packet({
type: PacketType.CONNECT_ERROR,
nsp: name,
data: {
message: "Invalid namespace",
},
})
}
}
)
}
/**
* Connects a client to a namespace.
*
* @param name - the namespace
* @param {Object} auth - the auth parameters
*
* @private
*/
private doConnect(name: string, auth: object): void {
const nsp = this.server.of(name)
// @java-patch multi thread need direct callback socket
const socket = nsp._add(this, auth, (socket) => {
this.sockets.set(socket.id, socket)
this.nsps.set(nsp.name, socket)
if (this.connectTimeout) {
clearTimeout(this.connectTimeout)
this.connectTimeout = undefined
}
})
}
/**
* Disconnects from all namespaces and closes transport.
*
* @private
*/
_disconnect(): void {
for (const socket of this.sockets.values()) {
socket.disconnect()
}
this.sockets.clear()
this.close()
}
/**
* Removes a socket. Called by each `Socket`.
*
* @private
*/
_remove(socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>): void {
if (this.sockets.has(socket.id)) {
const nsp = this.sockets.get(socket.id)!.nsp.name
this.sockets.delete(socket.id)
this.nsps.delete(nsp)
} else { } else {
console.debug("ignoring remove for", socket.id) debug("creation of namespace %s was denied", name);
this._packet({
type: PacketType.CONNECT_ERROR,
nsp: name,
data: {
message: "Invalid namespace",
},
});
} }
// @java-patch disconnect client when no live socket }
process.nextTick(() => { );
if (this.sockets.size == 0) { }
this.onclose('no live socket')
}
})
}
/** /**
* Closes the underlying connection. * Connects a client to a namespace.
* *
* @private * @param name - the namespace
*/ * @param {Object} auth - the auth parameters
private close(): void { *
console.debug(`client ${this.id} close - reason: forcing transport close`) * @private
if ("open" === this.conn.readyState) { */
console.debug("forcing transport close") private doConnect(name: string, auth: Record<string, unknown>): void {
this.conn.close() const nsp = this.server.of(name);
this.onclose("forced server close")
}
}
/** // @java-patch multi thread need direct callback socket
const socket = nsp._add(this, auth, (socket) => {
this.sockets.set(socket.id, socket);
this.nsps.set(nsp.name, socket);
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = undefined;
}
});
}
/**
* Disconnects from all namespaces and closes transport.
*
* @private
*/
_disconnect(): void {
for (const socket of this.sockets.values()) {
socket.disconnect();
}
this.sockets.clear();
this.close();
}
/**
* Removes a socket. Called by each `Socket`.
*
* @private
*/
_remove(
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
): void {
if (this.sockets.has(socket.id)) {
const nsp = this.sockets.get(socket.id)!.nsp.name;
this.sockets.delete(socket.id);
this.nsps.delete(nsp);
} else {
debug("ignoring remove for %s", socket.id);
}
}
/**
* Closes the underlying connection.
*
* @private
*/
private close(): void {
if ("open" === this.conn.readyState) {
debug("forcing transport close");
this.conn.close();
this.onclose("forced server close");
}
}
/**
* Writes a packet to the transport. * Writes a packet to the transport.
* *
* @param {Object} packet object * @param {Object} packet object
* @param {Object} opts * @param {Object} opts
* @private * @private
*/ */
_packet(packet: Packet | any[], opts: WriteOptions = {}): void { _packet(packet: Packet | any[], opts: WriteOptions = {}): void {
if (this.conn.readyState !== "open") { if (this.conn.readyState !== "open") {
console.debug(`client ${this.id} ignoring packet write ${JSON.stringify(packet)}`) debug("ignoring packet write %j", packet);
return return;
}
const encodedPackets = opts.preEncoded
? (packet as any[]) // previous versions of the adapter incorrectly used socket.packet() instead of writeToEngine()
: this.encoder.encode(packet as Packet)
for (const encodedPacket of encodedPackets) {
this.writeToEngine(encodedPacket, opts)
}
} }
const encodedPackets = opts.preEncoded
? (packet as any[]) // previous versions of the adapter incorrectly used socket.packet() instead of writeToEngine()
: this.encoder.encode(packet as Packet);
this.writeToEngine(encodedPackets, opts);
}
private writeToEngine( private writeToEngine(
encodedPacket: String | Buffer, encodedPackets: Array<string | Buffer>,
opts: WriteOptions opts: WriteOptions
): void { ): void {
if (opts.volatile && !this.conn.transport.writable) { if (opts.volatile && !this.conn.transport.writable) {
console.debug(`client ${this.id} volatile packet is discarded since the transport is not currently writable`) debug(
return "volatile packet is discarded since the transport is not currently writable"
} );
this.conn.write(encodedPacket, opts) return;
} }
const packets = Array.isArray(encodedPackets)
/** ? encodedPackets
* Called with incoming transport data. : [encodedPackets];
* for (const encodedPacket of packets) {
* @private this.conn.write(encodedPacket, opts);
*/
private ondata(data): void {
// try/catch is needed for protocol violations (GH-1880)
try {
this.decoder.add(data)
} catch (error: any) {
this.onerror(error)
}
} }
}
/** /**
* Called when parser fully decodes a packet. * Called with incoming transport data.
* *
* @private * @private
*/ */
private ondecoded(packet: Packet): void { private ondata(data): void {
if (PacketType.CONNECT === packet.type) { // try/catch is needed for protocol violations (GH-1880)
if (this.conn.protocol === 3) { try {
const parsed = url.parse(packet.nsp, true) this.decoder.add(data);
this.connect(parsed.pathname!, parsed.query) } catch (e) {
} else { debug("invalid packet format");
this.connect(packet.nsp, packet.data) this.onerror(e);
}
} else {
const socket = this.nsps.get(packet.nsp)
if (socket) {
process.nextTick(function () {
socket._onpacket(packet)
})
} else {
console.debug(`client ${this.id} no socket for namespace ${packet.nsp}.`)
}
}
} }
}
/** /**
* Handles an error. * Called when parser fully decodes a packet.
* *
* @param {Object} err object * @private
* @private */
*/ private ondecoded(packet: Packet): void {
private onerror(err): void { let namespace: string;
for (const socket of this.sockets.values()) { let authPayload: Record<string, unknown>;
socket._onerror(err) if (this.conn.protocol === 3) {
} const parsed = url.parse(packet.nsp, true);
this.conn.close() namespace = parsed.pathname!;
authPayload = parsed.query;
} else {
namespace = packet.nsp;
authPayload = packet.data;
} }
const socket = this.nsps.get(namespace);
/** if (!socket && packet.type === PacketType.CONNECT) {
* Called upon transport close. this.connect(namespace, authPayload);
* } else if (
* @param reason socket &&
* @private packet.type !== PacketType.CONNECT &&
*/ packet.type !== PacketType.CONNECT_ERROR
private onclose(reason: string): void { ) {
console.debug(`client ${this.id} close with reason ${reason}`) process.nextTick(function () {
socket._onpacket(packet);
// ignore a potential subsequent `close` event });
this.destroy() } else {
debug("invalid state (packet type: %s)", packet.type);
// `nsps` and `sockets` are cleaned up seamlessly this.close();
for (const socket of this.sockets.values()) {
socket._onclose(reason)
}
this.sockets.clear()
this.decoder.destroy() // clean up decoder
} }
}
/** /**
* Cleans up event listeners. * Handles an error.
* @private *
*/ * @param {Object} err object
private destroy(): void { * @private
this.conn.removeListener("data", this.ondata) */
this.conn.removeListener("error", this.onerror) private onerror(err): void {
this.conn.removeListener("close", this.onclose) for (const socket of this.sockets.values()) {
// @ts-ignore socket._onerror(err);
this.decoder.removeListener("decoded", this.ondecoded)
if (this.connectTimeout) {
clearTimeout(this.connectTimeout)
this.connectTimeout = undefined
}
} }
this.conn.close();
}
/**
* Called upon transport close.
*
* @param reason
* @param description
* @private
*/
private onclose(
reason: CloseReason | "forced server close",
description?: any
): void {
debug("client close with reason %s", reason);
// ignore a potential subsequent `close` event
this.destroy();
// `nsps` and `sockets` are cleaned up seamlessly
for (const socket of this.sockets.values()) {
socket._onclose(reason, description);
}
this.sockets.clear();
this.decoder.destroy(); // clean up decoder
}
/**
* Cleans up event listeners.
* @private
*/
private destroy(): void {
this.conn.removeListener("data", this.ondata);
this.conn.removeListener("error", this.onerror);
this.conn.removeListener("close", this.onclose);
// @ts-ignore
this.decoder.removeListener("decoded", this.ondecoded);
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = undefined;
}
}
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More