mirror of
https://github.com/easydiffusion/easydiffusion.git
synced 2024-11-25 01:34:38 +01:00
1314 lines
51 KiB
JavaScript
1314 lines
51 KiB
JavaScript
/** SD-UI Backend control and classes.
|
|
*/
|
|
(function () { "use strict";
|
|
const RETRY_DELAY_IF_BUFFER_IS_EMPTY = 1000 // ms
|
|
const RETRY_DELAY_IF_SERVER_IS_BUSY = 30 * 1000 // ms, status_code 503, already a task running
|
|
const RETRY_DELAY_ON_ERROR = 4000 // ms
|
|
const TASK_STATE_SERVER_UPDATE_DELAY = 1500 // ms
|
|
const SERVER_STATE_VALIDITY_DURATION = 90 * 1000 // ms - 90 seconds to allow ping to timeout more than once before killing tasks.
|
|
const HEALTH_PING_INTERVAL = 5000 // ms
|
|
const IDLE_COOLDOWN = 2500 // ms
|
|
const CONCURRENT_TASK_INTERVAL = 100 // ms
|
|
|
|
/** Connects to an endpoint and resumes connection after reaching end of stream until all data is received.
|
|
* Allows closing the connection while the server buffers more data.
|
|
*/
|
|
class ChunkedStreamReader {
|
|
#bufferedString = '' // Data received waiting to be read.
|
|
#url
|
|
#fetchOptions
|
|
#response
|
|
|
|
constructor(url, initialContent='', options={}) {
|
|
if (typeof url !== 'string' && !(url instanceof String)) {
|
|
throw new Error('url is not a string.')
|
|
}
|
|
if (typeof initialContent !== 'undefined' && typeof initialContent !== 'string') {
|
|
throw new Error('initialContent is not a string.')
|
|
}
|
|
this.#bufferedString = initialContent
|
|
this.#url = url
|
|
this.#fetchOptions = Object.assign({
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
}, options)
|
|
this.onNext = undefined
|
|
}
|
|
|
|
get url() {
|
|
if (this.#response.redirected) {
|
|
return this.#response.url
|
|
}
|
|
return this.#url
|
|
}
|
|
get bufferedString() {
|
|
return this.#bufferedString
|
|
}
|
|
get status() {
|
|
this.#response?.status
|
|
}
|
|
get statusText() {
|
|
this.#response?.statusText
|
|
}
|
|
|
|
parse(value) {
|
|
if (typeof value === 'undefined') {
|
|
return
|
|
}
|
|
if (!isArrayOrTypedArray(value)) {
|
|
return [value]
|
|
}
|
|
if (value.length === 0) {
|
|
return value
|
|
}
|
|
if (typeof this.textDecoder === 'undefined') {
|
|
this.textDecoder = new TextDecoder()
|
|
}
|
|
return [this.textDecoder.decode(value)]
|
|
}
|
|
onComplete(value) {
|
|
return value
|
|
}
|
|
onError(response) {
|
|
throw new Error(response.statusText)
|
|
}
|
|
onNext({value, done}, response) {
|
|
return {value, done}
|
|
}
|
|
|
|
async *[Symbol.asyncIterator]() {
|
|
return this.open()
|
|
}
|
|
async *open() {
|
|
let value = undefined
|
|
let done = undefined
|
|
do {
|
|
if (this.#response) {
|
|
await asyncDelay(RETRY_DELAY_IF_BUFFER_IS_EMPTY)
|
|
}
|
|
this.#response = await fetch(this.#url, this.#fetchOptions)
|
|
if (!this.#response.ok) {
|
|
if (this.#response.status === 425) {
|
|
continue
|
|
}
|
|
// Request status indicate failure
|
|
console.warn('Stream %o stopped unexpectedly.', this.#response)
|
|
value = await Promise.resolve(this.onError(this.#response))
|
|
if (typeof value === 'boolean' && value) {
|
|
continue
|
|
}
|
|
return value
|
|
}
|
|
const reader = this.#response.body.getReader()
|
|
done = false
|
|
do {
|
|
const readState = await reader.read()
|
|
value = this.parse(readState.value)
|
|
if (value) {
|
|
for(let sVal of value) {
|
|
({value: sVal, done} = await Promise.resolve(this.onNext({value:sVal, done:readState.done})))
|
|
yield sVal
|
|
if (done) {
|
|
return this.onComplete(sVal)
|
|
}
|
|
}
|
|
}
|
|
if (done) {
|
|
return
|
|
}
|
|
} while(value && !done)
|
|
} while (!done && (this.#response.ok || this.#response.status === 425))
|
|
}
|
|
*readStreamAsJSON(jsonStr, throwOnError) {
|
|
if (typeof jsonStr !== 'string') {
|
|
throw new Error('jsonStr is not a string.')
|
|
}
|
|
do {
|
|
if (this.#bufferedString.length > 0) {
|
|
// Append new data when required
|
|
if (jsonStr.length > 0) {
|
|
jsonStr = this.#bufferedString + jsonStr
|
|
} else {
|
|
jsonStr = this.#bufferedString
|
|
}
|
|
this.#bufferedString = ''
|
|
}
|
|
if (!jsonStr) {
|
|
return
|
|
}
|
|
// Find next delimiter
|
|
let lastChunkIdx = jsonStr.indexOf('}{')
|
|
if (lastChunkIdx >= 0) {
|
|
this.#bufferedString = jsonStr.substring(0, lastChunkIdx + 1)
|
|
jsonStr = jsonStr.substring(lastChunkIdx + 1)
|
|
} else {
|
|
this.#bufferedString = jsonStr
|
|
jsonStr = ''
|
|
}
|
|
if (this.#bufferedString.length <= 0) {
|
|
return
|
|
}
|
|
// hack for a middleman buffering all the streaming updates, and unleashing them on the poor browser in one shot.
|
|
// this results in having to parse JSON like {"step": 1}{"step": 2}{"step": 3}{"ste...
|
|
// which is obviously invalid and can happen at any point while rendering.
|
|
// So we need to extract only the next {} section
|
|
try { // Try to parse
|
|
const jsonObj = JSON.parse(this.#bufferedString)
|
|
this.#bufferedString = jsonStr
|
|
jsonStr = ''
|
|
yield jsonObj
|
|
} catch (e) {
|
|
if (throwOnError) {
|
|
console.error(`Parsing: "${this.#bufferedString}", Buffer: "${jsonStr}"`)
|
|
}
|
|
this.#bufferedString += jsonStr
|
|
if (e instanceof SyntaxError && !throwOnError) {
|
|
return
|
|
}
|
|
throw e
|
|
}
|
|
} while (this.#bufferedString.length > 0 && this.#bufferedString.indexOf('}') >= 0)
|
|
}
|
|
}
|
|
|
|
const EVENT_IDLE = 'idle'
|
|
const EVENT_STATUS_CHANGED = 'statusChange'
|
|
const EVENT_UNHANDLED_REJECTION = 'unhandledRejection'
|
|
const EVENT_TASK_QUEUED = 'taskQueued'
|
|
const EVENT_TASK_START = 'taskStart'
|
|
const EVENT_TASK_END = 'taskEnd'
|
|
const EVENT_TASK_ERROR = 'task_error'
|
|
const EVENT_UNEXPECTED_RESPONSE = 'unexpectedResponse'
|
|
const EVENTS_TYPES = [
|
|
EVENT_IDLE,
|
|
EVENT_STATUS_CHANGED,
|
|
EVENT_UNHANDLED_REJECTION,
|
|
|
|
EVENT_TASK_QUEUED,
|
|
EVENT_TASK_START,
|
|
EVENT_TASK_END,
|
|
EVENT_TASK_ERROR,
|
|
|
|
EVENT_UNEXPECTED_RESPONSE,
|
|
]
|
|
Object.freeze(EVENTS_TYPES)
|
|
const eventSource = new GenericEventSource(EVENTS_TYPES)
|
|
|
|
function setServerStatus(msgType, msg) {
|
|
return eventSource.fireEvent(EVENT_STATUS_CHANGED, {type: msgType, message: msg})
|
|
}
|
|
|
|
const ServerStates = {
|
|
init: 'Init',
|
|
loadingModel: 'LoadingModel',
|
|
online: 'Online',
|
|
rendering: 'Rendering',
|
|
unavailable: 'Unavailable',
|
|
}
|
|
Object.freeze(ServerStates)
|
|
|
|
let sessionId = Date.now()
|
|
let serverState = {'status': ServerStates.unavailable, 'time': Date.now()}
|
|
|
|
async function healthCheck() {
|
|
if (Date.now() < serverState.time + (HEALTH_PING_INTERVAL / 2) && isServerAvailable()) {
|
|
// Ping confirmed online less than half of HEALTH_PING_INTERVAL ago.
|
|
return true
|
|
}
|
|
if (Date.now() >= serverState.time + SERVER_STATE_VALIDITY_DURATION) {
|
|
console.warn('WARNING! SERVER_STATE_VALIDITY_DURATION has elapsed since the last Ping completed.')
|
|
}
|
|
try {
|
|
let res = undefined
|
|
if (typeof sessionId !== 'undefined') {
|
|
res = await fetch('/ping?session_id=' + sessionId)
|
|
} else {
|
|
res = await fetch('/ping')
|
|
}
|
|
serverState = await res.json()
|
|
if (typeof serverState !== 'object' || typeof serverState.status !== 'string') {
|
|
console.error(`Server reply didn't contain a state value.`)
|
|
serverState = {'status': ServerStates.unavailable, 'time': Date.now()}
|
|
setServerStatus('error', 'offline')
|
|
return false
|
|
}
|
|
// Set status
|
|
switch(serverState.status) {
|
|
case ServerStates.init:
|
|
// Wait for init to complete before updating status.
|
|
break
|
|
case ServerStates.online:
|
|
setServerStatus('online', 'ready')
|
|
break
|
|
case ServerStates.loadingModel:
|
|
setServerStatus('busy', 'loading..')
|
|
break
|
|
case ServerStates.rendering:
|
|
setServerStatus('busy', 'rendering..')
|
|
break
|
|
default: // Unavailable
|
|
console.error('Ping received an unexpected server status. Status: %s', serverState.status)
|
|
setServerStatus('error', serverState.status.toLowerCase())
|
|
break
|
|
}
|
|
serverState.time = Date.now()
|
|
return true
|
|
} catch (e) {
|
|
console.error(e)
|
|
serverState = {'status': ServerStates.unavailable, 'time': Date.now()}
|
|
setServerStatus('error', 'offline')
|
|
}
|
|
return false
|
|
}
|
|
|
|
function isServerAvailable() {
|
|
if (typeof serverState !== 'object') {
|
|
console.error('serverState not set to a value. Connection to server could be lost...')
|
|
return false
|
|
}
|
|
if (Date.now() >= serverState.time + SERVER_STATE_VALIDITY_DURATION) {
|
|
console.warn('SERVER_STATE_VALIDITY_DURATION elapsed. Connection to server could be lost...')
|
|
return false
|
|
}
|
|
switch (serverState.status) {
|
|
case ServerStates.loadingModel:
|
|
case ServerStates.rendering:
|
|
case ServerStates.online:
|
|
return true
|
|
default:
|
|
console.warn('Unexpected server status. Server could be unavailable... Status: %s', serverState.status)
|
|
return false
|
|
}
|
|
}
|
|
|
|
async function waitUntil(isReadyFn, delay, timeout) {
|
|
if (typeof delay === 'number') {
|
|
const msDelay = delay
|
|
delay = () => asyncDelay(msDelay)
|
|
}
|
|
if (typeof delay !== 'function') {
|
|
throw new Error('delay is not a number or a function.')
|
|
}
|
|
if (typeof timeout !== 'undefined' && typeof timeout !== 'number') {
|
|
throw new Error('timeout is not a number.')
|
|
}
|
|
if (typeof timeout === 'undefined' || timeout < 0) {
|
|
timeout = Number.MAX_SAFE_INTEGER
|
|
}
|
|
timeout = Date.now() + timeout
|
|
while (timeout > Date.now()
|
|
&& Date.now() < serverState.time + SERVER_STATE_VALIDITY_DURATION
|
|
&& !Boolean(await Promise.resolve(isReadyFn()))
|
|
) {
|
|
await delay()
|
|
if (!isServerAvailable()) { // Can fail if ping got frozen/suspended...
|
|
if (await healthCheck() && isServerAvailable()) { // Force a recheck of server status before failure...
|
|
continue // Continue waiting if last healthCheck confirmed the server is still alive.
|
|
}
|
|
throw new Error('Connection with server lost.')
|
|
}
|
|
}
|
|
if (Date.now() >= serverState.time + SERVER_STATE_VALIDITY_DURATION) {
|
|
console.warn('SERVER_STATE_VALIDITY_DURATION elapsed. Released waitUntil on stale server state.')
|
|
}
|
|
}
|
|
|
|
const TaskStatus = {
|
|
init: 'init',
|
|
pending: 'pending', // Queued locally, not yet posted to server
|
|
waiting: 'waiting', // Waiting to run on server
|
|
processing: 'processing',
|
|
stopped: 'stopped',
|
|
completed: 'completed',
|
|
failed: 'failed',
|
|
}
|
|
Object.freeze(TaskStatus)
|
|
|
|
const TASK_STATUS_ORDER = [
|
|
TaskStatus.init,
|
|
TaskStatus.pending,
|
|
TaskStatus.waiting,
|
|
TaskStatus.processing,
|
|
//Don't add status that are final.
|
|
]
|
|
|
|
const task_queue = new Map()
|
|
const concurrent_generators = new Map()
|
|
const weak_results = new WeakMap()
|
|
|
|
class Task {
|
|
// Private properties...
|
|
_reqBody = {} // request body of this task.
|
|
#reader = undefined
|
|
#status = TaskStatus.init
|
|
#id = undefined
|
|
#exception = undefined
|
|
|
|
constructor(options={}) {
|
|
this._reqBody = Object.assign({}, options)
|
|
if (typeof this._reqBody.session_id === 'undefined') {
|
|
this._reqBody.session_id = sessionId
|
|
} else if (this._reqBody.session_id !== SD.sessionId && String(this._reqBody.session_id) !== String(SD.sessionId)) {
|
|
throw new Error('Use SD.sessionId to set the request session_id.')
|
|
}
|
|
this._reqBody.session_id = String(this._reqBody.session_id)
|
|
}
|
|
|
|
get id() {
|
|
return this.#id
|
|
}
|
|
_setId(id) {
|
|
if (typeof this.#id !== 'undefined') {
|
|
throw new Error('The task ID can only be set once.')
|
|
}
|
|
this.#id = id
|
|
}
|
|
|
|
get exception() {
|
|
return this.#exception
|
|
}
|
|
async abort(exception) {
|
|
if (this.isCompleted || this.isStopped || this.hasFailed) {
|
|
return
|
|
}
|
|
if (typeof exception !== 'undefined') {
|
|
if (typeof exception === 'string') {
|
|
exception = new Error(exception)
|
|
}
|
|
if (typeof exception !== 'object') {
|
|
throw new Error('exception is not an object.')
|
|
}
|
|
if (!(exception instanceof Error)) {
|
|
throw new Error('exception is not an Error or a string.')
|
|
}
|
|
}
|
|
const res = await fetch('/image/stop?task=' + this.id)
|
|
if (!res.ok) {
|
|
console.log('Stop response:', res)
|
|
throw new Error(res.statusText)
|
|
}
|
|
task_queue.delete(this)
|
|
this.#exception = exception
|
|
this.#status = (exception ? TaskStatus.failed : TaskStatus.stopped)
|
|
}
|
|
|
|
get reqBody() {
|
|
if (this.#status === TaskStatus.init) {
|
|
return this._reqBody
|
|
}
|
|
console.warn('Task reqBody cannot be changed after the init state.')
|
|
return Object.assign({}, this._reqBody)
|
|
}
|
|
|
|
get isPending() {
|
|
return TASK_STATUS_ORDER.indexOf(this.#status) >= 0
|
|
}
|
|
get isCompleted() {
|
|
return this.#status === TaskStatus.completed
|
|
}
|
|
get hasFailed() {
|
|
return this.#status === TaskStatus.failed
|
|
}
|
|
get isStopped() {
|
|
return this.#status === TaskStatus.stopped
|
|
}
|
|
get status() {
|
|
return this.#status
|
|
}
|
|
_setStatus(status) {
|
|
if (status === this.#status) {
|
|
return
|
|
}
|
|
const currentIdx = TASK_STATUS_ORDER.indexOf(this.#status)
|
|
if (currentIdx < 0) {
|
|
throw Error(`The task status ${this.#status} is final and can't be changed.`)
|
|
}
|
|
const newIdx = TASK_STATUS_ORDER.indexOf(status)
|
|
if (newIdx >= 0 && newIdx < currentIdx) {
|
|
throw Error(`The task status ${status} can't replace ${this.#status}.`)
|
|
}
|
|
this.#status = status
|
|
}
|
|
|
|
/** Send current task to server.
|
|
* @param {*} [timeout=-1] Optional timeout value in ms
|
|
* @returns the response from the render request.
|
|
* @memberof Task
|
|
*/
|
|
async post(url, timeout=-1) {
|
|
if(this.status !== TaskStatus.init && this.status !== TaskStatus.pending) {
|
|
throw new Error(`Task status ${this.status} is not valid for post.`)
|
|
}
|
|
this._setStatus(TaskStatus.pending)
|
|
Object.freeze(this._reqBody)
|
|
|
|
const abortSignal = (timeout >= 0 ? AbortSignal.timeout(timeout) : undefined)
|
|
let res = undefined
|
|
try {
|
|
this.checkReqBody()
|
|
do {
|
|
abortSignal?.throwIfAborted()
|
|
res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(this._reqBody),
|
|
signal: abortSignal
|
|
})
|
|
// status_code 503, already a task running.
|
|
} while (res.status === 503 && await asyncDelay(RETRY_DELAY_IF_SERVER_IS_BUSY))
|
|
} catch (err) {
|
|
this.abort(err)
|
|
throw err
|
|
}
|
|
if (!res.ok) {
|
|
const err = new Error(`Unexpected response HTTP${res.status}. Details: ${res.statusText}`)
|
|
this.abort(err)
|
|
throw err
|
|
}
|
|
return await res.json()
|
|
}
|
|
|
|
static getReader(url) {
|
|
const reader = new ChunkedStreamReader(url)
|
|
const parseToString = reader.parse
|
|
reader.parse = function(value) {
|
|
value = parseToString.call(this, value)
|
|
if (!value || value.length <= 0) {
|
|
return
|
|
}
|
|
return reader.readStreamAsJSON(value.join(''))
|
|
}
|
|
reader.onNext = function({done, value}) {
|
|
// By default is completed when the return value has a status defined.
|
|
if (typeof value === 'object' && 'status' in value) {
|
|
done = true
|
|
}
|
|
return {done, value}
|
|
}
|
|
return reader
|
|
}
|
|
_setReader(reader) {
|
|
if (typeof this.#reader !== 'undefined') {
|
|
throw new Error('The task reader can only be set once.')
|
|
}
|
|
this.#reader = reader
|
|
}
|
|
get reader() {
|
|
if (this.#reader) {
|
|
return this.#reader
|
|
}
|
|
if (!this.streamUrl) {
|
|
throw new Error('The task has no stream Url defined.')
|
|
}
|
|
this.#reader = Task.getReader(this.streamUrl)
|
|
const task = this
|
|
const onNext = this.#reader.onNext
|
|
this.#reader.onNext = function({done, value}) {
|
|
if (value && typeof value === 'object') {
|
|
if (task.status === TaskStatus.init
|
|
|| task.status === TaskStatus.pending
|
|
|| task.status === TaskStatus.waiting
|
|
) {
|
|
task._setStatus(TaskStatus.processing)
|
|
}
|
|
if ('step' in value && 'total_steps' in value) {
|
|
task.step = value.step
|
|
task.total_steps = value.total_steps
|
|
}
|
|
}
|
|
return onNext.call(this, {done, value})
|
|
}
|
|
this.#reader.onComplete = function(value) {
|
|
task.result = value
|
|
if (task.isPending) {
|
|
task._setStatus(TaskStatus.completed)
|
|
}
|
|
return value
|
|
}
|
|
this.#reader.onError = function(response) {
|
|
const err = new Error(response.statusText)
|
|
task.abort(err)
|
|
throw err
|
|
}
|
|
return this.#reader
|
|
}
|
|
|
|
async waitUntil({timeout=-1, callback, status, signal}) {
|
|
const currentIdx = TASK_STATUS_ORDER.indexOf(this.#status)
|
|
if (currentIdx <= 0) {
|
|
return false
|
|
}
|
|
const stIdx = (status ? TASK_STATUS_ORDER.indexOf(status) : currentIdx + 1)
|
|
if (stIdx >= 0 && stIdx <= currentIdx) {
|
|
return true
|
|
}
|
|
if (stIdx < 0 && currentIdx < 0) {
|
|
return this.#status === (status || TaskStatus.completed)
|
|
}
|
|
if (signal?.aborted) {
|
|
return false
|
|
}
|
|
const task = this
|
|
switch(this.#status) {
|
|
case TaskStatus.pending:
|
|
case TaskStatus.waiting:
|
|
// Wait for server status to include this task.
|
|
await waitUntil(
|
|
async () => {
|
|
if (task.#id && typeof serverState.tasks === 'object' && Object.keys(serverState.tasks).includes(String(task.#id))) {
|
|
return true
|
|
}
|
|
if (await Promise.resolve(callback?.call(task)) || signal?.aborted) {
|
|
return true
|
|
}
|
|
},
|
|
TASK_STATE_SERVER_UPDATE_DELAY,
|
|
timeout,
|
|
)
|
|
if (this.#id && typeof serverState.tasks === 'object' && Object.keys(serverState.tasks).includes(String(task.#id))) {
|
|
this._setStatus(TaskStatus.waiting)
|
|
}
|
|
if (await Promise.resolve(callback?.call(this)) || signal?.aborted) {
|
|
return false
|
|
}
|
|
if (stIdx >= 0 && stIdx <= TASK_STATUS_ORDER.indexOf(TaskStatus.waiting)) {
|
|
return true
|
|
}
|
|
// Wait for task to start on server.
|
|
await waitUntil(
|
|
async () => {
|
|
if (typeof serverState.tasks !== 'object' || serverState.tasks[String(task.#id)] !== 'pending') {
|
|
return true
|
|
}
|
|
if (await Promise.resolve(callback?.call(task)) || signal?.aborted) {
|
|
return true
|
|
}
|
|
},
|
|
TASK_STATE_SERVER_UPDATE_DELAY,
|
|
timeout,
|
|
)
|
|
const state = (typeof serverState.tasks === 'object' ? serverState.tasks[String(task.#id)] : undefined)
|
|
if (state === 'running' || state === 'buffer' || state === 'completed') {
|
|
this._setStatus(TaskStatus.processing)
|
|
}
|
|
if (await Promise.resolve(callback?.call(task)) || signal?.aborted) {
|
|
return false
|
|
}
|
|
if (stIdx >= 0 && stIdx <= TASK_STATUS_ORDER.indexOf(TaskStatus.processing)) {
|
|
return true
|
|
}
|
|
case TaskStatus.processing:
|
|
await waitUntil(
|
|
async () => {
|
|
if (typeof serverState.tasks !== 'object' || serverState.tasks[String(task.#id)] !== 'running') {
|
|
return true
|
|
}
|
|
if (await Promise.resolve(callback?.call(task)) || signal?.aborted) {
|
|
return true
|
|
}
|
|
},
|
|
TASK_STATE_SERVER_UPDATE_DELAY,
|
|
timeout,
|
|
)
|
|
await Promise.resolve(callback?.call(this))
|
|
default:
|
|
return this.#status === (status || TaskStatus.completed)
|
|
}
|
|
}
|
|
|
|
async enqueue(promiseGenerator, ...args) {
|
|
if (this.status !== TaskStatus.init) {
|
|
throw new Error(`Task is in an invalid status ${this.status} to add to queue.`)
|
|
}
|
|
this._setStatus(TaskStatus.pending)
|
|
task_queue.set(this, promiseGenerator)
|
|
await eventSource.fireEvent(EVENT_TASK_QUEUED, {task:this})
|
|
await Task.enqueue(promiseGenerator, ...args)
|
|
await this.waitUntil({status: TaskStatus.completed})
|
|
if (this.exception) {
|
|
throw this.exception
|
|
}
|
|
return this.result
|
|
}
|
|
static async enqueue(promiseGenerator, ...args) {
|
|
if (typeof promiseGenerator === 'undefined') {
|
|
throw new Error('To enqueue a concurrent task, a *Promise Generator is needed but undefined was found.')
|
|
}
|
|
//if (Symbol.asyncIterator in result || Symbol.iterator in result) {
|
|
//concurrent_generators.set(result, Promise.resolve(args))
|
|
if (typeof promiseGenerator === 'function') {
|
|
concurrent_generators.set(asGenerator({callback: promiseGenerator}), Promise.resolve(args))
|
|
} else {
|
|
concurrent_generators.set(promiseGenerator, Promise.resolve(args))
|
|
}
|
|
await waitUntil(() => !concurrent_generators.has(promiseGenerator), CONCURRENT_TASK_INTERVAL)
|
|
return weak_results.get(promiseGenerator)
|
|
}
|
|
static enqueueNew(task, classCtor, progressCallback) {
|
|
if (task.status !== TaskStatus.init) {
|
|
throw new Error('Task has an invalid status to add to queue.')
|
|
}
|
|
if (!(task instanceof classCtor)) {
|
|
throw new Error('Task is not a instance of classCtor.')
|
|
}
|
|
let promiseGenerator = undefined
|
|
if (typeof progressCallback === 'undefined') {
|
|
promiseGenerator = classCtor.start(task)
|
|
} else if (typeof progressCallback === 'function') {
|
|
promiseGenerator = classCtor.start(task, progressCallback)
|
|
} else {
|
|
throw new Error('progressCallback is not a function.')
|
|
}
|
|
return Task.prototype.enqueue.call(task, promiseGenerator)
|
|
}
|
|
|
|
static async run(promiseGenerator, {callback, signal, timeout=-1}={}) {
|
|
let value = undefined
|
|
let done = undefined
|
|
if (timeout < 0) {
|
|
timeout = Number.MAX_SAFE_INTEGER
|
|
}
|
|
timeout = Date.now() + timeout
|
|
do {
|
|
({value, done} = await Promise.resolve(promiseGenerator.next(value)))
|
|
if (value instanceof Promise) {
|
|
value = await value
|
|
}
|
|
if (callback) {
|
|
({value, done} = await Promise.resolve(callback.call(promiseGenerator, {value, done})))
|
|
}
|
|
if (value instanceof Promise) {
|
|
value = await value
|
|
}
|
|
} while(!done && !signal?.aborted && timeout > Date.now())
|
|
return value
|
|
}
|
|
static async *asGenerator({callback, generator, signal, timeout=-1}={}) {
|
|
let value = undefined
|
|
let done = undefined
|
|
if (timeout < 0) {
|
|
timeout = Number.MAX_SAFE_INTEGER
|
|
}
|
|
timeout = Date.now() + timeout
|
|
do {
|
|
({value, done} = await Promise.resolve(generator.next(value)))
|
|
if (value instanceof Promise) {
|
|
value = await value
|
|
}
|
|
if (callback) {
|
|
({value, done} = await Promise.resolve(callback.call(generator, {value, done})))
|
|
if (value instanceof Promise) {
|
|
value = await value
|
|
}
|
|
}
|
|
value = yield value
|
|
} while(!done && !signal?.aborted && timeout > Date.now())
|
|
return value
|
|
}
|
|
}
|
|
|
|
const TASK_REQUIRED = {
|
|
"session_id": 'string',
|
|
"prompt": 'string',
|
|
"negative_prompt": 'string',
|
|
"width": 'number',
|
|
"height": 'number',
|
|
"seed": 'number',
|
|
|
|
"sampler_name": 'string',
|
|
"use_stable_diffusion_model": 'string',
|
|
"num_inference_steps": 'number',
|
|
"guidance_scale": 'number',
|
|
|
|
"num_outputs": 'number',
|
|
"stream_progress_updates": 'boolean',
|
|
"stream_image_progress": 'boolean',
|
|
"show_only_filtered_image": 'boolean',
|
|
"output_format": 'string',
|
|
"output_quality": 'number',
|
|
}
|
|
const TASK_DEFAULTS = {
|
|
"sampler_name": "plms",
|
|
"use_stable_diffusion_model": "sd-v1-4",
|
|
"num_inference_steps": 50,
|
|
"guidance_scale": 7.5,
|
|
"negative_prompt": "",
|
|
|
|
"num_outputs": 1,
|
|
"stream_progress_updates": true,
|
|
"stream_image_progress": true,
|
|
"show_only_filtered_image": true,
|
|
"block_nsfw": false,
|
|
"output_format": "png",
|
|
"output_quality": 75,
|
|
"output_lossless": false,
|
|
}
|
|
const TASK_OPTIONAL = {
|
|
"device": 'string',
|
|
"init_image": 'string',
|
|
"mask": 'string',
|
|
"save_to_disk_path": 'string',
|
|
"use_face_correction": 'string',
|
|
"use_upscale": 'string',
|
|
"use_vae_model": 'string',
|
|
"use_hypernetwork_model": 'string',
|
|
"hypernetwork_strength": 'number',
|
|
"output_lossless": 'boolean',
|
|
}
|
|
|
|
// Higer values will result in...
|
|
// pytorch_lightning/utilities/seed.py:60: UserWarning: X is not in bounds, numpy accepts from 0 to 4294967295
|
|
const MAX_SEED_VALUE = 4294967295
|
|
|
|
class RenderTask extends Task {
|
|
constructor(options={}) {
|
|
super(options)
|
|
if (typeof this._reqBody.seed === 'undefined') {
|
|
this._reqBody.seed = Math.floor(Math.random() * (MAX_SEED_VALUE + 1))
|
|
}
|
|
if (typeof typeof this._reqBody.seed === 'number' && (this._reqBody.seed > MAX_SEED_VALUE || this._reqBody.seed < 0)) {
|
|
throw new Error(`seed must be in range 0 to ${MAX_SEED_VALUE}.`)
|
|
}
|
|
|
|
if ('use_cpu' in this._reqBody) {
|
|
if (this._reqBody.use_cpu) {
|
|
this._reqBody.device = 'cpu'
|
|
}
|
|
delete this._reqBody.use_cpu
|
|
}
|
|
if (this._reqBody.init_image) {
|
|
if (typeof this._reqBody.prompt_strength === 'undefined') {
|
|
this._reqBody.prompt_strength = 0.8
|
|
} else if (typeof this._reqBody.prompt_strength !== 'number') {
|
|
throw new Error(`prompt_strength need to be of type number but ${typeof this._reqBody.prompt_strength} was found.`)
|
|
}
|
|
}
|
|
if ('modifiers' in this._reqBody) {
|
|
if (Array.isArray(this._reqBody.modifiers) && this._reqBody.modifiers.length > 0) {
|
|
this._reqBody.modifiers = this._reqBody.modifiers.filter((val) => val.trim())
|
|
if (this._reqBody.modifiers.length > 0) {
|
|
this._reqBody.prompt = `${this._reqBody.prompt}, ${this._reqBody.modifiers.join(', ')}`
|
|
}
|
|
}
|
|
if (typeof this._reqBody.modifiers === 'string' && this._reqBody.modifiers.length > 0) {
|
|
this._reqBody.modifiers = this._reqBody.modifiers.trim()
|
|
if (this._reqBody.modifiers.length > 0) {
|
|
this._reqBody.prompt = `${this._reqBody.prompt}, ${this._reqBody.modifiers}`
|
|
}
|
|
}
|
|
delete this._reqBody.modifiers
|
|
}
|
|
this.checkReqBody()
|
|
}
|
|
|
|
checkReqBody() {
|
|
for (const key in TASK_DEFAULTS) {
|
|
if (typeof this._reqBody[key] === 'undefined') {
|
|
this._reqBody[key] = TASK_DEFAULTS[key]
|
|
}
|
|
}
|
|
for (const key in TASK_REQUIRED) {
|
|
if (typeof this._reqBody[key] !== TASK_REQUIRED[key]) {
|
|
throw new Error(`${key} need to be of type ${TASK_REQUIRED[key]} but ${typeof this._reqBody[key]} was found.`)
|
|
}
|
|
}
|
|
for (const key in this._reqBody) {
|
|
if (key in TASK_REQUIRED) {
|
|
continue
|
|
}
|
|
if (key in TASK_OPTIONAL) {
|
|
if (typeof this._reqBody[key] == "undefined") {
|
|
delete this._reqBody[key]
|
|
console.warn(`reqBody[${key}] was set to undefined. Removing optional key without value...`)
|
|
continue
|
|
}
|
|
if (typeof this._reqBody[key] !== TASK_OPTIONAL[key]) {
|
|
throw new Error(`${key} need to be of type ${TASK_OPTIONAL[key]} but ${typeof this._reqBody[key]} was found.`)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Send current task to server.
|
|
* @param {*} [timeout=-1] Optional timeout value in ms
|
|
* @returns the response from the render request.
|
|
* @memberof Task
|
|
*/
|
|
async post(timeout=-1) {
|
|
performance.mark('make-render-request')
|
|
if (performance.getEntriesByName('click-makeImage', 'mark').length > 0) {
|
|
performance.measure('diff', 'click-makeImage', 'make-render-request')
|
|
console.log('delay between clicking and making the server request:', performance.getEntriesByName('diff', 'measure')[0].duration + ' ms')
|
|
}
|
|
|
|
let jsonResponse = await super.post('/render', timeout)
|
|
if (typeof jsonResponse?.task !== 'number') {
|
|
console.warn('Endpoint error response: ', jsonResponse)
|
|
const event = Object.assign({task:this}, jsonResponse)
|
|
await eventSource.fireEvent(EVENT_UNEXPECTED_RESPONSE, event)
|
|
if ('continueWith' in event) {
|
|
jsonResponse = await Promise.resolve(event.continueWith)
|
|
}
|
|
if (typeof jsonResponse?.task !== 'number') {
|
|
const err = new Error(jsonResponse?.detail || 'Endpoint response does not contains a task ID.')
|
|
this.abort(err)
|
|
throw err
|
|
}
|
|
}
|
|
this._setId(jsonResponse.task)
|
|
if (jsonResponse.stream) {
|
|
this.streamUrl = jsonResponse.stream
|
|
}
|
|
this._setStatus(TaskStatus.waiting)
|
|
return jsonResponse
|
|
}
|
|
|
|
enqueue(progressCallback) {
|
|
return Task.enqueueNew(this, RenderTask, progressCallback)
|
|
}
|
|
*start(progressCallback) {
|
|
if (typeof progressCallback !== 'undefined' && typeof progressCallback !== 'function') {
|
|
throw new Error('progressCallback is not a function. progressCallback type: ' + typeof progressCallback)
|
|
}
|
|
if (this.isStopped) {
|
|
return
|
|
}
|
|
|
|
this._setStatus(TaskStatus.pending)
|
|
progressCallback?.call(this, {reqBody: this._reqBody})
|
|
Object.freeze(this._reqBody)
|
|
|
|
// Post task request to backend
|
|
let renderRequest = undefined
|
|
try {
|
|
renderRequest = yield this.post()
|
|
yield progressCallback?.call(this, {renderResponse: renderRequest})
|
|
} catch (e) {
|
|
yield progressCallback?.call(this, { detail: e.message })
|
|
throw e
|
|
}
|
|
try { // Wait for task to start on server.
|
|
yield this.waitUntil({
|
|
callback: function() { return progressCallback?.call(this, {}) },
|
|
status: TaskStatus.processing,
|
|
})
|
|
} catch (e) {
|
|
this.abort(err)
|
|
throw e
|
|
}
|
|
// Update class status and callback.
|
|
const taskState = (typeof serverState.tasks === 'object' ? serverState.tasks[String(this.id)] : undefined)
|
|
switch(taskState) {
|
|
case 'pending': // Session has pending tasks.
|
|
console.error('Server %o render request %o is still waiting.', serverState, renderRequest)
|
|
//Only update status if not already set by waitUntil
|
|
if (this.status === TaskStatus.init
|
|
|| this.status === TaskStatus.pending
|
|
) {
|
|
// Set status as Waiting in backend.
|
|
this._setStatus(TaskStatus.waiting)
|
|
}
|
|
break
|
|
case 'running':
|
|
case 'buffer':
|
|
// Normal expected messages.
|
|
this._setStatus(TaskStatus.processing)
|
|
break
|
|
case 'completed':
|
|
if (this.isPending) {
|
|
// Set state to processing until we read the reply
|
|
this._setStatus(TaskStatus.processing)
|
|
}
|
|
console.warn('Server %o render request %o completed unexpectedly', serverState, renderRequest)
|
|
break // Continue anyway to try to read cached result.
|
|
case 'error':
|
|
this._setStatus(TaskStatus.failed)
|
|
console.error('Server %o render request %o has failed', serverState, renderRequest)
|
|
break // Still valid, Update UI with error message
|
|
case 'stopped':
|
|
this._setStatus(TaskStatus.stopped)
|
|
console.log('Server %o render request %o was stopped', serverState, renderRequest)
|
|
return false
|
|
default:
|
|
if (!progressCallback) {
|
|
const err = new Error('Unexpected server task state: ' + taskState || 'Undefined')
|
|
this.abort(err)
|
|
throw err
|
|
}
|
|
const response = yield progressCallback.call(this, {})
|
|
if (response instanceof Error) {
|
|
this.abort(response)
|
|
throw response
|
|
}
|
|
if (!response) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Task started!
|
|
// Open the reader.
|
|
const reader = this.reader
|
|
const task = this
|
|
reader.onError = function(response) {
|
|
if (progressCallback) {
|
|
task.abort(new Error(response.statusText))
|
|
return progressCallback.call(task, { response, reader })
|
|
}
|
|
return Task.prototype.onError.call(task, response)
|
|
}
|
|
yield progressCallback?.call(this, { reader })
|
|
|
|
//Start streaming the results.
|
|
const streamGenerator = reader.open()
|
|
let value = undefined
|
|
let done = undefined
|
|
yield progressCallback?.call(this, { stream: streamGenerator })
|
|
do {
|
|
({value, done} = yield streamGenerator.next())
|
|
if (typeof value !== 'object') {
|
|
continue
|
|
}
|
|
yield progressCallback?.call(this, { update: value })
|
|
} while(!done)
|
|
return value
|
|
}
|
|
static start(task, progressCallback) {
|
|
if (typeof task !== 'object') {
|
|
throw new Error ('task is not an object. task type: ' + typeof task)
|
|
}
|
|
if (!(task instanceof Task)) {
|
|
if (task.reqBody) {
|
|
task = new RenderTask(task.reqBody)
|
|
} else {
|
|
task = new RenderTask(task)
|
|
}
|
|
}
|
|
return task.start(progressCallback)
|
|
}
|
|
static run(task, progressCallback) {
|
|
const promiseGenerator = RenderTask.start(task, progressCallback)
|
|
return Task.run(promiseGenerator)
|
|
}
|
|
}
|
|
class FilterTask extends Task {
|
|
constructor(options={}) {
|
|
}
|
|
/** Send current task to server.
|
|
* @param {*} [timeout=-1] Optional timeout value in ms
|
|
* @returns the response from the render request.
|
|
* @memberof Task
|
|
*/
|
|
async post(timeout=-1) {
|
|
let jsonResponse = await super.post('/filter', timeout)
|
|
//this._setId(jsonResponse.task)
|
|
this._setStatus(TaskStatus.waiting)
|
|
}
|
|
enqueue(progressCallback) {
|
|
return Task.enqueueNew(this, FilterTask, progressCallback)
|
|
}
|
|
*start(progressCallback) {
|
|
if (typeof progressCallback !== 'undefined' && typeof progressCallback !== 'function') {
|
|
throw new Error('progressCallback is not a function. progressCallback type: ' + typeof progressCallback)
|
|
}
|
|
if (this.isStopped) {
|
|
return
|
|
}
|
|
}
|
|
static start(task, progressCallback) {
|
|
if (typeof task !== 'object') {
|
|
throw new Error ('task is not an object. task type: ' + typeof task)
|
|
}
|
|
if (!(task instanceof Task)) {
|
|
if (task.reqBody) {
|
|
task = new FilterTask(task.reqBody)
|
|
} else {
|
|
task = new FilterTask(task)
|
|
}
|
|
}
|
|
return task.start(progressCallback)
|
|
}
|
|
static run(task, progressCallback) {
|
|
const promiseGenerator = FilterTask.start(task, progressCallback)
|
|
return Task.run(promiseGenerator)
|
|
}
|
|
}
|
|
|
|
const getSystemInfo = debounce(async function() {
|
|
let systemInfo = {
|
|
devices: {
|
|
all: {},
|
|
active: {},
|
|
},
|
|
hosts: []
|
|
}
|
|
try {
|
|
const res = await fetch('/get/system_info')
|
|
if (!res.ok) {
|
|
console.error('Invalid response fetching devices', res.statusText)
|
|
return systemInfo
|
|
}
|
|
systemInfo = await res.json()
|
|
} catch (e) {
|
|
console.error('error fetching system info', e)
|
|
}
|
|
return systemInfo
|
|
}, 250, true)
|
|
async function getDevices() {
|
|
let systemInfo = getSystemInfo()
|
|
return systemInfo.devices
|
|
}
|
|
async function getHosts() {
|
|
let systemInfo = getSystemInfo()
|
|
return systemInfo.hosts
|
|
}
|
|
|
|
async function getModels() {
|
|
let models = {
|
|
'stable-diffusion': [],
|
|
'vae': [],
|
|
}
|
|
try {
|
|
const res = await fetch('/get/models')
|
|
if (!res.ok) {
|
|
console.error('Invalid response fetching models', res.statusText)
|
|
return models
|
|
}
|
|
models = await res.json()
|
|
console.log('get models response', models)
|
|
} catch (e) {
|
|
console.log('get models error', e)
|
|
}
|
|
return models
|
|
}
|
|
|
|
function getServerCapacity() {
|
|
let activeDevicesCount = Object.keys(serverState?.devices?.active || {}).length
|
|
if (typeof window === "object" && window.document.visibilityState === 'hidden') {
|
|
activeDevicesCount = 1 + activeDevicesCount
|
|
}
|
|
return activeDevicesCount
|
|
}
|
|
|
|
let idleEventPromise = undefined
|
|
function continueTasks() {
|
|
if (typeof navigator?.scheduling?.isInputPending === 'function') {
|
|
const inputPendingOptions = {
|
|
// Report mouse/pointer move events when queue is empty.
|
|
// Delay idle after mouse moves stops.
|
|
includeContinuous: Boolean(task_queue.size <= 0 && concurrent_generators.size <= 0)
|
|
}
|
|
if (navigator.scheduling.isInputPending(inputPendingOptions)) {
|
|
// Browser/User still active.
|
|
return asyncDelay(CONCURRENT_TASK_INTERVAL)
|
|
}
|
|
}
|
|
const serverCapacity = getServerCapacity()
|
|
if (task_queue.size <= 0 && concurrent_generators.size <= 0) {
|
|
if (!idleEventPromise?.isPending) {
|
|
idleEventPromise = makeQuerablePromise(eventSource.fireEvent(EVENT_IDLE, {capacity: serverCapacity, idle: true}))
|
|
}
|
|
// Calling idle could result in task being added to queue.
|
|
// if (task_queue.size <= 0 && concurrent_generators.size <= 0) {
|
|
// return asyncDelay(IDLE_COOLDOWN).then(() => idleEventPromise)
|
|
// }
|
|
}
|
|
if (task_queue.size < serverCapacity) {
|
|
if (!idleEventPromise?.isPending) {
|
|
idleEventPromise = makeQuerablePromise(eventSource.fireEvent(EVENT_IDLE, {capacity: serverCapacity - task_queue.size}))
|
|
}
|
|
}
|
|
const completedTasks = []
|
|
for (let [generator, promise] of concurrent_generators.entries()) {
|
|
if (promise.isPending) {
|
|
continue
|
|
}
|
|
let value = promise.resolvedValue?.value || promise.resolvedValue
|
|
if (promise.isRejected) {
|
|
console.error(promise.rejectReason)
|
|
const event = {generator, reason: promise.rejectReason}
|
|
eventSource.fireEvent(EVENT_UNHANDLED_REJECTION, event)
|
|
if ('continueWith' in event) {
|
|
value = Promise.resolve(event.continueWith)
|
|
} else {
|
|
concurrent_generators.delete(generator)
|
|
completedTasks.push({generator, promise})
|
|
continue
|
|
}
|
|
}
|
|
if (value instanceof Promise) {
|
|
promise = makeQuerablePromise(value.then((val) => ({done: promise.resolvedValue?.done, value: val})))
|
|
concurrent_generators.set(generator, promise)
|
|
continue
|
|
}
|
|
weak_results.set(generator, value)
|
|
if (promise.resolvedValue?.done) {
|
|
concurrent_generators.delete(generator)
|
|
completedTasks.push({generator, promise})
|
|
continue
|
|
}
|
|
|
|
promise = generator.next(value)
|
|
if (!(promise instanceof Promise)) {
|
|
promise = Promise.resolve(promise)
|
|
}
|
|
promise = makeQuerablePromise(promise)
|
|
concurrent_generators.set(generator, promise)
|
|
}
|
|
|
|
for (let [task, generator] of task_queue.entries()) {
|
|
const cTsk = completedTasks.find((item) => item.generator === generator)
|
|
if (cTsk?.promise?.rejectReason || task.hasFailed) {
|
|
eventSource.fireEvent(EVENT_TASK_ERROR, {task, generator, reason: cTsk?.promise?.rejectReason || task.exception })
|
|
task_queue.delete(task)
|
|
continue
|
|
}
|
|
if (task.isCompleted || task.isStopped || cTsk) {
|
|
const eventEndArgs = {task, generator}
|
|
if (task.isStopped) {
|
|
eventEndArgs.stopped = true
|
|
}
|
|
eventSource.fireEvent(EVENT_TASK_END, eventEndArgs)
|
|
task_queue.delete(task)
|
|
continue
|
|
}
|
|
if (concurrent_generators.size > serverCapacity) {
|
|
break
|
|
}
|
|
if (!generator) {
|
|
if (typeof task.start === 'function') {
|
|
generator = task.start()
|
|
}
|
|
} else if (concurrent_generators.has(generator)) {
|
|
continue
|
|
}
|
|
const event = {task, generator};
|
|
const beforeStart = eventSource.fireEvent(EVENT_TASK_START, event) // optional beforeStart promise to wait on before starting task.
|
|
const promise = makeQuerablePromise(beforeStart.then(() => Promise.resolve(event.beforeStart)))
|
|
concurrent_generators.set(event.generator, promise)
|
|
task_queue.set(task, event.generator)
|
|
}
|
|
const promises = Array.from(concurrent_generators.values())
|
|
if (promises.length <= 0) {
|
|
return asyncDelay(CONCURRENT_TASK_INTERVAL)
|
|
}
|
|
return Promise.race(promises).finally(continueTasks)
|
|
}
|
|
let taskPromise = undefined
|
|
function startCheck() {
|
|
if (taskPromise?.isPending) {
|
|
return
|
|
}
|
|
do {
|
|
if (taskPromise?.resolvedValue instanceof Promise) {
|
|
taskPromise = makeQuerablePromise(taskPromise.resolvedValue)
|
|
continue
|
|
}
|
|
if (typeof navigator?.scheduling?.isInputPending === 'function' && navigator.scheduling.isInputPending()) {
|
|
return
|
|
}
|
|
const continuePromise = continueTasks().catch(async function(err) {
|
|
console.error(err)
|
|
await eventSource.fireEvent(EVENT_UNHANDLED_REJECTION, {reason: err})
|
|
await asyncDelay(RETRY_DELAY_ON_ERROR)
|
|
})
|
|
taskPromise = makeQuerablePromise(continuePromise)
|
|
} while(taskPromise?.isResolved)
|
|
}
|
|
|
|
const SD = {
|
|
ChunkedStreamReader,
|
|
ServerStates,
|
|
TaskStatus,
|
|
Task,
|
|
RenderTask,
|
|
FilterTask,
|
|
|
|
Events: EVENTS_TYPES,
|
|
init: async function(options={}) {
|
|
if ('events' in options) {
|
|
for (const key in options.events) {
|
|
eventSource.addEventListener(key, options.events[key])
|
|
}
|
|
}
|
|
await healthCheck()
|
|
setInterval(healthCheck, HEALTH_PING_INTERVAL)
|
|
setInterval(startCheck, CONCURRENT_TASK_INTERVAL)
|
|
},
|
|
|
|
/** Add a new event listener
|
|
*/
|
|
addEventListener: (...args) => eventSource.addEventListener(...args),
|
|
/** Remove the event listener
|
|
*/
|
|
removeEventListener: (...args) => eventSource.removeEventListener(...args),
|
|
|
|
isServerAvailable,
|
|
getServerCapacity,
|
|
|
|
getSystemInfo,
|
|
getDevices,
|
|
getHosts,
|
|
|
|
getModels,
|
|
|
|
render: (...args) => RenderTask.run(...args),
|
|
filter: (...args) => FilterTask.run(...args),
|
|
waitUntil,
|
|
};
|
|
|
|
Object.defineProperties(SD, {
|
|
serverState: {
|
|
configurable: false,
|
|
get: () => serverState,
|
|
},
|
|
isAvailable: {
|
|
configurable: false,
|
|
get: () => isServerAvailable(),
|
|
},
|
|
serverCapacity: {
|
|
configurable: false,
|
|
get: () => getServerCapacity(),
|
|
},
|
|
sessionId: {
|
|
configurable: false,
|
|
get: () => sessionId,
|
|
set: (val) => {
|
|
if (typeof val === 'undefined') {
|
|
throw new Error("Can't set sessionId to undefined.")
|
|
}
|
|
sessionId = val
|
|
},
|
|
},
|
|
MAX_SEED_VALUE: {
|
|
configurable: false,
|
|
get: () => MAX_SEED_VALUE,
|
|
},
|
|
activeTasks: {
|
|
configurable: false,
|
|
get: () => task_queue,
|
|
},
|
|
})
|
|
Object.defineProperties(getGlobal(), {
|
|
SD: {
|
|
configurable: false,
|
|
get: () => SD,
|
|
},
|
|
sessionId: { //TODO Remove in the future in favor of SD.sessionId
|
|
configurable: false,
|
|
get: () => {
|
|
console.warn('Deprecated window.sessionId has been replaced with SD.sessionId.')
|
|
console.trace()
|
|
return SD.sessionId
|
|
},
|
|
set: (val) => {
|
|
console.warn('Deprecated window.sessionId has been replaced with SD.sessionId.')
|
|
console.trace()
|
|
SD.sessionId = val
|
|
}
|
|
}
|
|
})
|
|
})()
|