사용자:Hsl0/연구소/숫자야구 live/실시간.js: 두 판 사이의 차이
보이기
imported>Hsl0 잔글편집 요약 없음 |
imported>Hsl0 중간 저장 |
||
(같은 사용자의 중간 판 9개는 보이지 않습니다) | |||
1번째 줄: | 1번째 줄: | ||
var _a, _b, _c, _d, _e, _f; | |||
const TIMEOUT = Symbol('timeout_id'); | const TIMEOUT = Symbol('timeout_id'); | ||
const STOPPED = Symbol('is_stopped'); | const STOPPED = Symbol('is_stopped'); | ||
const RUNNING = Symbol('is_running'); | const RUNNING = Symbol('is_running'); | ||
const THROTTLE = Symbol('throttle_timeout_id'); | const THROTTLE = Symbol('throttle_timeout_id'); | ||
const | const DOWN_START = Symbol('process_start_time'); | ||
const REQUEST = Symbol('request_event_handler'); | const REQUEST = Symbol('request_event_handler'); | ||
const FETCH = Symbol('fetch_event_handler'); | const FETCH = Symbol('fetch_event_handler'); | ||
const CLIENTS = Symbol('clients_list'); | |||
function parseJSON(text, reviver) { | |||
try { | |||
return JSON.parse(text, reviver); | |||
} | |||
catch (error) { | |||
return text; | |||
} | |||
} | |||
/** 실시간 클라이언트 */ | |||
export class LiveClient extends EventTarget { | export class LiveClient extends EventTarget { | ||
/** | |||
* LiveClient 객체 생성자 | |||
* @param title 편집 대상 문서 | |||
* @param options 추가 설정 객체 | |||
*/ | |||
constructor(title, options = {}) { | |||
var _g; | |||
if (!title) | |||
throw new TypeError('문서를 지정해 주세요'); | |||
super(); | |||
/** 시작 리비전 */ | |||
this.now = null; | |||
/** 자동 요청 실행 여부 */ | |||
this[_a] = false; | |||
/** 정지 예약 여부, 다음 이벤트 무시 */ | |||
this[_b] = false; | |||
/** 쓰로틀 해제 코드 */ | |||
this[_c] = null; | |||
/** 필수 대기 해제 코드 */ | |||
this[_d] = null; | |||
this.target = title; | |||
this.api = options.api || this.constructor.api; | |||
this.interval = { | |||
...this.constructor.interval, | |||
...options.interval, | |||
}; | |||
this.loadParams = { | |||
...this.constructor.loadParams, | |||
titles: this.target, | |||
...options.loadParams, | |||
}; | |||
this.postParams = { | |||
...this.constructor.postParams, | |||
title: this.target, | |||
...options.postParams, | |||
}; | |||
this.loadAjax = { | |||
...this.constructor.loadAjax, | |||
...options.loadAjax, | |||
}; | |||
this.postAjax = { | |||
...this.constructor.postAjax, | |||
...options.postAjax, | |||
}; | |||
this.now = options.from || null; | |||
this.stopOnError = (_g = options.stopOnError) !== null && _g !== void 0 ? _g : true; | |||
} | |||
/** | |||
* 1회 불러오기 | |||
* @param from 시작 리비전 | |||
* @param options 추가 파라미터 및 AJAX 설정 | |||
* @returns 원본 응답, 리비전, 시작 리비전, 추가 요청 가능 여부를 묶은 객체 | |||
*/ | |||
loadOnce(from, { params, ajax } = {}) { | |||
from !== null && from !== void 0 ? from : (from = this.now); | |||
// 시작 리비전 | |||
return this.api | |||
.get({ ...from, ...this.loadParams, ...params }, { ...this.loadAjax, ...ajax }) | |||
.then((response, xhr) => { | |||
var _g; | |||
if (response.error) { | |||
throw { | |||
type: response.error.code, | |||
info: response.error.info, | |||
xhr: xhr, | |||
target: from || null, | |||
response: response, | |||
exception: null, | |||
}; | |||
} | |||
else if (response.query) { | |||
const revisions = (_g = response.query.pages[0]) === null || _g === void 0 ? void 0 : _g.revisions; | |||
const result = { | |||
response, | |||
revisions, | |||
target: from, | |||
more: Boolean(response.continue), | |||
}; | |||
return result; | |||
} | |||
else | |||
throw new TypeError('API 결과를 받지 못했습니다'); | |||
}, (type, info, response, xhr) => { | |||
throw { | |||
type: type === 'http' && | |||
info.textStatus !== 'error' | |||
? info.textStatus | |||
: type, | |||
info: response === null || response === void 0 ? void 0 : response.error.info, | |||
xhr: xhr || info, | |||
target: from, | |||
response: response || null, | |||
exception: xhr ? null : info.exception, | |||
}; | |||
}); | |||
} | |||
/** | |||
* 끝까지 불러오기, 마지막까지 불러오면 종료 | |||
* @param from 시작 리비전 | |||
* @param options 추가 파라미터 및 AJAX 설정 | |||
*/ | |||
async *loadAll(from = this.now, { params, ajax } = {}) { | |||
let result; | |||
do { | |||
result = await this.loadOnce(from, { params, ajax }); | |||
yield result; | |||
} while (result.more); | |||
} | |||
/** | |||
* 무한 반복 불러오기 코루틴 | |||
* @param from 시작 리비전 | |||
* @param globalOptions 추가 파라미터 및 AJAX 설정 | |||
*/ | |||
async *load(from = this.now, globalOptions = {}) { | |||
let options = globalOptions; | |||
while (true) { | |||
const result = await this.loadOnce(from, options); | |||
if (result === null || result === void 0 ? void 0 : result.revisions) { | |||
// 이번 요청에서 데이터가 갱신되었을 때 | |||
if (result.more) { | |||
// 더 불러올 데이터가 있으면 | |||
from = result.response.continue; | |||
} | |||
else { | |||
// 더 불러올 데이터가 없으면 | |||
const last = result.revisions[result.revisions.length - 1]; | |||
from = { | |||
rvstartid: last.revid + 1, | |||
rvstart: last.timestamp, | |||
}; | |||
} | |||
} | |||
options = { ...globalOptions, ...(yield result) }; | |||
} | |||
} | |||
/** | |||
* 데이터 전송 | |||
* @param content 내용 | |||
* @param options 추가 파라미터 및 AJAX 설정 | |||
* @returns 업로드 결과 | |||
*/ | |||
post(content, { params, ajax } = {}) { | |||
return this.api.postWithEditToken({ | |||
...this.postParams, | |||
...params, | |||
text: content, | |||
}, { ...this.postAjax, ...ajax }); | |||
} | |||
/** | |||
* 계속 불러오기(간격 재설정) | |||
* @param options 간격 및 시작 리비전 설정 | |||
* @returns 이 객체 (메소드 체이닝) | |||
*/ | |||
start({ interval = {}, from = this.now, } = {}) { | |||
Object.assign(this.interval, interval); // 시간 간격 갱신 | |||
if (typeof this.interval !== 'object' || | |||
(this.interval.throttle && | |||
typeof this.interval.throttle !== 'number') || | |||
(this.interval.margin && typeof this.interval.margin !== 'number')) { | |||
throw new TypeError('정상적인 간격이 지정되지 않았습니다'); | |||
} | |||
const generator = this.load(from); | |||
const load = (options) => { | |||
// 다음 폴링 | |||
const next = () => { | |||
if (throttle && request) | |||
this[TIMEOUT] = setTimeout(load, this.interval.margin || 0, { | |||
params: { | |||
maxage: 0, | |||
}, | |||
}); | |||
}; | |||
let throttle = false; // 최소 시간 경과 여부 | |||
let request = false; // 요청 완료 여부 | |||
this[TIMEOUT] = null; | |||
this[RUNNING] = true; | |||
// 일정 시간이 지나기 전에는 다음 요청을 하지 않음 | |||
this[THROTTLE] = setTimeout(() => { | |||
if (this[STOPPED]) | |||
this[STOPPED] = false; | |||
else { | |||
throttle = true; | |||
this[THROTTLE] = null; | |||
next(); | |||
} | |||
}, this.interval.throttle || 0); | |||
// AJAX 요청 | |||
generator.next(options).then(({ value: result }) => { | |||
if (this[STOPPED]) | |||
this[STOPPED] = false; | |||
// 정지가 예약되면 이번 이벤트를 무시하고 정지 예약 해제 | |||
else { | |||
const { response, revisions } = result; | |||
result.error = false; | |||
result.updated = Boolean(revisions); | |||
// 로드 완료 | |||
super.dispatchEvent(new CustomEvent('fetch', { | |||
detail: result, | |||
})); | |||
if (revisions) { | |||
// 이번 요청에서 데이터가 갱신되었을 때 | |||
super.dispatchEvent(new CustomEvent('update', { | |||
detail: result, | |||
})); | |||
if (response.continue) { | |||
// 더 불러올 데이터가 있으면 바로 요청 | |||
return load(); | |||
} | |||
} | |||
request = true; // 데이터 불러옴 | |||
next(); // 다음 요청 시도 | |||
} | |||
}, (error) => { | |||
if (this[STOPPED]) | |||
this[STOPPED] = false; | |||
// 정지가 예약되면 이번 이벤트를 무시하고 정지 예약 해제 | |||
else { | |||
// 에러 이벤트 | |||
super.dispatchEvent(new CustomEvent('error', { | |||
detail: error, | |||
})); | |||
// 서버에 요청이 정상적으로 보내졌으나 API 에러가 발생한 경우 (넷코드용) | |||
if (error.type === 'api') | |||
super.dispatchEvent(new CustomEvent('fetch', { | |||
detail: { | |||
response: error.response, | |||
error: true, | |||
updated: false, | |||
}, | |||
})); | |||
if (this.stopOnError) | |||
this[RUNNING] = false; // 에러가 발생하면 정지 | |||
else { | |||
request = true; // 데이터 불러옴 | |||
next(); // 다음 요청 시도 | |||
} | |||
} | |||
}); | |||
// 요청 시도 이벤트 | |||
super.dispatchEvent(new CustomEvent('request', { | |||
detail: { | |||
target: { ...this.now }, | |||
params: { | |||
...this.loadParams, | |||
...options === null || options === void 0 ? void 0 : options.params, | |||
}, | |||
ajax: options === null || options === void 0 ? void 0 : options.ajax, | |||
}, | |||
})); | |||
}; | |||
if (!this[RUNNING]) | |||
load(); // 자동 요청이 실행중이 아니라면 시작 | |||
return this; // 메소드 체이닝 | |||
} | |||
/** | |||
* 계속 불러오기 중단 | |||
* @returns 이 객체 (메소드 체이닝) | |||
*/ | |||
stop() { | |||
if (this[RUNNING]) { | |||
// 자동 요청이 실행중이면 | |||
// 필수 대기시간 | |||
if (this[TIMEOUT]) { | |||
clearTimeout(this[TIMEOUT]); | |||
this[TIMEOUT] = null; | |||
} | |||
else | |||
this[STOPPED] = true; // 요청중이면 결과 무시 예약 | |||
// 쓰로틀 대기중이면 | |||
if (this[THROTTLE]) { | |||
clearTimeout(this[THROTTLE]); | |||
this[THROTTLE] = null; | |||
} | |||
this[RUNNING] = false; // 정지로 표시 | |||
} | |||
else | |||
throw new Error('자동 로드가 실행중이 아닙니다'); | |||
return this; // 메소드 체이닝 | |||
} | |||
/** | |||
* 현재(다음) 리비전 (지정된 리비전으로) 초기화 | |||
* @param from 변경할 현재 리비전 위치 | |||
* @returns 이 객체 (메소드 체이닝) | |||
*/ | |||
reset(from) { | |||
if (this[RUNNING]) | |||
this.stop(); // 일단 진행중인 요청 정지 | |||
this.now = from; // 리비전 위치 변경/초기화 | |||
this.start(); // 다시 시작 | |||
return this; // 메소드 체이닝 | |||
} | |||
dispatchEvent() { | |||
throw new TypeError('이벤트를 임의로 발생시킬 수 없습니다'); | |||
} | |||
/** 자동 요청 실행 여부 */ | |||
get running() { | |||
return this[RUNNING]; | |||
} | |||
} | } | ||
_a = RUNNING, _b = STOPPED, _c = THROTTLE, _d = TIMEOUT; | |||
/** 기본 미디어위키 API 인터페이스 */ | |||
LiveClient.api = new mw.Api(); | |||
/** interval 기본값 */ | |||
LiveClient.interval = { | |||
throttle: null, | |||
margin: null, | |||
}; | |||
/** loadParams 기본값 */ | |||
LiveClient.loadParams = { | |||
action: 'query', | |||
curtimestamp: true, | |||
prop: 'revisions', | |||
formatversion: 2, | |||
rvprop: ['ids', 'timestamp', 'user', 'content'], | |||
rvslots: 'main', | |||
rvlimit: 'max', | |||
rvdir: 'newer', | |||
}; | |||
/** postParams 기본값 */ | |||
LiveClient.postParams = { | |||
action: 'edit', | |||
curtimestamp: true, | |||
minor: true, | |||
}; | |||
/** loadAjax 기본값 */ | |||
LiveClient.loadAjax = {}; | |||
/** postAjax 기본값 */ | |||
LiveClient.postAjax = {}; | |||
/** 레이턴시 기록 및 보정 대기 시간 산출 클래스 */ | |||
class Latency { | class Latency { | ||
constructor() { | |||
/** 최근 레이턴시 기록 */ | |||
this.history = []; | |||
/** 다른 사용자의 레이턴시 */ | |||
this.peers = []; | |||
} | |||
/** 마지막 접속 레이턴시 */ | |||
get last() { | |||
var _g; | |||
return (_g = this.history[0]) !== null && _g !== void 0 ? _g : null; | |||
} | |||
// 레이턴시 기록 추가시 오래된 기록 제거 (5개로 유지) | |||
set last(time) { | |||
if (typeof time !== 'number') | |||
throw new TypeError('last 속성에는 숫자만 할당할 수 있습니다'); | |||
this.history.unshift(time); | |||
while (this.history.length > 5) | |||
this.history.pop(); | |||
} | |||
/** 최근 평균 레이턴시 */ | |||
get average() { | |||
if (!this.history.length) | |||
return null; | |||
return (this.history.reduce((sum, cur) => sum + cur) / this.history.length); | |||
} | |||
/** 자동 산출된 보정 대기 시간 */ | |||
get delay() { | |||
if (!this.history.length) | |||
return null; | |||
return Math.max( | |||
// 평균값, 최댓값의 중간값 | |||
(this.peers.reduce((sum, cur) => sum + cur, this.average) / | |||
(this.peers.length + 1) + // 평균 | |||
Math.max(...this.peers)) / | |||
2 - | |||
this.average, // 자신의 평균 핑과의 차 | |||
0 // 자연수가 아니면 0 | |||
); | |||
} | |||
} | } | ||
/** 보정 대기 시간을 자동으로 적용하는 클라이언트 래퍼 */ | |||
export class Netcode { | export class Netcode { | ||
/** | |||
* Netcode 객체 생성자 | |||
* @param client 넷코드를 연결할 클라이언트 및 클라이언트를 연결할 문서 | |||
* @param config 넷코드 설정 객체 | |||
*/ | |||
constructor(client, config = {}) { | |||
/** 다운로드 시작 시간 */ | |||
this[_e] = []; | |||
if (!client) | |||
throw new TypeError('연결할 클라이언트나 문서를 지정하지 않았습니다'); | |||
this.client = | |||
client instanceof LiveClient ? client : new LiveClient(client); | |||
this.running = false; | |||
this.upload = new Latency(); | |||
this.download = new Latency(); | |||
this.gateway = config.gateway || null; | |||
this.event = | |||
config.event || this.constructor.event; | |||
this[REQUEST] = this[REQUEST].bind(this); | |||
this[FETCH] = this[FETCH].bind(this); | |||
if (typeof config.report === 'function') | |||
this.report = config.report.bind(this); | |||
} | |||
/** | |||
* 자동 요청 시작 | |||
* @param delay 최소 지연 시간 | |||
* @param from 시작 리비전 | |||
*/ | |||
start(delay, from) { | |||
this.client.start({ interval: delay, from }); | |||
if (!this.gateway) | |||
this[TIMEOUT] = setTimeout(this.report, 30000); | |||
this.client.addEventListener('request', this[REQUEST]); | |||
this.client.addEventListener('fetch', this[FETCH]); | |||
this.running = true; | |||
} | |||
/** | |||
* 자동 레이턴시 보정 적용 해제 | |||
* @param stopClient 클라이언트 자동 요청도 중단 (기본적으로 활성화됨) | |||
*/ | |||
stop(stopClient = true) { | |||
if (stopClient) | |||
this.client.stop(); | |||
clearTimeout(this[TIMEOUT]); | |||
this.client.removeEventListener('request', this[REQUEST]); | |||
this.client.removeEventListener('fetch', this[FETCH]); | |||
this.running = false; | |||
} | |||
/** | |||
* 데이터 전송 | |||
* @param content 내용 | |||
*/ | |||
async post(content) { | |||
let before; | |||
const start = Date.now(); | |||
if (typeof this.upload.last === 'number') { | |||
before = this.upload.average; | |||
await timeout(this.upload.delay); | |||
} | |||
const result = await this.client.post(content); | |||
this.upload.last = | |||
new Date(result.edit.newtimestamp).getTime() - start; | |||
if (!this.gateway && | |||
typeof before === 'number' && | |||
this.download.average !== null && | |||
Math.abs(this.download.average - before) > 5000) | |||
this.report(); | |||
} | |||
/** 레이턴시 정보 전송 */ | |||
report() { | |||
var _g; | |||
return (_g = this.gateway) === null || _g === void 0 ? void 0 : _g.post(this.event, { | |||
up: this.upload.average, | |||
down: this.download.average, | |||
}); | |||
} | |||
/** | |||
* 요청 시도시 소요시간 측정 | |||
* @param event 이벤트 객체 | |||
*/ | |||
[(_e = DOWN_START, REQUEST)](event) { | |||
this[DOWN_START].push(Date.now()); | |||
} | |||
/** | |||
* 다운로드 완료시 소요시간 측정 | |||
* @param event 이벤트 객체 | |||
*/ | |||
[FETCH](event) { | |||
const before = this.download.average; | |||
if (this[DOWN_START].length) | |||
this.download.last = Date.now() - this[DOWN_START].shift(); | |||
if (this.download.delay !== null) | |||
this.client.interval.throttle = this.download.delay; | |||
if (!this.gateway && | |||
before !== null && | |||
this.download.average !== null && | |||
Math.abs(this.download.average - before) > 5000) | |||
this.report(); | |||
} | |||
} | } | ||
/** 레이턴시 공유 이벤트 종류 이름 기본값 */ | |||
Netcode.event = 'connect'; | |||
/** | |||
* Promise를 사용하는 일시 중단 함수 | |||
* @param sec 일시 중단 시간 (ms) | |||
* @returns Promise | |||
*/ | |||
function timeout(sec) { | function timeout(sec) { | ||
return new Promise((resolve) => setTimeout(resolve, sec)); | |||
} | |||
/** | |||
* 서버 이벤트를 자동으로 실행 | |||
* | |||
* LiveEventDistributor, LiveEventGateway의 기반 클래스. 직접 사용하지 않는 상속용 클래스 | |||
* | |||
* EventTarget과 유사한 구조를 가지고 있지만, EventTarget을 상속받지 않고 Event 객체를 받지 않는 클래스이므로 주의하세요. | |||
*/ | |||
class LiveEventTarget { | |||
/** | |||
* LiveEventTarget 객체 생성자 | |||
* @param events 등록할 액션 | |||
* @param config 추가 설정 | |||
*/ | |||
constructor(events = {}, config = {}) { | |||
/** 등록된 이벤트 */ | |||
this.listeners = {}; | |||
this.onceListeners = {}; | |||
this.handle = (config.handle || this.handle).bind(this); | |||
if (events) | |||
this.addEventListener(events); | |||
} | |||
addEventListener(typeOrListeners, callbackOrOptions, options) { | |||
switch (typeof typeOrListeners) { | |||
case 'object': // 객체를 통해 여러 타입의 리스너를 추가할 경우 | |||
for (const type in typeOrListeners) | |||
this.addEventListener(type, typeOrListeners[type], callbackOrOptions); | |||
break; | |||
case 'string': // 하나의 리스너를 추가할 경우 | |||
switch (typeof callbackOrOptions) { | |||
case 'object': // 두번째 인자가 객체일 때 handleEvent가 있는지 확인하여 리스너 객체인지 확인 | |||
if (!('handleEvent' in callbackOrOptions)) | |||
// 리스너 객체가 아니면 타입 오류 | |||
throw new TypeError(`${typeOrListeners} 타입에 대한 이벤트 리스너가 올바르지 않습니다`); | |||
// fallthrough // 리스너 객체면 속행 | |||
case 'function': | |||
if (options === null || options === void 0 ? void 0 : options.once) { | |||
if (!this.listeners[typeOrListeners]) | |||
this.listeners[typeOrListeners] = new Set(); | |||
} | |||
else { | |||
if (!this.onceListeners[typeOrListeners]) | |||
this.onceListeners[typeOrListeners] = new Set(); | |||
} | |||
((options === null || options === void 0 ? void 0 : options.once) | |||
? this.onceListeners[typeOrListeners] | |||
: this.listeners[typeOrListeners]).add(callbackOrOptions); | |||
break; | |||
default: | |||
throw new TypeError(`${typeOrListeners} 타입에 대한 이벤트 리스너가 올바르지 않습니다`); | |||
} | |||
break; | |||
default: | |||
throw new TypeError('addEventListener 메소드에 올바른 인자를 넣지 않았습니다'); | |||
} | |||
return this; | |||
} | |||
removeEventListener(type, callback, options) { | |||
var _g; | |||
if (typeof type === 'string') { | |||
// 첫번째 인자에 하나의 type만 넣을 경우 | |||
if (options && 'once' in options) { | |||
// option.once 지정시 onceListeners 또는 listeners 둘 중 하나 제거 | |||
const listeners = (options === null || options === void 0 ? void 0 : options.once) | |||
? this.listeners | |||
: this.onceListeners; | |||
if (callback) | |||
// callback 지정시 지정된 callback만 제거 | |||
(_g = listeners[type]) === null || _g === void 0 ? void 0 : _g.delete(callback); | |||
else | |||
delete listeners[type]; // 미지정시 해당 type의 모든 callback 제거 | |||
} | |||
else { | |||
// option.once 미지정시 onceListeners 또는 listeners 둘 다 제거 | |||
this.removeEventListener(type, callback, { once: false }); | |||
this.removeEventListener(type, callback, { once: true }); | |||
} | |||
} | |||
else if (type.forEach) | |||
// 첫번째 인자에 배열이나 Set을 통해 여러 type을 보낼 경우 모든 type 제거 | |||
type.forEach((type) => this.removeEventListener(type, null, callback)); | |||
else | |||
throw new TypeError('removeEventListener 메소드에 올바른 인자를 넣지 않았습니다'); | |||
return this; | |||
} | |||
dispatchEvent(type, arg, rev) { | |||
var _g, _h; | |||
const run = (listener) => { | |||
try { | |||
switch (typeof listener) { | |||
case 'function': | |||
listener(arg, rev); | |||
break; | |||
case 'object': | |||
listener.handleEvent(arg, rev); | |||
break; | |||
} | |||
} | |||
catch (error) { | |||
console.error('Uncaught', error); | |||
window.dispatchEvent(new ErrorEvent('error', error)); | |||
} | |||
}; | |||
(_g = this.listeners[type]) === null || _g === void 0 ? void 0 : _g.forEach(run); | |||
(_h = this.onceListeners[type]) === null || _h === void 0 ? void 0 : _h.forEach(run); | |||
delete this.onceListeners[type]; | |||
} | |||
/** | |||
* 폴링 데이터를 받을 때 실행되어 이벤트를 실행하는 클라이언트 이벤트 핸들러 | |||
* @param event 이벤트 객체 | |||
*/ | |||
handle({ detail }) { | |||
console.log(...arguments); | |||
for (const rev of detail.revisions) { | |||
const content = rev.slots.main.content; | |||
const sep = content.search(' '); | |||
const type = content.slice(0, sep); | |||
const arg = parseJSON(content.slice(sep + 1)); | |||
this.dispatchEvent(type, arg, rev); | |||
} | |||
} | |||
} | |||
/** | |||
* 서버 이벤트를 자동으로 실행 | |||
* | |||
* 여러 클라이언트를 연결해 서버 이벤트를 분배할 수 있지만, 이벤트를 서버에 전송할 수 없습니다. LiveEventGateway를 이용해 보세요. | |||
* | |||
* EventTarget과 유사한 구조를 가지고 있지만, EventTarget을 상속받지 않고 Event 객체를 받지 않는 클래스이므로 주의하세요. | |||
*/ | |||
export class LiveEventDistributor extends LiveEventTarget { | |||
/** | |||
* LiveEventDistributor 객체 생성자 | |||
* @param clients 연결할 클라이언트 | |||
* @param events 등록할 액션 | |||
* @param config 추가 설정 | |||
*/ | |||
constructor(clients, events = {}, config = {}) { | |||
super(events, config); | |||
/** 연결된 클라이언트 */ | |||
this[_f] = new Set(); | |||
if (clients) | |||
this.connect(clients); | |||
} | |||
/** 클라이언트 목록 불러오기 */ | |||
get clients() { | |||
return [...this[CLIENTS]]; | |||
} | |||
/** | |||
* 클라이언트 연결 | |||
* @param clients 연결할 클라이언트 | |||
* @returns 이 객체 (메소드 체이닝) | |||
*/ | |||
connect(...clients) { | |||
if (Array.isArray(clients[0]) || clients[0] instanceof Set) | |||
clients = clients[0]; | |||
for (const client of clients) { | |||
if (!(client instanceof LiveClient)) | |||
throw new TypeError('EventDistributor에 LiveClient가 아닌 객체를 연결하려 시도했습니다'); | |||
if (this[CLIENTS].has(client)) | |||
continue; | |||
this[CLIENTS].add(client); | |||
client.addEventListener('update', | |||
this.handle); | |||
} | |||
return this; | |||
} | |||
/** | |||
* 클라이언트 연결 해제 | |||
* @param clients 연결 해제할 클라이언트 | |||
* @returns 이 객체 (메소드 체이닝) | |||
*/ | |||
disconnect(...clients) { | |||
if (Array.isArray(clients[0]) || clients[0] instanceof Set) | |||
clients = clients[0]; | |||
if (clients.length <= 0) | |||
clients = this.clients; | |||
for (const client of clients) { | |||
if (!(client instanceof LiveClient)) | |||
throw new TypeError('disconnect 메소드에 잘못된 인자를 넣었습니다'); | |||
if (!this[CLIENTS].has(client)) | |||
continue; | |||
client.removeEventListener('update', | |||
this.handle); | |||
this[CLIENTS].delete(client); | |||
} | |||
return this; | |||
} | |||
} | |||
_f = CLIENTS; | |||
/** | |||
* 서버 이벤트를 자동으로 실행 | |||
* | |||
* 하나의 클라이언트를 연결해 이벤트를 분배받고, 서버에 이벤트를 전송할 수 있습니다. 여러 클라이언트를 연결하려면 LiveEventDistributor를 이용해 보세요. | |||
* | |||
* EventTarget과 유사한 구조를 가지고 있지만, EventTarget을 상속받지 않고 Event 객체를 받지 않는 클래스이므로 주의하세요. | |||
*/ | |||
export class LiveEventGateway extends LiveEventTarget { | |||
/** | |||
* LiveEventGateway 객체 생성자 | |||
* @param client 연결할 클라이언트 | |||
* @param events 등록할 액션 | |||
* @param config 추가 설정 | |||
*/ | |||
constructor(client, events = {}, config = {}) { | |||
if (!(client instanceof LiveClient)) | |||
throw new TypeError('EventGateway에 LiveClient가 아닌 객체를 연결하려 시도했습니다'); | |||
super(events, config); | |||
this.client = client; | |||
client.addEventListener('update', | |||
this.handle); | |||
} | |||
/** | |||
* 이벤트 전송 | |||
* @param type 이벤트 타입 이름 | |||
* @param content 이벤트 인자값 | |||
* @returns 업로드 결과 | |||
*/ | |||
post(type, content) { | |||
return this.client.post(`${type} ${JSON.stringify(content)}`); | |||
} | |||
} | } |
2023년 2월 24일 (금) 02:55 기준 최신판
var _a, _b, _c, _d, _e, _f;
const TIMEOUT = Symbol('timeout_id');
const STOPPED = Symbol('is_stopped');
const RUNNING = Symbol('is_running');
const THROTTLE = Symbol('throttle_timeout_id');
const DOWN_START = Symbol('process_start_time');
const REQUEST = Symbol('request_event_handler');
const FETCH = Symbol('fetch_event_handler');
const CLIENTS = Symbol('clients_list');
function parseJSON(text, reviver) {
try {
return JSON.parse(text, reviver);
}
catch (error) {
return text;
}
}
/** 실시간 클라이언트 */
export class LiveClient extends EventTarget {
/**
* LiveClient 객체 생성자
* @param title 편집 대상 문서
* @param options 추가 설정 객체
*/
constructor(title, options = {}) {
var _g;
if (!title)
throw new TypeError('문서를 지정해 주세요');
super();
/** 시작 리비전 */
this.now = null;
/** 자동 요청 실행 여부 */
this[_a] = false;
/** 정지 예약 여부, 다음 이벤트 무시 */
this[_b] = false;
/** 쓰로틀 해제 코드 */
this[_c] = null;
/** 필수 대기 해제 코드 */
this[_d] = null;
this.target = title;
this.api = options.api || this.constructor.api;
this.interval = {
...this.constructor.interval,
...options.interval,
};
this.loadParams = {
...this.constructor.loadParams,
titles: this.target,
...options.loadParams,
};
this.postParams = {
...this.constructor.postParams,
title: this.target,
...options.postParams,
};
this.loadAjax = {
...this.constructor.loadAjax,
...options.loadAjax,
};
this.postAjax = {
...this.constructor.postAjax,
...options.postAjax,
};
this.now = options.from || null;
this.stopOnError = (_g = options.stopOnError) !== null && _g !== void 0 ? _g : true;
}
/**
* 1회 불러오기
* @param from 시작 리비전
* @param options 추가 파라미터 및 AJAX 설정
* @returns 원본 응답, 리비전, 시작 리비전, 추가 요청 가능 여부를 묶은 객체
*/
loadOnce(from, { params, ajax } = {}) {
from !== null && from !== void 0 ? from : (from = this.now);
// 시작 리비전
return this.api
.get({ ...from, ...this.loadParams, ...params }, { ...this.loadAjax, ...ajax })
.then((response, xhr) => {
var _g;
if (response.error) {
throw {
type: response.error.code,
info: response.error.info,
xhr: xhr,
target: from || null,
response: response,
exception: null,
};
}
else if (response.query) {
const revisions = (_g = response.query.pages[0]) === null || _g === void 0 ? void 0 : _g.revisions;
const result = {
response,
revisions,
target: from,
more: Boolean(response.continue),
};
return result;
}
else
throw new TypeError('API 결과를 받지 못했습니다');
}, (type, info, response, xhr) => {
throw {
type: type === 'http' &&
info.textStatus !== 'error'
? info.textStatus
: type,
info: response === null || response === void 0 ? void 0 : response.error.info,
xhr: xhr || info,
target: from,
response: response || null,
exception: xhr ? null : info.exception,
};
});
}
/**
* 끝까지 불러오기, 마지막까지 불러오면 종료
* @param from 시작 리비전
* @param options 추가 파라미터 및 AJAX 설정
*/
async *loadAll(from = this.now, { params, ajax } = {}) {
let result;
do {
result = await this.loadOnce(from, { params, ajax });
yield result;
} while (result.more);
}
/**
* 무한 반복 불러오기 코루틴
* @param from 시작 리비전
* @param globalOptions 추가 파라미터 및 AJAX 설정
*/
async *load(from = this.now, globalOptions = {}) {
let options = globalOptions;
while (true) {
const result = await this.loadOnce(from, options);
if (result === null || result === void 0 ? void 0 : result.revisions) {
// 이번 요청에서 데이터가 갱신되었을 때
if (result.more) {
// 더 불러올 데이터가 있으면
from = result.response.continue;
}
else {
// 더 불러올 데이터가 없으면
const last = result.revisions[result.revisions.length - 1];
from = {
rvstartid: last.revid + 1,
rvstart: last.timestamp,
};
}
}
options = { ...globalOptions, ...(yield result) };
}
}
/**
* 데이터 전송
* @param content 내용
* @param options 추가 파라미터 및 AJAX 설정
* @returns 업로드 결과
*/
post(content, { params, ajax } = {}) {
return this.api.postWithEditToken({
...this.postParams,
...params,
text: content,
}, { ...this.postAjax, ...ajax });
}
/**
* 계속 불러오기(간격 재설정)
* @param options 간격 및 시작 리비전 설정
* @returns 이 객체 (메소드 체이닝)
*/
start({ interval = {}, from = this.now, } = {}) {
Object.assign(this.interval, interval); // 시간 간격 갱신
if (typeof this.interval !== 'object' ||
(this.interval.throttle &&
typeof this.interval.throttle !== 'number') ||
(this.interval.margin && typeof this.interval.margin !== 'number')) {
throw new TypeError('정상적인 간격이 지정되지 않았습니다');
}
const generator = this.load(from);
const load = (options) => {
// 다음 폴링
const next = () => {
if (throttle && request)
this[TIMEOUT] = setTimeout(load, this.interval.margin || 0, {
params: {
maxage: 0,
},
});
};
let throttle = false; // 최소 시간 경과 여부
let request = false; // 요청 완료 여부
this[TIMEOUT] = null;
this[RUNNING] = true;
// 일정 시간이 지나기 전에는 다음 요청을 하지 않음
this[THROTTLE] = setTimeout(() => {
if (this[STOPPED])
this[STOPPED] = false;
else {
throttle = true;
this[THROTTLE] = null;
next();
}
}, this.interval.throttle || 0);
// AJAX 요청
generator.next(options).then(({ value: result }) => {
if (this[STOPPED])
this[STOPPED] = false;
// 정지가 예약되면 이번 이벤트를 무시하고 정지 예약 해제
else {
const { response, revisions } = result;
result.error = false;
result.updated = Boolean(revisions);
// 로드 완료
super.dispatchEvent(new CustomEvent('fetch', {
detail: result,
}));
if (revisions) {
// 이번 요청에서 데이터가 갱신되었을 때
super.dispatchEvent(new CustomEvent('update', {
detail: result,
}));
if (response.continue) {
// 더 불러올 데이터가 있으면 바로 요청
return load();
}
}
request = true; // 데이터 불러옴
next(); // 다음 요청 시도
}
}, (error) => {
if (this[STOPPED])
this[STOPPED] = false;
// 정지가 예약되면 이번 이벤트를 무시하고 정지 예약 해제
else {
// 에러 이벤트
super.dispatchEvent(new CustomEvent('error', {
detail: error,
}));
// 서버에 요청이 정상적으로 보내졌으나 API 에러가 발생한 경우 (넷코드용)
if (error.type === 'api')
super.dispatchEvent(new CustomEvent('fetch', {
detail: {
response: error.response,
error: true,
updated: false,
},
}));
if (this.stopOnError)
this[RUNNING] = false; // 에러가 발생하면 정지
else {
request = true; // 데이터 불러옴
next(); // 다음 요청 시도
}
}
});
// 요청 시도 이벤트
super.dispatchEvent(new CustomEvent('request', {
detail: {
target: { ...this.now },
params: {
...this.loadParams,
...options === null || options === void 0 ? void 0 : options.params,
},
ajax: options === null || options === void 0 ? void 0 : options.ajax,
},
}));
};
if (!this[RUNNING])
load(); // 자동 요청이 실행중이 아니라면 시작
return this; // 메소드 체이닝
}
/**
* 계속 불러오기 중단
* @returns 이 객체 (메소드 체이닝)
*/
stop() {
if (this[RUNNING]) {
// 자동 요청이 실행중이면
// 필수 대기시간
if (this[TIMEOUT]) {
clearTimeout(this[TIMEOUT]);
this[TIMEOUT] = null;
}
else
this[STOPPED] = true; // 요청중이면 결과 무시 예약
// 쓰로틀 대기중이면
if (this[THROTTLE]) {
clearTimeout(this[THROTTLE]);
this[THROTTLE] = null;
}
this[RUNNING] = false; // 정지로 표시
}
else
throw new Error('자동 로드가 실행중이 아닙니다');
return this; // 메소드 체이닝
}
/**
* 현재(다음) 리비전 (지정된 리비전으로) 초기화
* @param from 변경할 현재 리비전 위치
* @returns 이 객체 (메소드 체이닝)
*/
reset(from) {
if (this[RUNNING])
this.stop(); // 일단 진행중인 요청 정지
this.now = from; // 리비전 위치 변경/초기화
this.start(); // 다시 시작
return this; // 메소드 체이닝
}
dispatchEvent() {
throw new TypeError('이벤트를 임의로 발생시킬 수 없습니다');
}
/** 자동 요청 실행 여부 */
get running() {
return this[RUNNING];
}
}
_a = RUNNING, _b = STOPPED, _c = THROTTLE, _d = TIMEOUT;
/** 기본 미디어위키 API 인터페이스 */
LiveClient.api = new mw.Api();
/** interval 기본값 */
LiveClient.interval = {
throttle: null,
margin: null,
};
/** loadParams 기본값 */
LiveClient.loadParams = {
action: 'query',
curtimestamp: true,
prop: 'revisions',
formatversion: 2,
rvprop: ['ids', 'timestamp', 'user', 'content'],
rvslots: 'main',
rvlimit: 'max',
rvdir: 'newer',
};
/** postParams 기본값 */
LiveClient.postParams = {
action: 'edit',
curtimestamp: true,
minor: true,
};
/** loadAjax 기본값 */
LiveClient.loadAjax = {};
/** postAjax 기본값 */
LiveClient.postAjax = {};
/** 레이턴시 기록 및 보정 대기 시간 산출 클래스 */
class Latency {
constructor() {
/** 최근 레이턴시 기록 */
this.history = [];
/** 다른 사용자의 레이턴시 */
this.peers = [];
}
/** 마지막 접속 레이턴시 */
get last() {
var _g;
return (_g = this.history[0]) !== null && _g !== void 0 ? _g : null;
}
// 레이턴시 기록 추가시 오래된 기록 제거 (5개로 유지)
set last(time) {
if (typeof time !== 'number')
throw new TypeError('last 속성에는 숫자만 할당할 수 있습니다');
this.history.unshift(time);
while (this.history.length > 5)
this.history.pop();
}
/** 최근 평균 레이턴시 */
get average() {
if (!this.history.length)
return null;
return (this.history.reduce((sum, cur) => sum + cur) / this.history.length);
}
/** 자동 산출된 보정 대기 시간 */
get delay() {
if (!this.history.length)
return null;
return Math.max(
// 평균값, 최댓값의 중간값
(this.peers.reduce((sum, cur) => sum + cur, this.average) /
(this.peers.length + 1) + // 평균
Math.max(...this.peers)) /
2 -
this.average, // 자신의 평균 핑과의 차
0 // 자연수가 아니면 0
);
}
}
/** 보정 대기 시간을 자동으로 적용하는 클라이언트 래퍼 */
export class Netcode {
/**
* Netcode 객체 생성자
* @param client 넷코드를 연결할 클라이언트 및 클라이언트를 연결할 문서
* @param config 넷코드 설정 객체
*/
constructor(client, config = {}) {
/** 다운로드 시작 시간 */
this[_e] = [];
if (!client)
throw new TypeError('연결할 클라이언트나 문서를 지정하지 않았습니다');
this.client =
client instanceof LiveClient ? client : new LiveClient(client);
this.running = false;
this.upload = new Latency();
this.download = new Latency();
this.gateway = config.gateway || null;
this.event =
config.event || this.constructor.event;
this[REQUEST] = this[REQUEST].bind(this);
this[FETCH] = this[FETCH].bind(this);
if (typeof config.report === 'function')
this.report = config.report.bind(this);
}
/**
* 자동 요청 시작
* @param delay 최소 지연 시간
* @param from 시작 리비전
*/
start(delay, from) {
this.client.start({ interval: delay, from });
if (!this.gateway)
this[TIMEOUT] = setTimeout(this.report, 30000);
this.client.addEventListener('request', this[REQUEST]);
this.client.addEventListener('fetch', this[FETCH]);
this.running = true;
}
/**
* 자동 레이턴시 보정 적용 해제
* @param stopClient 클라이언트 자동 요청도 중단 (기본적으로 활성화됨)
*/
stop(stopClient = true) {
if (stopClient)
this.client.stop();
clearTimeout(this[TIMEOUT]);
this.client.removeEventListener('request', this[REQUEST]);
this.client.removeEventListener('fetch', this[FETCH]);
this.running = false;
}
/**
* 데이터 전송
* @param content 내용
*/
async post(content) {
let before;
const start = Date.now();
if (typeof this.upload.last === 'number') {
before = this.upload.average;
await timeout(this.upload.delay);
}
const result = await this.client.post(content);
this.upload.last =
new Date(result.edit.newtimestamp).getTime() - start;
if (!this.gateway &&
typeof before === 'number' &&
this.download.average !== null &&
Math.abs(this.download.average - before) > 5000)
this.report();
}
/** 레이턴시 정보 전송 */
report() {
var _g;
return (_g = this.gateway) === null || _g === void 0 ? void 0 : _g.post(this.event, {
up: this.upload.average,
down: this.download.average,
});
}
/**
* 요청 시도시 소요시간 측정
* @param event 이벤트 객체
*/
[(_e = DOWN_START, REQUEST)](event) {
this[DOWN_START].push(Date.now());
}
/**
* 다운로드 완료시 소요시간 측정
* @param event 이벤트 객체
*/
[FETCH](event) {
const before = this.download.average;
if (this[DOWN_START].length)
this.download.last = Date.now() - this[DOWN_START].shift();
if (this.download.delay !== null)
this.client.interval.throttle = this.download.delay;
if (!this.gateway &&
before !== null &&
this.download.average !== null &&
Math.abs(this.download.average - before) > 5000)
this.report();
}
}
/** 레이턴시 공유 이벤트 종류 이름 기본값 */
Netcode.event = 'connect';
/**
* Promise를 사용하는 일시 중단 함수
* @param sec 일시 중단 시간 (ms)
* @returns Promise
*/
function timeout(sec) {
return new Promise((resolve) => setTimeout(resolve, sec));
}
/**
* 서버 이벤트를 자동으로 실행
*
* LiveEventDistributor, LiveEventGateway의 기반 클래스. 직접 사용하지 않는 상속용 클래스
*
* EventTarget과 유사한 구조를 가지고 있지만, EventTarget을 상속받지 않고 Event 객체를 받지 않는 클래스이므로 주의하세요.
*/
class LiveEventTarget {
/**
* LiveEventTarget 객체 생성자
* @param events 등록할 액션
* @param config 추가 설정
*/
constructor(events = {}, config = {}) {
/** 등록된 이벤트 */
this.listeners = {};
this.onceListeners = {};
this.handle = (config.handle || this.handle).bind(this);
if (events)
this.addEventListener(events);
}
addEventListener(typeOrListeners, callbackOrOptions, options) {
switch (typeof typeOrListeners) {
case 'object': // 객체를 통해 여러 타입의 리스너를 추가할 경우
for (const type in typeOrListeners)
this.addEventListener(type, typeOrListeners[type], callbackOrOptions);
break;
case 'string': // 하나의 리스너를 추가할 경우
switch (typeof callbackOrOptions) {
case 'object': // 두번째 인자가 객체일 때 handleEvent가 있는지 확인하여 리스너 객체인지 확인
if (!('handleEvent' in callbackOrOptions))
// 리스너 객체가 아니면 타입 오류
throw new TypeError(`${typeOrListeners} 타입에 대한 이벤트 리스너가 올바르지 않습니다`);
// fallthrough // 리스너 객체면 속행
case 'function':
if (options === null || options === void 0 ? void 0 : options.once) {
if (!this.listeners[typeOrListeners])
this.listeners[typeOrListeners] = new Set();
}
else {
if (!this.onceListeners[typeOrListeners])
this.onceListeners[typeOrListeners] = new Set();
}
((options === null || options === void 0 ? void 0 : options.once)
? this.onceListeners[typeOrListeners]
: this.listeners[typeOrListeners]).add(callbackOrOptions);
break;
default:
throw new TypeError(`${typeOrListeners} 타입에 대한 이벤트 리스너가 올바르지 않습니다`);
}
break;
default:
throw new TypeError('addEventListener 메소드에 올바른 인자를 넣지 않았습니다');
}
return this;
}
removeEventListener(type, callback, options) {
var _g;
if (typeof type === 'string') {
// 첫번째 인자에 하나의 type만 넣을 경우
if (options && 'once' in options) {
// option.once 지정시 onceListeners 또는 listeners 둘 중 하나 제거
const listeners = (options === null || options === void 0 ? void 0 : options.once)
? this.listeners
: this.onceListeners;
if (callback)
// callback 지정시 지정된 callback만 제거
(_g = listeners[type]) === null || _g === void 0 ? void 0 : _g.delete(callback);
else
delete listeners[type]; // 미지정시 해당 type의 모든 callback 제거
}
else {
// option.once 미지정시 onceListeners 또는 listeners 둘 다 제거
this.removeEventListener(type, callback, { once: false });
this.removeEventListener(type, callback, { once: true });
}
}
else if (type.forEach)
// 첫번째 인자에 배열이나 Set을 통해 여러 type을 보낼 경우 모든 type 제거
type.forEach((type) => this.removeEventListener(type, null, callback));
else
throw new TypeError('removeEventListener 메소드에 올바른 인자를 넣지 않았습니다');
return this;
}
dispatchEvent(type, arg, rev) {
var _g, _h;
const run = (listener) => {
try {
switch (typeof listener) {
case 'function':
listener(arg, rev);
break;
case 'object':
listener.handleEvent(arg, rev);
break;
}
}
catch (error) {
console.error('Uncaught', error);
window.dispatchEvent(new ErrorEvent('error', error));
}
};
(_g = this.listeners[type]) === null || _g === void 0 ? void 0 : _g.forEach(run);
(_h = this.onceListeners[type]) === null || _h === void 0 ? void 0 : _h.forEach(run);
delete this.onceListeners[type];
}
/**
* 폴링 데이터를 받을 때 실행되어 이벤트를 실행하는 클라이언트 이벤트 핸들러
* @param event 이벤트 객체
*/
handle({ detail }) {
console.log(...arguments);
for (const rev of detail.revisions) {
const content = rev.slots.main.content;
const sep = content.search(' ');
const type = content.slice(0, sep);
const arg = parseJSON(content.slice(sep + 1));
this.dispatchEvent(type, arg, rev);
}
}
}
/**
* 서버 이벤트를 자동으로 실행
*
* 여러 클라이언트를 연결해 서버 이벤트를 분배할 수 있지만, 이벤트를 서버에 전송할 수 없습니다. LiveEventGateway를 이용해 보세요.
*
* EventTarget과 유사한 구조를 가지고 있지만, EventTarget을 상속받지 않고 Event 객체를 받지 않는 클래스이므로 주의하세요.
*/
export class LiveEventDistributor extends LiveEventTarget {
/**
* LiveEventDistributor 객체 생성자
* @param clients 연결할 클라이언트
* @param events 등록할 액션
* @param config 추가 설정
*/
constructor(clients, events = {}, config = {}) {
super(events, config);
/** 연결된 클라이언트 */
this[_f] = new Set();
if (clients)
this.connect(clients);
}
/** 클라이언트 목록 불러오기 */
get clients() {
return [...this[CLIENTS]];
}
/**
* 클라이언트 연결
* @param clients 연결할 클라이언트
* @returns 이 객체 (메소드 체이닝)
*/
connect(...clients) {
if (Array.isArray(clients[0]) || clients[0] instanceof Set)
clients = clients[0];
for (const client of clients) {
if (!(client instanceof LiveClient))
throw new TypeError('EventDistributor에 LiveClient가 아닌 객체를 연결하려 시도했습니다');
if (this[CLIENTS].has(client))
continue;
this[CLIENTS].add(client);
client.addEventListener('update',
this.handle);
}
return this;
}
/**
* 클라이언트 연결 해제
* @param clients 연결 해제할 클라이언트
* @returns 이 객체 (메소드 체이닝)
*/
disconnect(...clients) {
if (Array.isArray(clients[0]) || clients[0] instanceof Set)
clients = clients[0];
if (clients.length <= 0)
clients = this.clients;
for (const client of clients) {
if (!(client instanceof LiveClient))
throw new TypeError('disconnect 메소드에 잘못된 인자를 넣었습니다');
if (!this[CLIENTS].has(client))
continue;
client.removeEventListener('update',
this.handle);
this[CLIENTS].delete(client);
}
return this;
}
}
_f = CLIENTS;
/**
* 서버 이벤트를 자동으로 실행
*
* 하나의 클라이언트를 연결해 이벤트를 분배받고, 서버에 이벤트를 전송할 수 있습니다. 여러 클라이언트를 연결하려면 LiveEventDistributor를 이용해 보세요.
*
* EventTarget과 유사한 구조를 가지고 있지만, EventTarget을 상속받지 않고 Event 객체를 받지 않는 클래스이므로 주의하세요.
*/
export class LiveEventGateway extends LiveEventTarget {
/**
* LiveEventGateway 객체 생성자
* @param client 연결할 클라이언트
* @param events 등록할 액션
* @param config 추가 설정
*/
constructor(client, events = {}, config = {}) {
if (!(client instanceof LiveClient))
throw new TypeError('EventGateway에 LiveClient가 아닌 객체를 연결하려 시도했습니다');
super(events, config);
this.client = client;
client.addEventListener('update',
this.handle);
}
/**
* 이벤트 전송
* @param type 이벤트 타입 이름
* @param content 이벤트 인자값
* @returns 업로드 결과
*/
post(type, content) {
return this.client.post(`${type} ${JSON.stringify(content)}`);
}
}