본문으로 이동

사용자:Hsl0/연구소/숫자야구 live/실시간.js: 두 판 사이의 차이

리버티게임, 모두가 만들어가는 자유로운 게임
imported>Hsl0
잔글편집 요약 없음
imported>Hsl0
중간 저장
 
(같은 사용자의 중간 판 9개는 보이지 않습니다)
1번째 줄: 1번째 줄:
const api = new mw.Api();
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 START = Symbol('process_start_time');
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 {
constructor(title, options = {}) {
    /**
if(!title) throw new TypeError('문서를 지정해 주세요');
    * LiveClient 객체 생성자
    * @param title 편집 대상 문서
super();
    * @param options 추가 설정 객체
this.src = title;
    */
this.interval = options.interval || {
    constructor(title, options = {}) {
throttle: null,
        var _g;
margin: null
        if (!title)
};
            throw new TypeError('문서를 지정해 주세요');
this.loadParams = options.loadParams || {
        super();
action: "query",
        /** 시작 리비전 */
curtimestamp: true,
        this.now = null;
prop: "revisions",
        /** 자동 요청 실행 여부 */
titles: this.src,
        this[_a] = false;
formatversion: 2,
        /** 정지 예약 여부, 다음 이벤트 무시 */
rvprop: ['ids', 'timestamp', 'user', 'content'],
        this[_b] = false;
rvlimit: "max",
        /** 쓰로틀 해제 코드 */
rvdir: "newer",
        this[_c] = null;
};
        /** 필수 대기 해제 코드 */
this.postParams = options.postParams || {
        this[_d] = null;
action: "edit",
        this.target = title;
curtimestamp: true,
        this.api = options.api || this.constructor.api;
title: this.src,
        this.interval = {
minor: true
            ...this.constructor.interval,
};
            ...options.interval,
this.now = options.from;
        };
this.stopError = 'stopError' in options? options.stopError : true;  
        this.loadParams = {
this[RUNNING] = false;
            ...this.constructor.loadParams,
}
            titles: this.target,
// 1회 불러오기
            ...options.loadParams,
load(from, options = {}) {
        };
const params = {};
        this.postParams = {
            ...this.constructor.postParams,
// 시작 리비전
            title: this.target,
switch(typeof from) {
            ...options.postParams,
case 'string':
        };
params.rvcontinue = from;
        this.loadAjax = {
break;
            ...this.constructor.loadAjax,
case 'object':
            ...options.loadAjax,
if(from.timestamp) {
        };
if(from.id) params.rvcontinue = from.timestamp + '|' + from.id;
        this.postAjax = {
else params.rvfrom = from.timestamp;
            ...this.constructor.postAjax,
} else if(from.id) params.rvfromid = from.id;
            ...options.postAjax,
break;
        };
}
        this.now = options.from || null;
        this.stopOnError = (_g = options.stopOnError) !== null && _g !== void 0 ? _g : true;
return api.get(Object.assign(params, this.loadParams, options.params), options.ajax).then(response => {
    }
if(response.error) {
    /**
// API 에러
    * 1회 불러오기
throw {
    * @param from 시작 리비전
type: 'api',
    * @param options 추가 파라미터 및 AJAX 설정
error: response.error,
    * @returns 원본 응답, 리비전, 시작 리비전, 추가 요청 가능 여부를 묶은 객체
target: from,
    */
response
    loadOnce(from, { params, ajax } = {}) {
};
        from !== null && from !== void 0 ? from : (from = this.now);
} else {
        // 시작 리비전
const revisions = response.query && response.query.pages[0].revisions;
        return this.api
const result = {
            .get({ ...from, ...this.loadParams, ...params }, { ...this.loadAjax, ...ajax })
response,
            .then((response, xhr) => {
revisions,
            var _g;
target: from
            if (response.error) {
};
                throw {
                    type: response.error.code,
// 갱신됨 (후순위 동작)
                    info: response.error.info,
if(!options.silent && revisions) setTimeout(() => super.dispatchEvent(new CustomEvent('update', {
                    xhr: xhr,
detail: result
                    target: from || null,
})), 0);
                    response: response,
                    exception: null,
return result;
                };
}
            }
}, error => {
            else if (response.query) {
// HTTP 에러
                const revisions = (_g = response.query.pages[0]) === null || _g === void 0 ? void 0 : _g.revisions;
throw {
                const result = {
type: 'fetch',
                    response,
error,
                    revisions,
target: from,
                    target: from,
response: null
                    more: Boolean(response.continue),
};
                };
});
                return result;
}
            }
// 데이터 전송
            else
post(content) {
                throw new TypeError('API 결과를 받지 못했습니다');
return api.postWithEditToken(Object.assign({
        }, (type, info, response, xhr) => {
text: content
            throw {
}, this.postParams));
                type: type === 'http' &&
}
                    info.textStatus !== 'error'
// 계속 불러오기 (간격 재설정)
                    ? info.textStatus
start(interval) {
                    : type,
Object.assign(this.interval, interval); // 시간 간격 갱신
                info: response === null || response === void 0 ? void 0 : response.error.info,
                xhr: xhr || info,
if(typeof this.interval !== 'object' && (typeof this.interval.throttle !== 'number' || typeof this.interval.margin !== 'number')) {
                target: from,
throw new TypeError('정상적인 간격이 지정되지 않았습니다');
                response: response || null,
}
                exception: xhr ? null : info.exception,
            };
const load = options => {
        });
// 다음 폴링
    }
const next = () => {
    /**
if(throttle && request) this[TIMEOUT] = setTimeout(load, this.interval.margin, {
    * 끝까지 불러오기, 마지막까지 불러오면 종료
params: {
    * @param from 시작 리비전
maxage: 0
    * @param options 추가 파라미터 및 AJAX 설정
}
    */
});
    async *loadAll(from = this.now, { params, ajax } = {}) {
};
        let result;
        do {
let throttle = false; // 최소 시간 경과 여부
            result = await this.loadOnce(from, { params, ajax });
let request = false; // 요청 완료 여부
            yield result;
        } while (result.more);
this[TIMEOUT] = null;
    }
this[RUNNING] = true;
    /**
    * 무한 반복 불러오기 코루틴
// 일정 시간이 지나기 전에는 다음 요청을 하지 않음
    * @param from 시작 리비전
this[THROTTLE] = setTimeout(() => {
    * @param globalOptions 추가 파라미터 및 AJAX 설정
if(this[STOPPED]) this[STOPPED] = false;
    */
else {
    async *load(from = this.now, globalOptions = {}) {
throttle = true;
        let options = globalOptions;
this[THROTTLE] = null;
        while (true) {
next();
            const result = await this.loadOnce(from, options);
}
            if (result === null || result === void 0 ? void 0 : result.revisions) {
}, this.interval.throttle);
                // 이번 요청에서 데이터가 갱신되었을 때
                if (result.more) {
// AJAX 요청
                    // 더 불러올 데이터가 있으면
this.load(this.now, options).then(result => {
                    from = result.response.continue;
if(this[STOPPED]) this[STOPPED] = false; // 정지가 예약되면 무시하고 다음 예약 해제
                }
else {
                else {
const response = result.response;
                    // 더 불러올 데이터가 없으면
const revisions = result.revisions;
                    const last = result.revisions[result.revisions.length - 1];
                    from = {
result.error = false;
                        rvstartid: last.revid + 1,
result.updated = Boolean(revisions);
                        rvstart: last.timestamp,
delete result.revisions;
                    };
// 로드 완료
                }
super.dispatchEvent(new CustomEvent('fetch', {
            }
detail: result
            options = { ...globalOptions, ...(yield result) };
}));
        }
    }
if(revisions) {
    /**
// 이번 요청에서 데이터가 갱신되었을 때
    * 데이터 전송
if(response.continue) {
    * @param content 내용
// 더 불러올 데이터가 있으면 바로 요청
    * @param options 추가 파라미터 및 AJAX 설정
this.now = response.continue.rvcontinue;
    * @returns 업로드 결과
    */
return load();
    post(content, { params, ajax } = {}) {
} else {
        return this.api.postWithEditToken({
// 더 불러올 데이터가 없으면
            ...this.postParams,
const last = revisions[revisions.length - 1];
            ...params,
this.now = {
            text: content,
id: last.revid,
        }, { ...this.postAjax, ...ajax });
timestamp: last.timestamp
    }
};
    /**
this.now.id++;
    * 계속 불러오기(간격 재설정)
}
    * @param options 간격 및 시작 리비전 설정
}
    * @returns 이 객체 (메소드 체이닝)
    */
request = true; // 데이터 불러옴
    start({ interval = {}, from = this.now, } = {}) {
next(); // 다음 요청 시도
        Object.assign(this.interval, interval); // 시간 간격 갱신
}
        if (typeof this.interval !== 'object' ||
}, error => {
            (this.interval.throttle &&
if(this[STOPPED]) this[STOPPED] = false; // 정지가 예약되면 무시하고 다음 정지 예약 해제
                typeof this.interval.throttle !== 'number') ||
else {
            (this.interval.margin && typeof this.interval.margin !== 'number')) {
// 에러 이벤트
            throw new TypeError('정상적인 간격이 지정되지 않았습니다');
super.dispatchEvent(new CustomEvent('error', {
        }
detail: error
        const generator = this.load(from);
}));
        const load = (options) => {
            // 다음 폴링
// 서버에 요청이 정상적으로 보내졌으나 API 에러가 발생한 경우 (넷코드용)
            const next = () => {
if(error.type === 'api') super.dispatchEvent(new CustomEvent('fetch', {
                if (throttle && request)
detail: {
                    this[TIMEOUT] = setTimeout(load, this.interval.margin || 0, {
response: error.response,
                        params: {
error: true,
                            maxage: 0,
updated: false
                        },
}
                    });
}));
            };
            let throttle = false; // 최소 시간 경과 여부
if(this.stopError) this[RUNNING] = false; // 에러가 발생하면 정지
            let request = false; // 요청 완료 여부
else {
            this[TIMEOUT] = null;
request = true; // 데이터 불러옴
            this[RUNNING] = true;
next(); // 다음 요청 시도
            // 일정 시간이 지나기 전에는 다음 요청을 하지 않음
}
            this[THROTTLE] = setTimeout(() => {
}
                if (this[STOPPED])
});
                    this[STOPPED] = false;
                else {
// 요청 시도 이벤트
                    throttle = true;
super.dispatchEvent(new CustomEvent('request', {
                    this[THROTTLE] = null;
detail: {
                    next();
target: typeof this.now === 'object'? Object.assign({}, this.now) : this.now,
                }
params: Object.assign({}, this.loadParams, options && options.params),
            }, this.interval.throttle || 0);
ajax: options && options.ajax
            // AJAX 요청
}
            generator.next(options).then(({ value: result }) => {
}));
                if (this[STOPPED])
};
                    this[STOPPED] = false;
if(!this[RUNNING]) load(); // 자동 요청이 실행중이 아니라면 시작
                // 정지가 예약되면 이번 이벤트를 무시하고 정지 예약 해제
                else {
return this; // 메소드 체이닝
                    const { response, revisions } = result;
}
                    result.error = false;
stop(noStrict) {
                    result.updated = Boolean(revisions);
if(this[RUNNING]) { // 자동 요청이 실행중이면
                    // 로드 완료
// 필수 대기시간
                    super.dispatchEvent(new CustomEvent('fetch', {
if(this[TIMEOUT]) {
                        detail: result,
clearTimeout(this[TIMEOUT]);
                    }));
this[TIMEOUT] = null;
                    if (revisions) {
} else this[STOPPED] = true; // 요청중이면 결과 무시 예약
                        // 이번 요청에서 데이터가 갱신되었을 때
                        super.dispatchEvent(new CustomEvent('update', {
// 쓰로틀 대기중이면
                            detail: result,
if(this[THROTTLE]) {
                        }));
clearTimeout(this[THROTTLE]);
                        if (response.continue) {
this[THROTTLE] = null;
                            // 더 불러올 데이터가 있으면 바로 요청
}
                            return load();
                        }
this[RUNNING] = false; // 정지로 표시
                    }
} else if(!noStrict) throw new Error('자동 로드가 실행중이 아닙니다');
                    request = true; // 데이터 불러옴
                    next(); // 다음 요청 시도
return this; // 메소드 체이닝
                }
}
            }, (error) => {
// 현재(다음) 리비전 (지정된 리비전으로) 초기화
                if (this[STOPPED])
reset(from) {
                    this[STOPPED] = false;
if(this[RUNNING]) stop(); // 일단 진행중인 요청 정지
                // 정지가 예약되면 이번 이벤트를 무시하고 정지 예약 해제
                else {
this.now = from; // 리비전 위치 변경/초기화
                    // 에러 이벤트
start(); // 다시 시작
                    super.dispatchEvent(new CustomEvent('error', {
                        detail: error,
return this; // 메소드 체이닝
                    }));
}
                    // 서버에 요청이 정상적으로 보내졌으나 API 에러가 발생한 경우 (넷코드용)
dispatchEvent() {
                    if (error.type === 'api')
throw new TypeError('이벤트를 임의로 발생시킬 수 없습니다');
                        super.dispatchEvent(new CustomEvent('fetch', {
}
                            detail: {
// 자동 요청 실행 여부
                                response: error.response,
get running() {
                                error: true,
return this[RUNNING];
                                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(config = {}) {
    constructor() {
this.history = [];
        /** 최근 레이턴시 기록 */
this.peers = [];
        this.history = [];
this[START] = null;
        /** 다른 사용자의 레이턴시 */
}
        this.peers = [];
    }
get last() {
    /** 마지막 접속 레이턴시 */
return this.history[0];
    get last() {
}
        var _g;
set last(time) {
        return (_g = this.history[0]) !== null && _g !== void 0 ? _g : null;
this.history.unshift(time);
    }
if(this.history.length > 5) this.history.pop();
    // 레이턴시 기록 추가시 오래된 기록 제거 (5개로 유지)
}
    set last(time) {
        if (typeof time !== 'number')
get average() {
            throw new TypeError('last 속성에는 숫자만 할당할 수 있습니다');
if(!this.history.length) return null;
        this.history.unshift(time);
return this.history.reduce((sum, cur) => sum + cur) / this.history.length;
        while (this.history.length > 5)
}
            this.history.pop();
    }
get delay() {
    /** 최근 평균 레이턴시 */
if(!this.history.length) return null;
    get average() {
return Math.max(
        if (!this.history.length)
(// 평균값, 최댓값의 중간값
            return null;
this.peers.reduce((sum, cur) => sum + cur, this.average) / (this.peers.length + 1) // 평균
        return (this.history.reduce((sum, cur) => sum + cur) / this.history.length);
+ Math.max(...this.peers)
    }
) / 2 - this.average, // 자신의 평균 핑과의 차
    /** 자동 산출된 보정 대기 시간 */
0 // 자연수가 아니면 0
    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 {
constructor(client, config = {}) {
    /**
if(!client) throw new TypeError('연결할 클라이언트나 문서를 지정하지 않았습니다');
    * Netcode 객체 생성자
    * @param client 넷코드를 연결할 클라이언트 및 클라이언트를 연결할 문서
this.client = client instanceof LiveClient? client : new LiveClient(client);
    * @param config 넷코드 설정 객체
this.running = false;
    */
this.upload = new Latency(config.upload);
    constructor(client, config = {}) {
this.download = new Latency(config.download);
        /** 다운로드 시작 시간 */
this.noReport = 'noReport' in config? config.noReport : typeof config.report !== 'function';
        this[_e] = [];
this[TIMEOUT] = null;
        if (!client)
this[REQUEST] = this[REQUEST].bind(this);
            throw new TypeError('연결할 클라이언트나 문서를 지정하지 않았습니다');
this[FETCH] = this[FETCH].bind(this);
        this.client =
            client instanceof LiveClient ? client : new LiveClient(client);
if(typeof config.report === 'function') this.report = config.report;
        this.running = false;
}
        this.upload = new Latency();
        this.download = new Latency();
start(delay) {
        this.gateway = config.gateway || null;
this.client.start(delay);
        this.event =
            config.event || this.constructor.event;
this[TIMEOUT] = setTimeout(report, 30000);
        this[REQUEST] = this[REQUEST].bind(this);
        this[FETCH] = this[FETCH].bind(this);
this.client.addEventListener('request', this[REQUEST]);
        if (typeof config.report === 'function')
this.client.addEventListener('fetch', this[FETCH]);
            this.report = config.report.bind(this);
    }
this.running = true;
    /**
}
    * 자동 요청 시작
stop(stopClient = true) {
    * @param delay 최소 지연 시간
if(stopClient) this.client.stop();
    * @param from 시작 리비전
    */
clearTimeout(this[TIMEOUT]);
    start(delay, from) {
        this.client.start({ interval: delay, from });
this.client.removeEventListener('request', this[REQUEST]);
        if (!this.gateway)
this.client.removeEventListener('fetch', this[FETCH]);
            this[TIMEOUT] = setTimeout(this.report, 30000);
        this.client.addEventListener('request', this[REQUEST]);
this.running = false;
        this.client.addEventListener('fetch', this[FETCH]);
}
        this.running = true;
async post(content) {
    }
let before;
    /**
    * 자동 레이턴시 보정 적용 해제
this.upload[START] = Date.now();
    * @param stopClient 클라이언트 자동 요청도 중단 (기본적으로 활성화됨)
    */
if(this.upload.length) {
    stop(stopClient = true) {
before = this.upload.average;
        if (stopClient)
await timeout(this.upload.delay);
            this.client.stop();
}
        clearTimeout(this[TIMEOUT]);
await this.client.post(content);
        this.client.removeEventListener('request', this[REQUEST]);
        this.client.removeEventListener('fetch', this[FETCH]);
this.upload.last = Date.now() - this.upload[START];
        this.running = false;
if(!this.noReport && typeof before === 'number' && Math.abs(this.download.average - before) > 5000) this.report();
    }
}
    /**
    * 데이터 전송
get report() {
    * @param content 내용
return () => {
    */
throw new TypeError('추상 메소드 report가 정의되지 않은 채로 실행되었습니다');
    async post(content) {
};
        let before;
}
        const start = Date.now();
set report(func) {
        if (typeof this.upload.last === 'number') {
delete this.report;
            before = this.upload.average;
this.report = (...args) => {
            await timeout(this.upload.delay);
clearTimeout(this[TIMEOUT]);
        }
this[TIMEOUT] = setTimeout(this.report, 30000);
        const result = await this.client.post(content);
        this.upload.last =
return func(...args);
            new Date(result.edit.newtimestamp).getTime() - start;
};
        if (!this.gateway &&
}
            typeof before === 'number' &&
            this.download.average !== null &&
[REQUEST](event) {
            Math.abs(this.download.average - before) > 5000)
this.download[START] = Date.now();
            this.report();
}
    }
[FETCH](event) {
    /** 레이턴시 정보 전송 */
const before = this.download.average;
    report() {
        var _g;
this.download.last = Date.now() - this.download[START];
        return (_g = this.gateway) === null || _g === void 0 ? void 0 : _g.post(this.event, {
if(this.download.delay !== null) this.client.timeout.throttle = this.download.delay;
            up: this.upload.average,
            down: this.download.average,
if(!this.noReport && before !== null && Math.abs(this.download.average - before) > 5000) this.report();
        });
}
    }
    /**
    * 요청 시도시 소요시간 측정
    * @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));
    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)}`);
    }
}