1- import { detectOpfsSupport } from './opfs.js' ;
1+ import { clearOpfsStorage , detectOpfsSupport } from './opfs.js' ;
22import type { Operation , ReplicaId } from '@treecrdt/interface' ;
33import {
44 createTreecrdtSqliteWriter ,
@@ -60,6 +60,7 @@ export type TreecrdtClient = TreecrdtEngine & {
6060 meta : TreecrdtMetaApi ;
6161 local : TreecrdtLocalApi ;
6262 close : ( ) => Promise < void > ;
63+ drop : ( ) => Promise < void > ;
6364} ;
6465
6566export type ClientOptions = {
@@ -141,8 +142,12 @@ async function createWorkerClient(opts: {
141142 { resolve : ( value : any ) => void ; reject : ( err : Error ) => void }
142143 > ( ) ;
143144 let terminalError : Error | null = null ;
145+ let closed = false ;
146+
147+ const closedError = new Error ( CLIENT_CLOSED_ERROR ) ;
144148
145149 const call = < M extends RpcMethod > ( method : M , params : RpcParams < M > ) : Promise < RpcResult < M > > => {
150+ if ( closed ) return Promise . reject ( closedError ) ;
146151 const id = nextId ++ ;
147152 if ( terminalError ) return Promise . reject ( terminalError ) ;
148153 return new Promise ( ( resolve , reject ) => {
@@ -175,27 +180,44 @@ async function createWorkerClient(opts: {
175180 opts . docId ,
176181 ] ) ) as { storage ?: StorageMode ; opfsError ?: string } | undefined ;
177182 const effectiveStorage : StorageMode = initResult ?. storage === 'opfs' ? 'opfs' : 'memory' ;
183+ const cleanup = ( ) => {
184+ closed = true ;
185+ for ( const { reject } of pending . values ( ) ) reject ( closedError ) ;
186+ pending . clear ( ) ;
187+ worker . removeEventListener ( 'error' , onError ) ;
188+ worker . removeEventListener ( 'message' , onMessage ) ;
189+ worker . terminate ( ) ;
190+ } ;
191+
178192 if ( opts . requireOpfs && effectiveStorage !== 'opfs' ) {
179193 const reason = initResult ?. opfsError ? `: ${ initResult . opfsError } ` : '' ;
180194 try {
181195 if ( ! terminalError ) await call ( 'close' , [ ] as RpcParams < 'close' > ) ;
182196 } catch {
183197 // ignore close errors on init failure
184198 } finally {
185- worker . removeEventListener ( 'error' , onError ) ;
186- worker . removeEventListener ( 'message' , onMessage ) ;
187- worker . terminate ( ) ;
199+ cleanup ( ) ;
188200 }
189201 throw new Error ( `OPFS requested but could not be initialized${ reason } ` ) ;
190202 }
191203
192204 const closeImpl = async ( ) => {
205+ if ( closed ) return ;
193206 try {
194207 if ( ! terminalError ) await call ( 'close' , [ ] as RpcParams < 'close' > ) ;
208+ cleanup ( ) ;
209+ } finally {
210+ // noop: cleanup already handles terminal teardown, and repeated close is idempotent
211+ }
212+ } ;
213+
214+ const dropImpl = async ( ) => {
215+ if ( closed ) return ;
216+ try {
217+ if ( ! terminalError ) await call ( 'drop' , [ ] as RpcParams < 'drop' > ) ;
218+ cleanup ( ) ;
195219 } finally {
196- worker . removeEventListener ( 'error' , onError ) ;
197- worker . removeEventListener ( 'message' , onMessage ) ;
198- worker . terminate ( ) ;
220+ // noop: cleanup already handles terminal teardown, and repeated drop is idempotent
199221 }
200222 } ;
201223
@@ -205,6 +227,7 @@ async function createWorkerClient(opts: {
205227 docId : opts . docId ,
206228 call,
207229 close : closeImpl ,
230+ drop : dropImpl ,
208231 } ) ;
209232}
210233
@@ -253,8 +276,11 @@ async function createDirectClient(opts: {
253276 message : err instanceof Error ? err . message : String ( err ) ,
254277 } ) ,
255278 ) ;
279+ let closed = false ;
280+ const closedError = new Error ( CLIENT_CLOSED_ERROR ) ;
256281
257282 const call : RpcCall = async ( method , params ) => {
283+ if ( closed ) throw closedError ;
258284 try {
259285 switch ( method ) {
260286 case 'sqlExec' : {
@@ -357,9 +383,17 @@ async function createDirectClient(opts: {
357383 const [ replica , node , payload ] = params as RpcParams < 'localPayload' > ;
358384 return ( await localWriterFor ( Uint8Array . from ( replica ) ) . payload ( node , payload ) ) as any ;
359385 }
360- case 'close' :
386+ case 'close' : {
387+ if ( db . close ) await db . close ( ) ;
388+ return undefined as any ;
389+ }
390+ case 'drop' : {
361391 if ( db . close ) await db . close ( ) ;
392+ if ( finalStorage === 'opfs' ) {
393+ await clearOpfsStorage ( filename ) ;
394+ }
362395 return undefined as any ;
396+ }
363397 default :
364398 throw new Error ( `unsupported direct method: ${ method } ` ) ;
365399 }
@@ -374,7 +408,17 @@ async function createDirectClient(opts: {
374408 docId : opts . docId ,
375409 call,
376410 close : async ( ) => {
411+ if ( closed ) return ;
412+ if ( db . close ) await db . close ( ) ;
413+ closed = true ;
414+ } ,
415+ drop : async ( ) => {
416+ if ( closed ) return ;
377417 if ( db . close ) await db . close ( ) ;
418+ if ( finalStorage === 'opfs' ) {
419+ await clearOpfsStorage ( filename ) ;
420+ }
421+ closed = true ;
378422 } ,
379423 } ) ;
380424}
@@ -387,6 +431,7 @@ function makeTreecrdtClientFromCall(opts: {
387431 docId : string ;
388432 call : RpcCall ;
389433 close : ( ) => Promise < void > ;
434+ drop : ( ) => Promise < void > ;
390435} ) : TreecrdtClient {
391436 const call = opts . call ;
392437 let closePromise : Promise < void > | null = null ;
@@ -510,6 +555,7 @@ function makeTreecrdtClientFromCall(opts: {
510555 payload : localPayloadImpl ,
511556 } ,
512557 close : closeImpl ,
558+ drop : opts . drop ,
513559 } ;
514560}
515561
0 commit comments