Compare commits
	
		
			4 commits
		
	
	
		
			530a89755c
			...
			f20b5da8e4
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | f20b5da8e4 | ||
|  | f0456337d5 | ||
|  | 0fca81dc93 | ||
|  | bffa896184 | 
					 10 changed files with 166 additions and 55 deletions
				
			
		
							
								
								
									
										34
									
								
								.forgejo/workflows/checkpr.yml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								.forgejo/workflows/checkpr.yml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,34 @@ | ||||||
|  | 
 | ||||||
|  | on: | ||||||
|  |   pull_request: | ||||||
|  |     types: [opened, synchronize, reopened] | ||||||
|  | 
 | ||||||
|  | jobs: | ||||||
|  |   depoly: | ||||||
|  |     runs-on: fedora-41 | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout | ||||||
|  |         uses: actions/checkout@v4 | ||||||
|  | 
 | ||||||
|  |       - name: Setup Bun | ||||||
|  |         uses: oven-sh/setup-bun@v2 | ||||||
|  |         with: | ||||||
|  |           bun-version-file: 'package.json' | ||||||
|  | 
 | ||||||
|  |       - name: Cache Dependencies | ||||||
|  |         id: dependencies-cache | ||||||
|  |         uses: actions/cache@v4 | ||||||
|  |         with: | ||||||
|  |           path: ~/.bun/install/cache | ||||||
|  |           key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} | ||||||
|  |           restore-keys: | | ||||||
|  |             ${{ runner.os }}-bun- | ||||||
|  | 
 | ||||||
|  |       - name: Install Dependencies | ||||||
|  |         run: bun install | ||||||
|  | 
 | ||||||
|  |       - name: Validate types | ||||||
|  |         run: bun typecheck | ||||||
|  | 
 | ||||||
|  |       - name: Build Dist | ||||||
|  |         run: VITE_CODE_VERSION=$GITHUB_SHA bun dist | ||||||
|  | @ -30,6 +30,9 @@ jobs: | ||||||
|       - name: Install Dependencies |       - name: Install Dependencies | ||||||
|         run: bun install |         run: bun install | ||||||
| 
 | 
 | ||||||
|  |       - name: Validate types | ||||||
|  |         run: bun typecheck | ||||||
|  | 
 | ||||||
|       - name: Build Dist (Staging) |       - name: Build Dist (Staging) | ||||||
|         run: VITE_CODE_VERSION=$GITHUB_SHA bun dist -m staging |         run: VITE_CODE_VERSION=$GITHUB_SHA bun dist -m staging | ||||||
|         if: env.GITHUB_REF_NAME == 'master' |         if: env.GITHUB_REF_NAME == 'master' | ||||||
|  |  | ||||||
|  | @ -9,7 +9,8 @@ | ||||||
|     "dev": "vite --host 0.0.0.0", |     "dev": "vite --host 0.0.0.0", | ||||||
|     "preview": "vite preview", |     "preview": "vite preview", | ||||||
|     "dist": "vite build", |     "dist": "vite build", | ||||||
|     "count-source-lines": "exec scripts/src-lc.sh" |     "count-source-lines": "exec scripts/src-lc.sh", | ||||||
|  |     "typecheck": "tsc --noEmit --skipLibCheck" | ||||||
|   }, |   }, | ||||||
|   "keywords": [], |   "keywords": [], | ||||||
|   "author": "Rubicon", |   "author": "Rubicon", | ||||||
|  |  | ||||||
|  | @ -15,3 +15,24 @@ if (typeof window.crypto.randomUUID === "undefined") { | ||||||
|       ) as `${string}-${string}-${string}-${string}-${string}`; |       ) as `${string}-${string}-${string}-${string}-${string}`; | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if (typeof Promise.withResolvers === "undefined") { | ||||||
|  |   // Chrome/Edge 119, Firefox 121, Safari/iOS 17.4
 | ||||||
|  | 
 | ||||||
|  |   // Promise.withResolvers is generic and works with subclasses - the typescript built-in decl
 | ||||||
|  |   // could not handle the subclassing case.
 | ||||||
|  |   Promise.withResolvers = function <T>(this: AnyPromiseConstructor<T>) { | ||||||
|  |     let resolve!: PromiseWithResolvers<T>["resolve"], reject!: PromiseWithResolvers<T>["reject"]; | ||||||
|  |     // These variables are expected to be set after `new this()`
 | ||||||
|  | 
 | ||||||
|  |     const promise = new this((resolve0, reject0) => { | ||||||
|  |       resolve = resolve0; | ||||||
|  |       reject = reject0; | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |       promise, resolve, reject | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -4,23 +4,27 @@ import { dispatchCall, isJSONRPCCall, type Call } from "./workerrpc"; | ||||||
| 
 | 
 | ||||||
| function isServiceWorker( | function isServiceWorker( | ||||||
|   self: WorkerGlobalScope, |   self: WorkerGlobalScope, | ||||||
|  |   // @ts-ignore: workaround for workbox logger.d.ts decl
 | ||||||
| ): self is ServiceWorkerGlobalScope { | ): self is ServiceWorkerGlobalScope { | ||||||
|   return !!(self as unknown as ServiceWorkerGlobalScope).registration; |   return ( | ||||||
|  |     (self as unknown as Record<string, unknown>)["serviceWorker"] instanceof | ||||||
|  |     ServiceWorker | ||||||
|  |   ); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| if (isServiceWorker(self)) { | if (!isServiceWorker(self)) { | ||||||
|   cleanupOutdatedCaches(); |  | ||||||
|   precacheAndRoute(self.__WB_MANIFEST, { |  | ||||||
|     cleanURLs: false, |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   // auto update
 |  | ||||||
|   self.skipWaiting(); |  | ||||||
|   clientsClaim(); |  | ||||||
| } else { |  | ||||||
|   throw new TypeError("This entry point must be run in a service worker"); |   throw new TypeError("This entry point must be run in a service worker"); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | cleanupOutdatedCaches(); | ||||||
|  | precacheAndRoute(self.__WB_MANIFEST, { | ||||||
|  |   cleanURLs: false, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | // auto update
 | ||||||
|  | self.skipWaiting(); | ||||||
|  | clientsClaim(); | ||||||
|  | 
 | ||||||
| export const Service = { | export const Service = { | ||||||
|   ping() {}, |   ping() {}, | ||||||
| }; | }; | ||||||
|  | @ -29,6 +33,9 @@ self.addEventListener("message", (event: MessageEvent<unknown>) => { | ||||||
|   const payload = event.data; |   const payload = event.data; | ||||||
|   if (typeof payload !== "object") return; |   if (typeof payload !== "object") return; | ||||||
|   if (isJSONRPCCall(payload as Record<string, unknown>)) { |   if (isJSONRPCCall(payload as Record<string, unknown>)) { | ||||||
|     dispatchCall(Service, event as MessageEvent<Call<unknown>>); |     dispatchCall( | ||||||
|  |       Service, | ||||||
|  |       event as MessageEvent<Call<unknown>> & { source: MessageEventSource }, | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| { | { | ||||||
|   "compilerOptions": { |   "compilerOptions": { | ||||||
|     "lib": ["ESNext", "WebWorker"], |     "lib": ["WebWorker", "ESNext"], | ||||||
|   }, |   }, | ||||||
| } |   "extends": ["../../tsconfig.super.json"], | ||||||
|  |   "include": ["./**/*.ts"], | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -46,7 +46,7 @@ export type Result<T, E> = JSONRPC & { id: string | number } & ( | ||||||
| export function isJSONRPCResult( | export function isJSONRPCResult( | ||||||
|   object: Record<string, unknown>, |   object: Record<string, unknown>, | ||||||
| ): object is Result<unknown, unknown> { | ): object is Result<unknown, unknown> { | ||||||
|   return object["jsonrpc"] === "2.0" && object["id"] && !object["method"]; |   return !!(object["jsonrpc"] === "2.0" && object["id"] && !object["method"]); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function isJSONRPCCall( | export function isJSONRPCCall( | ||||||
|  | @ -114,6 +114,9 @@ export class ResultDispatcher { | ||||||
|     { |     { | ||||||
|       const callback = this.map.get(id); |       const callback = this.map.get(id); | ||||||
|       if (!callback) return; |       if (!callback) return; | ||||||
|  |       // Now the callback is not undefined
 | ||||||
|  | 
 | ||||||
|  |       // Fast path
 | ||||||
|       if (typeof callback !== "boolean") { |       if (typeof callback !== "boolean") { | ||||||
|         callback(message); |         callback(message); | ||||||
|         this.map.delete(id); |         this.map.delete(id); | ||||||
|  | @ -121,28 +124,30 @@ export class ResultDispatcher { | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return new Promise<void>((resolve) => { |     const { promise, resolve } = Promise.withResolvers<undefined>(); | ||||||
|       let retried = 0; |  | ||||||
| 
 | 
 | ||||||
|       const checkAndDispatch = () => { |     let retried = 0; | ||||||
|         const callback = this.map.get(id); |  | ||||||
|         if (typeof callback !== "boolean") { |  | ||||||
|           callback(message); |  | ||||||
|           this.map.delete(id); |  | ||||||
|           resolve(); |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|         setTimeout(checkAndDispatch, 0); |  | ||||||
|         if (++retried > 3) { |  | ||||||
|           console.warn( |  | ||||||
|             `retried ${retried} time(s) but the callback is still disappeared, id is "${id}"`, |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|       }; |  | ||||||
| 
 | 
 | ||||||
|       // start the loop
 |     const checkAndDispatch = () => { | ||||||
|       checkAndDispatch(); |       const callback = this.map.get(id); | ||||||
|     }); |       if (typeof callback !== "boolean") { | ||||||
|  |         callback!(message); // the nullability is already checked before
 | ||||||
|  |         this.map.delete(id); | ||||||
|  |         resolve(undefined); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       setTimeout(checkAndDispatch, 0); | ||||||
|  |       if (++retried > 3) { | ||||||
|  |         console.warn( | ||||||
|  |           `retried ${retried} time(s) but the callback is still disappeared, id is "${id}"`, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     // start the loop
 | ||||||
|  |     checkAndDispatch(); | ||||||
|  | 
 | ||||||
|  |     return promise; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   createTypedCall< |   createTypedCall< | ||||||
|  | @ -159,14 +164,16 @@ export class ResultDispatcher { | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type AnyService = Record<string, ((...args: unknown[]) => unknown) | undefined> | type AnyService = Record<string, (...args: unknown[]) => unknown>; | ||||||
| 
 | 
 | ||||||
| export async function dispatchCall< | export async function dispatchCall<S extends Partial<AnyService>>( | ||||||
|   S extends AnyService, |   service: S, | ||||||
| >(service: S, event: MessageEvent<Call<unknown>>) { |   event: MessageEvent<Call<unknown>> & { source: MessageEventSource }, | ||||||
|  | ) { | ||||||
|   try { |   try { | ||||||
|     const fn = service[event.data.method]; |     const fn = service[event.data.method]; | ||||||
|     if (!fn) { |     if (!fn) { | ||||||
|  |       console.warn("requested unknown method", event.data.method, event.data); | ||||||
|       if (event.data.id) |       if (event.data.id) | ||||||
|         return event.source.postMessage({ |         return event.source.postMessage({ | ||||||
|           jsonrpc: "2.0", |           jsonrpc: "2.0", | ||||||
|  | @ -176,10 +183,12 @@ export async function dispatchCall< | ||||||
|             message: "Method not found", |             message: "Method not found", | ||||||
|           }, |           }, | ||||||
|         } as Result<void, void>); |         } as Result<void, void>); | ||||||
|  | 
 | ||||||
|  |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     try { |     try { | ||||||
|       const result = await fn(...event.data.params as unknown[]); |       const result = await fn(...(event.data.params as unknown[])); | ||||||
| 
 | 
 | ||||||
|       if (!event.data.id) return; |       if (!event.data.id) return; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,20 +1,11 @@ | ||||||
| { | { | ||||||
|   "compilerOptions": { |   "compilerOptions": { | ||||||
|     "strict": true, |  | ||||||
|     "target": "ESNext", |  | ||||||
|     "module": "esnext", |  | ||||||
|     "moduleResolution": "node", |  | ||||||
|     "allowSyntheticDefaultImports": true, |  | ||||||
|     "esModuleInterop": true, |  | ||||||
|     "jsx": "preserve", |     "jsx": "preserve", | ||||||
|     "jsxImportSource": "solid-js", |     "jsxImportSource": "solid-js", | ||||||
|     "types": ["vite/client", "vite-plugin-pwa/solid"], |     "types": ["vite/client", "vite-plugin-pwa/solid"], | ||||||
|     "noEmit": true, |     "lib": ["ESNext", "DOM", "DOM.Iterable"] | ||||||
|     "isolatedModules": true, |   }, | ||||||
|     "resolveJsonModule": true, |   "include": ["./src/**/*.ts", "./src/**/*.tsx", "./*.ts", "./types/**.ts"], | ||||||
|     "paths": { |   "exclude": ["./src/serviceworker/**"], | ||||||
|       "~platform/*": ["./src/platform/*"], |   "extends": ["./tsconfig.super.json"] | ||||||
|       "~material/*": ["./src/material/*"] |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										17
									
								
								tsconfig.super.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								tsconfig.super.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | ||||||
|  | { | ||||||
|  |   "compilerOptions": { | ||||||
|  |     "strict": true, | ||||||
|  |     "target": "ESNext", | ||||||
|  |     "module": "esnext", | ||||||
|  |     "moduleResolution": "node", | ||||||
|  |     "allowSyntheticDefaultImports": true, | ||||||
|  |     "esModuleInterop": true, | ||||||
|  |     "noEmit": true, | ||||||
|  |     "isolatedModules": true, | ||||||
|  |     "resolveJsonModule": true, | ||||||
|  |     "paths": { | ||||||
|  |       "~platform/*": ["./src/platform/*"], | ||||||
|  |       "~material/*": ["./src/material/*"] | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										26
									
								
								types/lib.esnext.promise.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								types/lib.esnext.promise.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | ||||||
|  | interface AnyPromiseWithResolvers<T, Instance> { | ||||||
|  |   promise: Instance; | ||||||
|  |   resolve: (value: T | PromiseLike<T>) => void; | ||||||
|  |   reject: (reason?: any) => void; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type AnyPromiseConstructor<T> = new ( | ||||||
|  |   executor: ( | ||||||
|  |     resolve: PromiseWithResolvers<T>["resolve"], | ||||||
|  |     reject: PromiseWithResolvers<T>["reject"], | ||||||
|  |   ) => void, | ||||||
|  | ) => Promise<T>; | ||||||
|  | 
 | ||||||
|  | interface PromiseConstructor { | ||||||
|  |   /** | ||||||
|  |    * Creates a new Promise and returns it in an object, along with its resolve and reject functions. | ||||||
|  |    * @returns An object with the properties `promise`, `resolve`, and `reject`. | ||||||
|  |    * | ||||||
|  |    * ```ts
 | ||||||
|  |    * const { promise, resolve, reject } = Promise.withResolvers<T>(); | ||||||
|  |    * ``` | ||||||
|  |    */ | ||||||
|  |   withResolvers<T, K extends AnyPromiseConstructor<T>>( | ||||||
|  |     this: K, | ||||||
|  |   ): AnyPromiseWithResolvers<T, InstanceType<K>>; | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue