diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a0c25b9 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "request": "launch", + "name": "Launch Program", + "type": "node", + "program": "${workspaceFolder}/server.tsx", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "C:\\Users\\Seth\\.deno\\bin\\deno.EXE", + "runtimeArgs": [ + "run", + "--no-lock", + "--unstable", + "--inspect-wait", + "--allow-all" + ], + "attachSimplePort": 9229 + } + ] +} \ No newline at end of file diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..b8e174d --- /dev/null +++ b/deno.lock @@ -0,0 +1,16 @@ +{ + "version": "2", + "remote": { + "http://localhost:4507/lib/iso.tsx": "edf1cf4c539900040c75824eac8472e0ec43098b13af4049bd79ddbd76a5346d", + "https://esm.sh/preact@10.13.2/compat": "1cf68e0c8c6c84b60d42f30665403b67229c16ff5206824709b19df60ba9cdc3", + "https://esm.sh/stable/preact@10.13.2/deno/compat.js": "3151a948abd84aa75dfc9e57733da7e1a45fff7a25de58c7b6025b923874b508", + "https://esm.sh/stable/preact@10.13.2/deno/hooks.js": "c7a8e703bcbc6a05949f329b618c33d5d1ea5fee113ddcea44ff0f527af8556f", + "https://esm.sh/stable/preact@10.13.2/deno/preact.mjs": "365fab897381f4f403f859c5d12939084560545567108cc90dae901bbe892578", + "https://esm.sh/v116/preact@10.13.2/compat/src/index.d.ts": "d02f015638a40e32649151e011cfda7b520d66f7fbd3c12a28fa03de2a5e1421", + "https://esm.sh/v116/preact@10.13.2/compat/src/suspense-list.d.ts": "b8e274324392157ce476ef3a48ae215c9f7003b08525d140645f19eab20d1948", + "https://esm.sh/v116/preact@10.13.2/compat/src/suspense.d.ts": "81f5266e0977a94347505d11b8103024211f2b4f3b2eb2aa276a10d8fd169e65", + "https://esm.sh/v116/preact@10.13.2/hooks/src/index.d.ts": "5c29febb624fc25d71cb0e125848c9b711e233337a08f7eacfade38fd4c14cc3", + "https://esm.sh/v116/preact@10.13.2/src/index.d.ts": "65398710de6aa0a07412da79784e05e6e96763f51c7c91b77344d2d0af06385c", + "https://esm.sh/v116/preact@10.13.2/src/jsx.d.ts": "9ac9b82c199fa7b04748807d750eba1a106c0be52041b8617416f88d6fc0a257" + } +} diff --git a/example/app.tsx b/example/app.tsx index ea4529d..2f4abe9 100644 --- a/example/app.tsx +++ b/example/app.tsx @@ -1,16 +1,24 @@ import TWPreTail from "https://esm.sh/v115/@twind/preset-tailwind@1.1.4/es2022/preset-tailwind.mjs"; import TWPreAuto from "https://esm.sh/v115/@twind/preset-autoprefix@1.0.7/es2022/preset-autoprefix.mjs"; import React from "react"; -import Component from "./deep/component.tsx"; import * as Iso from "@eno/iso"; +const Comp = React.lazy(()=>import("./deep/component.tsx")); + export default ()=> { return
-

Title!!

-

subtitle

- + + +

Title!!

+

suspended:

+ Loading!
}> + + diff --git a/example/deep/component.tsx b/example/deep/component.tsx index 124ecfc..7fa1bf2 100644 --- a/example/deep/component.tsx +++ b/example/deep/component.tsx @@ -4,14 +4,20 @@ import * as Iso from "@eno/iso"; export default ()=> { const [countGet, countSet] = React.useState(1); - const [routeGet, routeSet] = Iso.Router.Consumer(); + type CatFact = {fact:string, length:number}|undefined; + const [Data, Updating] = Iso.Fetch.Use(`https://catfact.ninja/fact`); + + + console.log("render!!") + return
- Route is: {routeGet.Path.toString()} - - Component!!! + Component Route is: {routeGet.Path.toString()} + a link +

Data:{Data && (Data as CatFact)?.fact}

+

Status:{Updating?'loading':'done'}

; }; \ No newline at end of file diff --git a/fetch.test.tsx b/fetch.test.tsx new file mode 100644 index 0000000..1a16c21 --- /dev/null +++ b/fetch.test.tsx @@ -0,0 +1,35 @@ +import {Fetch} from "./lib/iso.tsx"; + + +const delay =async(inHandler:()=>void, inDelay:number):Promise=> +{ + return new Promise((accept)=>{ + setTimeout(()=>{ + accept(inHandler()); + }, inDelay); + }); +}; + +const queue = [1, 2, 3]; +while(queue.length) +{ + await(delay(()=>{console.log(queue.pop())}, 1000)) +} + +console.log("all done!"); + +/* +let r1, r2, r3; + +delay(()=>{r1 = Fetch.Request(`https://catfact.ninja/fact`, undefined, 0.2); console.log(r1); }, 10); +delay(()=>{r2 = Fetch.Request(`https://catfact.ninja/fact`, undefined, 0.2); console.log(r2); }, 20); +delay(()=>{r3 = Fetch.Request(`https://catfact.ninja/fact`, undefined, 0.2); console.log(r3); }, 2000); + + +await delay(()=>{}, 3000); + +console.log(r1); +console.log(r2); +console.log(r3); + +*/ \ No newline at end of file diff --git a/lib/iso.tsx b/lib/iso.tsx index 2b1789a..1bb54ea 100644 --- a/lib/iso.tsx +++ b/lib/iso.tsx @@ -158,4 +158,113 @@ export const Switch =({children}:{children:Children})=> return fallback; }; export const Case =({children, value}:{children:Children, value?:string, default?:true})=>null; -export const useRouteVars =()=> React.useContext(SwitchContext).keys; \ No newline at end of file +export const useRouteVars =()=> React.useContext(SwitchContext).keys; + +export type FetchCachOptions = {CacheFor:number, CacheOnServer:boolean, DelaySSR:boolean, Seed:boolean}; +export type FetchRecord = {URL:string, Promise?:Promise, CachedAt:number, Error?:string, JSON?:object} & FetchCachOptions; +type FetchGuide = [Record:FetchRecord, Init:boolean, Listen:boolean]; +export type FetchHookState = [Data:undefined|object, Updating:boolean]; +export const Fetch = { + Cache:new Map() as Map, + ServerBlocking:false as false|Promise[], + ServerTouched:false as false|Set, + ServerRemove:false as false|Set, + Seed(seed:FetchRecord[]) + { + seed.forEach(r=>{ + //r.Promise = Promise.resolve(r); + Fetch.Cache.set(r.URL, r) + }); + }, + DefaultOptions:{CacheFor:60, CacheOnServer:true, DelaySSR:true, Seed:true} as FetchCachOptions, + Request(URL:string, Init?:RequestInit|null, CacheFor:number = 60, CacheOnServer:boolean = true, DelaySSR:boolean = true, Seed:boolean = true):FetchGuide + { + let check = Fetch.Cache.get(URL); + + const load =(inCheck:FetchRecord)=> + { + Fetch.Cache.set(URL, inCheck); + inCheck.CachedAt = 0; + inCheck.Promise = fetch(URL, Init?Init:undefined).then(resp=>resp.json()).then((json)=>{ + inCheck.JSON = json; + inCheck.CachedAt = new Date().getTime(); + console.log(`...cached!`); + return inCheck; + }); + return inCheck; + }; + + if(!check) + { + // not in the cache + // - listen + + console.log(`making new cache record...`); + return [load({URL, CacheFor, CachedAt:0, CacheOnServer, DelaySSR, Seed}), false, true]; + } + else if(check.CachedAt == 0) + { + // loading started but not finished + // - listen + // - possibly init if there is something in JSON + + console.log(`currently being cached...`); + return [check, check.JSON ? true : false, true]; + } + else + { + console.log(`found in cache...`); + let secondsAge = (new Date().getTime() - check.CachedAt)/1000; + if(secondsAge > check.CacheFor) + { + // cached but expired + // - listen + // - init + console.log(`...outdated...`); + return [load(check), true, true]; + } + else + { + // cached and ready + // - init + console.log(`...retrieved!`); + return [check, true, false]; + } + + } + }, + + Use(URL:string, Init?:RequestInit|null, Options?:FetchCachOptions) + { + const config = {...Fetch.DefaultOptions, ...Options}; + const [receipt, init, listen] = Fetch.Request(URL, Init, config.CacheFor, config.CacheOnServer, config.DelaySSR, config.Seed); + const initialState:FetchHookState = init ? [receipt.JSON, listen] : [undefined, true]; + const [cacheGet, cacheSet] = React.useState(initialState); + + if(Fetch.ServerBlocking && Fetch.ServerTouched && config.DelaySSR) // if server-side rendering + { + if(listen) // if the request is pending + { + receipt.Promise && Fetch.ServerBlocking.push(receipt.Promise); // add promise to blocking list + return [undefined, listen] as FetchHookState; // no need to return any actual data while waiting server-side + } + else // if request is ready + { + receipt.Seed && Fetch.ServerTouched.add(receipt); // add record to client seed list (if specified in receipt.seed) + return [receipt.JSON, false] as FetchHookState; + } + } + + React.useEffect(()=> + { + if(listen) + { + //const receipt = Fetch.Request(URL, Init, CacheFor, CacheOnServer, DelaySSR); + receipt.Promise?.then(()=>cacheSet([receipt.JSON, receipt.CachedAt === 0])); + } + } + , []); + + return cacheGet; + } +}; \ No newline at end of file diff --git a/server.tsx b/server.tsx index 300d9c6..385f1ff 100644 --- a/server.tsx +++ b/server.tsx @@ -2,6 +2,7 @@ import * as ESBuild from 'https://deno.land/x/esbuild@v0.14.45/mod.js'; import * as MIME from "https://deno.land/std@0.180.0/media_types/mod.ts"; import { debounce } from "https://deno.land/std@0.151.0/async/debounce.ts"; import SSR from "https://esm.sh/v113/preact-render-to-string@6.0.2"; +import Prepass from "https://esm.sh/preact-ssr-prepass@1.2.0"; import * as Twind from "https://esm.sh/@twind/core@1.1.3"; import React from "react"; import * as Iso from "@eno/iso"; @@ -35,7 +36,8 @@ const Transpileable =(inFilePath:string):boolean=> }; const Transpile =async(inCode:string, inKey:string):Promise=> { - const transpile = await ESBuild.transform(inCode, { loader: "tsx", minify:true}); + const transpile = await ESBuild.transform(inCode, { loader: "tsx", sourcemap:"inline", minify:true, sourcefile:inKey}); + Transpiled.set(inKey, transpile.code); return transpile.code; }; @@ -221,10 +223,36 @@ FileListen("${url.pathname}", reloadHandler);`; } else { - const results = Twind.extract(SSR(), TwindInst); + Iso.Fetch.ServerBlocking = []; + Iso.Fetch.ServerTouched = new Set(); + Iso.Fetch.ServerRemove = new Set(); + let app = ; + await Prepass(app) + let bake = SSR(app); + while(Iso.Fetch.ServerBlocking.length) + { + await Promise.all(Iso.Fetch.ServerBlocking); + Iso.Fetch.ServerBlocking = []; + // at this point, anything that was requested that was not cached, has now been loaded and cached + // this next render will use cached resources. using a cached resource (if its "Seed" is true) adds it to the "touched" set. + app = ; + await Prepass(app) + bake = SSR(app); + } + + const seed:Iso.FetchRecord[] = []; + Iso.Fetch.ServerTouched.forEach((record)=>{ + const r:Iso.FetchRecord = {...record}; + delete r.Promise; + seed.push(r); + }); + Iso.Fetch.ServerTouched = false; + + const results = Twind.extract(bake, TwindInst); type = `text/html`; - body = ` + body = +` ${Iso.Meta.title} @@ -236,12 +264,14 @@ FileListen("${url.pathname}", reloadHandler);`;
${results.html}
`;