353 lines
14 KiB
Nim
353 lines
14 KiB
Nim
# The contents of this file are subject to the Common Public Attribution License
|
|
# Version 1.0 (the “License”); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
# https://myou.dev/licenses/LICENSE-CPAL. The License is based on the Mozilla
|
|
# Public License Version 1.1 but Sections 14 and 15 have been added to cover use
|
|
# of software over a computer network and provide for limited attribution for
|
|
# the Original Developer. In addition, Exhibit A has been modified to be
|
|
# consistent with Exhibit B.
|
|
#
|
|
# Software distributed under the License is distributed on an “AS IS” basis,
|
|
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
|
|
# the specific language governing rights and limitations under the License.
|
|
#
|
|
# The Original Code is Myou Engine.
|
|
#
|
|
# the Original Developer is the Initial Developer.
|
|
#
|
|
# The Initial Developer of the Original Code is the Myou Engine developers.
|
|
# All portions of the code written by the Myou Engine developers are Copyright
|
|
# (c) 2024. All Rights Reserved.
|
|
#
|
|
# Alternatively, the contents of this file may be used under the terms of the
|
|
# GNU Affero General Public License version 3 (the [AGPL-3] License), in which
|
|
# case the provisions of [AGPL-3] License are applicable instead of those above.
|
|
#
|
|
# If you wish to allow use of your version of this file only under the terms of
|
|
# the [AGPL-3] License and not to allow others to use your version of this file
|
|
# under the CPAL, indicate your decision by deleting the provisions above and
|
|
# replace them with the notice and other provisions required by the [AGPL-3]
|
|
# License. If you do not delete the provisions above, a recipient may use your
|
|
# version of this file under either the CPAL or the [AGPL-3] License.
|
|
|
|
|
|
## A LoadableResource is similar to a JS promise or a Future, except that you can
|
|
## store it to load it any number of times, and instead of providing an object,
|
|
## you get a pointer, a length, and a function to tell it you're done with it.
|
|
## You cannot have more than one callback, but you can obtain the results of many
|
|
## with `loadAll` or get a callback when many are done with `allDone`
|
|
##
|
|
## TODO IMPORTANT: probably not thread safe, call all functions in the same thread
|
|
## if you use loadAll and allDone
|
|
##
|
|
## TODO: investigate the use of signals instead of callbacks
|
|
|
|
|
|
type LoadableResourceStatus* = enum
|
|
NotStarted
|
|
Started
|
|
Finished
|
|
Error
|
|
|
|
type Result = tuple[ok: bool, err: string, p: pointer, len: int]
|
|
|
|
type LoadableResource* = ref object of RootObj
|
|
status: LoadableResourceStatus
|
|
start_func: proc(self: LoadableResource)
|
|
onload_func: proc(ok: bool, err: string, p: pointer, len: int)
|
|
done_func: proc()
|
|
cancel_func: proc()
|
|
str*: proc(): string
|
|
use_threads: bool
|
|
result: ref Result # only to be used with loadAll
|
|
|
|
proc newLoadableResource*[T: LoadableResource](
|
|
start: proc(self: LoadableResource),
|
|
done: proc() = nil,
|
|
str: proc(): string = nil,
|
|
use_threads = false,
|
|
): T =
|
|
new(result)
|
|
result.start_func = start
|
|
result.done_func = done
|
|
result.str = str
|
|
if str == nil:
|
|
result.str = proc(): string = ""
|
|
result.use_threads = use_threads
|
|
|
|
# main -> thread channels
|
|
var to_start: Channel[LoadableResource]
|
|
# main <- thread channels
|
|
var to_return: Channel[(LoadableResource, bool, string, pointer, int)]
|
|
var to_done: Channel[LoadableResource]
|
|
|
|
proc start*[T: LoadableResource](self: T) =
|
|
self.status = Started
|
|
if self.use_threads:
|
|
to_start.send self
|
|
else:
|
|
self.start_func(self)
|
|
|
|
proc doneImpl*[T: LoadableResource](self: T) =
|
|
if self.status == Started:
|
|
self.cancel()
|
|
else:
|
|
self.status = NotStarted
|
|
if self.done_func != nil:
|
|
self.done_func()
|
|
|
|
proc `onload=`*[T: LoadableResource](self: T, onload_func: proc(ok: bool, err: string, p: pointer, len: int)) =
|
|
self.onload_func = onload_func
|
|
|
|
proc onload*[T: LoadableResource](self: T, ok: bool, err: string, p: pointer, len: int) =
|
|
if self.status == Started:
|
|
self.status = if ok: Finished else: Error
|
|
if self.result != nil:
|
|
self.result[] = (ok, err, p, len)
|
|
if self.onload_func != nil:
|
|
if self.use_threads:
|
|
to_return.send((self.LoadableResource, ok, err, p, len))
|
|
else:
|
|
self.onload_func(ok, err, p, len)
|
|
else: # cancelled
|
|
self.doneImpl()
|
|
|
|
# TODO: check if we can always use destructors instead of calling this
|
|
proc done*[T: LoadableResource](self: T) =
|
|
if self.use_threads:
|
|
to_done.send self
|
|
else:
|
|
self.doneImpl()
|
|
|
|
proc cancel*[T: LoadableResource](self: T) =
|
|
if self.status != Started:
|
|
return
|
|
if self.cancel_func == nil:
|
|
# self.`onload=`(proc(ok, err, p, len: auto) =
|
|
# self.done())
|
|
self.onload_func = proc(ok: bool, err: string, p: pointer, len: int) =
|
|
self.done()
|
|
else:
|
|
self.cancel_func()
|
|
self.status = NotStarted
|
|
|
|
# proc allDone*(list: seq[LoadableResource], done: proc()) =
|
|
|
|
|
|
# proc loadAll*(list: seq[LoadableResource], onload: proc(all_ok: bool, result: seq[Result])) =
|
|
# for i,res in list:
|
|
# if not defined(release):
|
|
# assert res.status == NotStarted, "All resources in the list must be not started"
|
|
# assert res.onload_func == nil, "All resources in the list must not have onload"
|
|
# new(res.result)
|
|
# res.start()
|
|
# list.allDone, proc() =
|
|
# var results = newSeqOfCap[Result](list.len)
|
|
# for res in list:
|
|
# results.add res.result[]
|
|
# res.result = nil
|
|
|
|
var worker: Thread[void]
|
|
proc workerThreadProc() {.thread.} =
|
|
while true:
|
|
let res = to_start.recv()
|
|
if res == nil:
|
|
break
|
|
cast[proc(self: LoadableResource) {.gcsafe.}](res.start_func)(res)
|
|
|
|
worker.createThread(workerThreadProc)
|
|
to_start.open()
|
|
to_return.open()
|
|
to_done.open()
|
|
|
|
proc updateLoadableWorkerThreads*() =
|
|
while true:
|
|
let tried = to_return.tryRecv()
|
|
if not tried.dataAvailable:
|
|
break
|
|
let (res, ok, err, p, len) = tried.msg
|
|
res.onload_func(ok, err, p, len)
|
|
while true:
|
|
let tried = to_done.tryRecv()
|
|
if not tried.dataAvailable:
|
|
break
|
|
tried.msg.done_func()
|
|
|
|
type Fetch* = ref object of LoadableResource
|
|
custom: pointer
|
|
auto_done: bool
|
|
|
|
when not defined(onlyLocalFiles):
|
|
when defined(emscripten):
|
|
type
|
|
FetchAttribute = enum
|
|
LoadToMemory = 1
|
|
StreamData = 2
|
|
PersistFile = 4
|
|
Append = 8
|
|
Replace = 16
|
|
NoDownload = 32
|
|
Synchronous = 64
|
|
Waitable = 128
|
|
emscripten_fetch_attr_t {.importc,nodecl,header:"emscripten/fetch.h".} = object
|
|
requestMethod: array[32, char]
|
|
userData: pointer
|
|
onsuccess: proc(fetch: ptr emscripten_fetch_t) {.cdecl.}
|
|
onerror: proc(fetch: ptr emscripten_fetch_t) {.cdecl.}
|
|
onprogress: proc(fetch: ptr emscripten_fetch_t) {.cdecl.}
|
|
onreadystatechange: proc(fetch: ptr emscripten_fetch_t) {.cdecl.}
|
|
attributes: FetchAttribute # uint32
|
|
timeoutMSecs: uint32
|
|
withCredentials: bool # actually int32
|
|
destinationPath: cstring
|
|
userName: cstring
|
|
password: cstring
|
|
requestHeaders: ptr UncheckedArray[cstring]
|
|
overriddenMimeType: cstring
|
|
requestData: cstring
|
|
requestDataSize: uint
|
|
emscripten_fetch_t {.importc,nodecl.} = object
|
|
id: uint32
|
|
userData: pointer
|
|
url: cstring
|
|
data: pointer
|
|
numBytes: uint64
|
|
dataOffset: uint64
|
|
totalBytes: uint64
|
|
readyState: uint16
|
|
status: uint16
|
|
statusText: array[64, char]
|
|
EmscriptenResult = enum
|
|
TimedOut = -8
|
|
NoData = -7
|
|
Failed = -6
|
|
InvalidParam = -5
|
|
UnknownTarget = -4
|
|
InvalidTarget = -3
|
|
FailedNotDeferred = -2
|
|
NotSupported = -1
|
|
Success = 0
|
|
Deferred = 1
|
|
const EMSCRIPTEN_RESULT_STRINGS = [
|
|
"TimedOut","NoData","Failed","InvalidParam",
|
|
"UnknownTarget","InvalidTarget","FailedNotDeferred",
|
|
"NotSupported","Success","Deferred"]
|
|
proc emscripten_fetch_attr_init(fetch_attr: var emscripten_fetch_attr_t) {.importc,nodecl.}
|
|
proc emscripten_fetch(fetch_attr: var emscripten_fetch_attr_t, url: cstring): ptr emscripten_fetch_t {.importc,nodecl.}
|
|
proc emscripten_fetch_wait(fetch: ptr emscripten_fetch_t, timeoutMSecs: float): EmscriptenResult {.importc,nodecl.}
|
|
proc emscripten_fetch_close(fetch: ptr emscripten_fetch_t): EmscriptenResult {.importc,nodecl.}
|
|
proc emscripten_fetch_get_response_headers_length(fetch: ptr emscripten_fetch_t): uint {.importc,nodecl.}
|
|
proc emscripten_fetch_get_response_headers(fetch: ptr emscripten_fetch_t, dst: cstring , dstSizeBytes: uint): uint {.importc,nodecl.}
|
|
proc emscripten_fetch_unpack_response_headers(headersString: cstring): ptr UncheckedArray[cstring] {.importc,nodecl.}
|
|
proc emscripten_fetch_free_unpacked_response_headers(unpackedHeaders: ptr UncheckedArray[cstring]) {.importc,nodecl.}
|
|
|
|
else:
|
|
import std/httpclient
|
|
|
|
import std/memfiles
|
|
import std/strutils
|
|
import std/strformat
|
|
|
|
func escapeUTF8*(s: string): string =
|
|
result = newStringOfCap(s.len + 5)
|
|
for i,c in s:
|
|
if c > ' ' and c <= '~':
|
|
result &= c
|
|
else:
|
|
result &= '%' & c.byte.toHex
|
|
|
|
proc loadUri*(
|
|
uri: string,
|
|
onload_func: proc(ok: bool, err: string, data: pointer, len: int) = nil,
|
|
range = (-1,-1),
|
|
auto_start = true,
|
|
auto_done = true,
|
|
): Fetch {.discardable.} =
|
|
|
|
echo "fetching ", uri
|
|
|
|
var start_func: proc(self: LoadableResource)
|
|
var done_func: proc()
|
|
var self: Fetch
|
|
var uri = uri
|
|
var use_threads = false
|
|
when not defined(onlyLocalFiles):
|
|
when defined(emscripten):
|
|
const is_remote = true
|
|
else:
|
|
let is_remote = uri.startswith("http://") or uri.startswith("https://")
|
|
|
|
if is_remote:
|
|
uri = uri.escapeUTF8
|
|
when defined(emscripten):
|
|
start_func = proc(self: LoadableResource) =
|
|
var attr: emscripten_fetch_attr_t
|
|
emscripten_fetch_attr_init(attr)
|
|
attr.userData = cast[pointer](self)
|
|
attr.requestMethod.addr.copyMem("GET".cstring, 4)
|
|
attr.attributes = LoadToMemory
|
|
attr.onsuccess = proc(fetch: ptr emscripten_fetch_t) {.cdecl.} =
|
|
var self = cast[Fetch](fetch.userData)
|
|
self.custom = fetch.pointer
|
|
self.onload(true, "", fetch.data, fetch.numBytes.int)
|
|
if self.auto_done:
|
|
self.done()
|
|
attr.onerror = proc(fetch: ptr emscripten_fetch_t) {.cdecl.} =
|
|
var self = cast[Fetch](fetch.userData)
|
|
let err_msg = $fetch.statusText.addr.cstring
|
|
let err = emscripten_fetch_close(fetch)
|
|
# self.onload(false, &"Error: {err_msg} ({EMSCRIPTEN_RESULT_STRINGS[err.int + 8]})", nil, 0)
|
|
self.onload(false, &"Error fetching {fetch.url}: {err_msg}", nil, 0)
|
|
if self.auto_done:
|
|
self.done()
|
|
discard emscripten_fetch(attr, uri.cstring)
|
|
done_func = proc() =
|
|
if self.custom != nil:
|
|
discard emscripten_fetch_close(cast[ptr emscripten_fetch_t](self.custom))
|
|
self.custom = nil
|
|
else:
|
|
use_threads = true
|
|
var client = newHttpClient()
|
|
var response: string
|
|
start_func = proc(self: LoadableResource) =
|
|
let self = cast[Fetch](self)
|
|
var ok = false
|
|
try:
|
|
response = client.getContent(uri)
|
|
ok = true
|
|
except:
|
|
self.onload(false, &"Error fetching {uri}: {getCurrentExceptionMsg()}", nil, 0)
|
|
if ok:
|
|
let p = response[0].addr
|
|
self.onload(true, "", p, response.len)
|
|
if self.auto_done:
|
|
self.done()
|
|
done_func = proc() =
|
|
response = ""
|
|
|
|
if not is_remote:
|
|
start_func = proc(self: LoadableResource) =
|
|
when not defined(release):
|
|
var done_called = false
|
|
try:
|
|
var memfile = memfiles.open(uri, mode=fmRead)
|
|
self.done_func = proc() =
|
|
when not defined(release):
|
|
assert not done_called, "Done is being called multiple times. Did you forget to set auto_done = false?"
|
|
done_called = true
|
|
memfile.close()
|
|
self.onload(true, "", memfile.mem, memfile.size)
|
|
# TODO!!!! check whether these objects are freed
|
|
# and if we have to add a destructor, or what
|
|
except OSError:
|
|
self.onload(false, "Could not open file: " & uri, nil, 0)
|
|
if cast[Fetch](self).auto_done:
|
|
self.done()
|
|
proc str(): string = uri
|
|
self = newLoadableResource[Fetch](start_func, done_func, str, use_threads=use_threads)
|
|
self.onload_func = onload_func
|
|
self.auto_done = auto_done
|
|
if auto_start:
|
|
start(self)
|
|
return self
|