# 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 result: ref Result # only to be used with loadAll proc newLoadableResource*[T: LoadableResource]( start: proc(self: LoadableResource), done: proc() = nil, str: proc(): string = nil, ): T = new(result) result.start_func = start result.done_func = done result.str = str if str == nil: result.str = proc(): string = "" proc start*[T: LoadableResource](self: T) = self.status = Started self.start_func(self) 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: self.onload_func(ok, err, p, len) else: # cancelled self.status = NotStarted if self.done_func != nil: self.done_func() # TODO: check if we can always use destructors instead of calling this proc done*[T: LoadableResource](self: T) = if self.status == Started: self.cancel() else: self.status = NotStarted if self.done_func != nil: self.done_func() 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 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 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: 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) self.onload_func = onload_func self.auto_done = auto_done if auto_start: start(self) return self