본문으로 이동
주 메뉴
주 메뉴
사이드바로 이동
숨기기
둘러보기
대문
도움말
최근 바뀜
게임 목록
임의의 게임으로
Top 20
청사진
커뮤니티
오락실
토론란
발전소
추천 게임
게임제작도움방
자매 프로젝트
리버티책
오사인덱스
진실위키
큰숲백과
위키연합회의장
연합회의장
사이트 개발 서버
개발 서버
리버티게임
검색
검색
보이기
계정 만들기
로그인
개인 도구
계정 만들기
로그인
로그아웃한 편집자를 위한 문서
더 알아보기
기여
토론
사용자:Hsl0/연구소/숫자야구 live/실시간.js 문서 원본 보기
사용자 문서
토론
English
읽기
원본 보기
역사 보기
도구
도구
사이드바로 이동
숨기기
동작
읽기
원본 보기
역사 보기
새로 고침
일반
여기를 가리키는 문서
가리키는 글의 최근 바뀜
사용자 기여
기록 목록
사용자 그룹을 보기
특수 문서 목록
문서 정보
축약된 URL 얻기
보이기
사이드바로 이동
숨기기
←
사용자:Hsl0/연구소/숫자야구 live/실시간.js
문서 편집 권한이 없습니다. 다음 이유를 확인해주세요:
여기에는 다른 사용자의 개인 설정이 포함되어 있기 때문에 이 자바스크립트 문서를 편집할 수 없습니다.
문서의 원본을 보거나 복사할 수 있습니다.
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)}`); } }
사용자:Hsl0/연구소/숫자야구 live/실시간.js
문서로 돌아갑니다.