diff --git a/build/build.node.ts b/build/build.node.ts index 97059586fa3..ec44c093bb8 100644 --- a/build/build.node.ts +++ b/build/build.node.ts @@ -1194,8 +1194,11 @@ namespace $ { } } catch( error ) { - if( $mol_promise_like( error ) ) $mol_fail_hidden( error ) - $mol_fail_log( error ) + if ($mol_fail_catch(error)) { + if (! (error as Error).message.match(/code E404/)) { + console.error( error ) + } + } } ++ version[2] diff --git a/build/client/client.js b/build/client/client.js index 22d91b68efe..8067a450afd 100644 --- a/build/client/client.js +++ b/build/client/client.js @@ -1,19 +1,31 @@ -function $mol_build_client() { - - const origin = document.location.origin.replace( /^http/ , 'ws' ) - const path = document.location.pathname - const uri = origin + path +// @ts-check - const socket = new WebSocket( uri ) - - socket.onclose = ()=> setTimeout( ()=> $mol_build_client() , 1000 ) - - socket.onmessage = message => { - if( message.data !== '$mol_build_obsolete' ) return - location.reload() - } +class $mol_build_client { + static closed = false + static run() { + const origin = document.location.origin.replace( /^http/ , 'ws' ) + const path = document.location.pathname + const uri = origin + path + + const socket = new WebSocket( uri ) + + socket.onclose = ()=> { + this.closed = true + setTimeout( ()=> $mol_build_client.run() , 1000 ) + } + socket.onopen = () => { + if (this.closed) location.reload() + this.closed = false + } + + socket.onmessage = message => { + if( message.data !== '$mol_build_obsolete' ) return + location.reload() + } + + } } -$mol_build_client() +$mol_build_client.run() diff --git a/build/server/server.node.ts b/build/server/server.node.ts index 22cd30e70ca..d5dd507365b 100644 --- a/build/server/server.node.ts +++ b/build/server/server.node.ts @@ -278,7 +278,7 @@ namespace $ { ...build.bundleFiles([ bundle.path() , [ 'node' ] ]) ] - for( const src of sources ) src.stat() + for( const src of sources ) src.version() } catch (error) { if ($mol_fail_catch(error)) { this.$.$mol_log3_fail({ @@ -328,9 +328,9 @@ namespace $ { try { - for( const file of build.bundle([ path, 'node.js' ]) ) file.stat() - for( const file of build.bundle([ path, 'node.audit.js' ]) ) file.stat() - for( const file of build.bundle([ path, 'node.test.js' ]) ) file.stat() + for( const file of build.bundle([ path, 'node.js' ]) ) file.version() + for( const file of build.bundle([ path, 'node.audit.js' ]) ) file.version() + for( const file of build.bundle([ path, 'node.test.js' ]) ) file.version() } catch( error: any ) { diff --git a/file/file.node.ts b/file/file.node.ts index a9db2d69b34..ba433be21a6 100644 --- a/file/file.node.ts +++ b/file/file.node.ts @@ -23,20 +23,6 @@ namespace $ { function buffer_normalize(buf: Buffer): Uint8Array { return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength) } - - export enum $mol_file_mode_open { - /** create if it doesn't already exist */ - create = $node.fs.constants.O_CREAT, - /** truncate to zero size if it already exists */ - exists_truncate = $node.fs.constants.O_TRUNC, - /** throw exception if it already exists */ - exists_fail = $node.fs.constants.O_EXCL, - read_only = $node.fs.constants.O_RDONLY, - write_only = $node.fs.constants.O_WRONLY, - read_write = $node.fs.constants.O_RDWR, - /** data will be appended to the end */ - append = $node.fs.constants.O_APPEND, - } export class $mol_file_node extends $mol_file { @@ -111,65 +97,36 @@ namespace $ { protected override drop() { $node.fs.unlinkSync( this.path() ) } - - @ $mol_mem - override buffer( next? : Uint8Array ) { + @ $mol_action + protected override read() { const path = this.path() - if( next === undefined ) { - - if( !this.stat() ) return new Uint8Array - - try { - - const prev = $mol_mem_cached( ()=> this.buffer() ) - - next = buffer_normalize( $node.fs.readFileSync( path ) ) - - if( prev !== undefined && !$mol_compare_array( prev, next ) ) { - this.$.$mol_log3_rise({ - place: `$mol_file_node.buffer()`, - message: 'Changed' , - path: this.relate() , - }) - } - - return next - - } catch( error: any ) { - if (this.$.$mol_fail_catch(error)) { - error.message += '\n' + path - } - return this.$.$mol_fail_hidden( error ) + try { + return buffer_normalize($node.fs.readFileSync( path )) + } catch( error: any ) { + if (this.$.$mol_fail_catch(error)) { + error.message += '\n' + path } - + + return this.$.$mol_fail_hidden( error ) } - - this.parent().exists( true ) - - const now = new Date - this.stat( { - type: 'file', - size: next.length, - atime: now, - mtime: now, - ctime: now, - }, 'virt' ) + } + + @ $mol_action + protected override write(buffer: Uint8Array) { + const path = this.path() try { - $node.fs.writeFileSync( path, next ) + $node.fs.writeFileSync( path, buffer ) } catch( error: any ) { if (this.$.$mol_fail_catch(error)) { error.message += '\n' + path } return this.$.$mol_fail_hidden( error ) - } - - return next } @@ -198,23 +155,25 @@ namespace $ { return $node.path.relative( base.path() , this.path() ).replace( /\\/g , '/' ) } - protected override append( next : Uint8Array | string ) { - const path = this.path() - try { - $node.fs.appendFileSync( path , next ) - } catch( e: any ) { - if (this.$.$mol_fail_catch(e)) { - e.message += '\n' + path - } - return this.$.$mol_fail_hidden(e) - } + @ $mol_mem + override stream_read() { + const { Readable } = $node['node:stream'] as typeof import('stream') + const ctl = new AbortController + const stream = $node.fs.createReadStream(this.path(), { signal: ctl.signal }) + const destructor = () => ctl.abort() + + return Object.assign(Readable.toWeb(stream), { destructor }) as ReadableStream } - - override open( ... modes: readonly ( keyof typeof $mol_file_mode_open )[] ) { - return $node.fs.openSync( - this.path(), - modes.reduce( ( res, mode )=> res | $mol_file_mode_open[ mode ], 0 ), - ) + + @ $mol_mem + override stream_write() { + const { Writable } = $node['node:stream'] as typeof import('stream') + + const ctl = new AbortController + const stream = $node.fs.createWriteStream(this.path(), { signal: ctl.signal }) + const destructor = () => ctl.abort() + + return Object.assign(Writable.toWeb(stream), { destructor }) as WritableStream } } diff --git a/file/file.ts b/file/file.ts index 62507eb0842..dc7b9d1ddb6 100644 --- a/file/file.ts +++ b/file/file.ts @@ -46,15 +46,18 @@ namespace $ { // Лучше ограничить mam-ом const root = this.$.$mol_file.watch_root ?? this if ( this !== root ) { - // Если родитель удалился, надо ресетнуть все дочерние на любой глубине - // Родитель может удалиться, потом создасться, а дочерняя папка только удалиться. - // Поэтому parent.exists() не запустит перевычисления - // parent.version() меняется не только при удалении, будет ложное срабатывание - // события вотчера addDir сбрасывает только parent.sub(), а parent.version() может остаться та же - // тогда дочерний не перзапустится - // Если addDir будет сбрасывать parent.version(), то будет лишний раз перевычислен parent, хоть и он сам не поменялся + /* + Если родитель удалился, надо ресетнуть все дочерние на любой глубине + Родитель может удалиться, потом создасться, а дочерняя папка только удалиться. + Поэтому parent.exists() не запустит перевычисления + + parent.version() меняется не только при удалении, будет ложное срабатывание + события вотчера addDir сбрасывает только parent.sub(), а parent.version() может остаться та же + тогда дочерний не перзапустится + Если addDir будет сбрасывать parent.version(), то будет лишний раз перевычислен parent, хоть и он сам не поменялся + */ + parent.version() - // parent.sub_version() } if( virt ) return next ?? null @@ -103,20 +106,6 @@ namespace $ { } ) } - @ $mol_mem - static flush_counter(reset?: null): number { - return 1 + ( $mol_wire_probe(() => this.flush_counter()) ?? 0 ) - } - - @ $mol_mem - static flusher() { - try { - // this.flush() - } catch (e) { - this.$.$mol_fail_log(e) - } - } - @ $mol_action static flush() { // this.flush_counter() @@ -127,12 +116,22 @@ namespace $ { for (const file of this.added) { const parent = file.parent() - if ($mol_wire_probe(() => parent.sub())) parent.sub(null) - file.reset() - // file.sub_version(null) + + try { + if ( $mol_wire_probe(() => parent.sub())) parent.sub(null) + file.reset() + } catch (error) { + if ($mol_fail_catch(error)) $mol_fail_log(error) + } } - this.changed.forEach(file => file.reset()) + for (const file of this.changed) { + try { + file.reset() + } catch (error) { + if ($mol_fail_catch(error)) $mol_fail_log(error) + } + } this.added.clear() this.changed.clear() @@ -141,25 +140,42 @@ namespace $ { this.watching = true } - @ $mol_mem - protected sub_version(reset?: null): number { - return 1 + ( $mol_wire_probe(() => this.sub_version()) ?? 0 ) - } - protected static watching = true - // @ $mol_action - static watch_off(cb: () => Result, path: string) { + protected static lock = new $mol_lock + + @ $mol_action + protected static watching_off(path: string) { + this.watching = false + /* + watch запаздывает и событие может прилететь через 3 сек после окончания сайд эффекта + поэтому добавляем папку, которую меняет side_effect + Когда дойдет до выполнения flush, он ресетнет ее + + Иначе будут лишние срабатывания + Например, удалили hyoo/board, watch ресетит и exists начинает отдавать false, срабатывает git clone + Сразу после него событие addDir еще не успело прийти, + на следующем перезапуске вызывается git pull, т.к. + с точки зрения реактивной системы hyoo/board еще не существует. + */ + this.changed.add(this.$.$mol_file.absolute(path)) + } + + static watch_off(side_effect: () => Result, affected_dir: string) { + // ждем, пока выполнится предыдущий watch_off + const unlock = () => {} // this.lock.grab() + this.watching_off(affected_dir) + try { - this.watching = false - const result = cb() - // this.flush_counter(null) - // watch запаздывает и событие может прилететь через 3 сек после окончания git pull - this.$.$mol_file.absolute(path).reset() + const result = side_effect() this.flush() + unlock() return result - } catch (e) { - if ( ! $mol_promise_like(e) ) this.flush() + } catch(e) { + if (! $mol_promise_like(e)) { + this.flush() + unlock() + } $mol_fail_hidden(e) } } @@ -180,6 +196,58 @@ namespace $ { protected ensure() {} protected drop() {} protected copy(to: string) {} + protected read() { return new Uint8Array } + protected write(buffer: Uint8Array) { } + protected kids() { + return [] as readonly $mol_file[] + } + stream_read() { return new ReadableStream } + stream_write() { return new WritableStream } + + @ $mol_mem + buffer( next? : Uint8Array ) { + + if( next === undefined ) { + + if( !this.stat() ) return new Uint8Array + + const prev = $mol_mem_cached( ()=> this.buffer() ) + + next = this.read() + + if( prev !== undefined && !$mol_compare_array( prev, next ) ) { + this.$.$mol_log3_rise({ + place: `$mol_file_node.buffer()`, + message: 'Changed' , + path: this.relate() , + }) + } + + return next + + } + + this.parent().exists( true ) + + this.stat( this.stat_make(next.length), 'virt' ) + + this.write(next) + + return next + + } + + @ $mol_action + stat_make(size: number) { + const now = new Date() + return { + type: 'file', + size, + atime: now, + mtime: now, + ctime: now, + } as const + } @ $mol_mem_key clone(to: string) { @@ -253,10 +321,9 @@ namespace $ { return match ? match[ 1 ].substring( 1 ) : '' } - @ $mol_mem - buffer( next? : Uint8Array ) { return next ?? new Uint8Array } - text(next?: string, virt?: 'virt') { + // Если пушим в text, то при сбросе таргета надо перезапускать пуш + // Например файл удалили, потом снова создали, версия поменялась - перезаписываем if (next !== undefined) this.version() return this.text_int(next, virt) } @@ -264,14 +331,7 @@ namespace $ { @ $mol_mem text_int(next?: string, virt?: 'virt') { if( virt ) { - const now = new Date - this.stat( { - type: 'file', - size: 0, - atime: now, - mtime: now, - ctime: now, - }, 'virt' ) + this.stat( this.stat_make(0), 'virt' ) return next! } @@ -295,10 +355,6 @@ namespace $ { return this.kids().filter(file => file.exists()) } - protected kids() { - return [] as readonly $mol_file[] - } - resolve(path: string): $mol_file { throw new Error('implement') } @@ -307,8 +363,6 @@ namespace $ { throw new Error('implement') } - protected append( next : Uint8Array | string ) {} - find( include? : RegExp , exclude? : RegExp @@ -340,15 +394,10 @@ namespace $ { } } - open( ... modes: readonly ( 'create' | 'exists_truncate' | 'exists_fail' | 'read_only' | 'write_only' | 'read_write' | 'append' )[] ) { - return 0 - } - toJSON() { return this.path() } } - $mol_file.flusher() } diff --git a/file/file.web.ts b/file/file.web.ts index fd9d61687ca..ffad921b8f3 100644 --- a/file/file.web.ts +++ b/file/file.web.ts @@ -41,7 +41,6 @@ namespace $ { throw new Error('$mol_file_web.drop() not implemented') } - @ $mol_mem protected override kids() : readonly $mol_file[] { throw new Error('$mol_file_web.sub() not implemented') } @@ -49,10 +48,7 @@ namespace $ { override relate( base = ( this.constructor as typeof $mol_file ).relative( '.' )): string { throw new Error('$mol_file_web.relate() not implemented') } - - override append( next : Uint8Array | string ) { - throw new Error('$mol_file_web.append() not implemented') - } + } $.$mol_file = $mol_file_web diff --git a/lock/lock.ts b/lock/lock.ts new file mode 100644 index 00000000000..ccd4d69a064 --- /dev/null +++ b/lock/lock.ts @@ -0,0 +1,32 @@ +namespace $ { + export class $mol_lock extends $mol_object { + protected promise = null as null | Promise + + async wait() { + let next = () => {} + let destructed = false + const task = $mol_wire_auto() + if (! task) return next + + const destructor = task.destructor.bind(task) + task.destructor = ()=> { + destructor() + destructed = true + next() + } + + let promise + + do { + promise = this.promise + await promise + if (destructed) return next + } while (promise !== this.promise) + + this.promise = new Promise(done => { next = done }) + return next + } + + grab() { return $mol_wire_sync(this).wait() } + } +} diff --git a/run/run.node.test.ts b/run/run.node.test.ts index 0859800947a..850321d2d26 100644 --- a/run/run.node.test.ts +++ b/run/run.node.test.ts @@ -2,19 +2,43 @@ namespace $ { $mol_test( { async 'exec timeout auto kill child process'($) { let close_mock = () => {} + const error_message = 'Run error, timeout' - const context_mock = $.$mol_ambient({ - $mol_run_spawn: () => ({ + function mol_run_spawn_sync_mock() { + return { + output: [], + stdout: error_message, + stderr: '', + status: 0, + signal: null, + pid: 123, + } + } + + function mol_run_spawn_mock() { + return { on(name: string, cb: () => void) { if (name === 'exit') close_mock = cb }, kill() { close_mock() } - } as any) + } as any + } + + const context_mock = $.$mol_ambient({ + $mol_run_spawn_sync: mol_run_spawn_sync_mock, + $mol_run_spawn: mol_run_spawn_mock }) - + + class $mol_run_mock extends $mol_run { + static get $() { return context_mock } + static override async_enabled() { + return true + } + } + let message = '' try { - const res = await $mol_wire_async(context_mock.$mol_run).spawn({ + const res = await $mol_wire_async($mol_run_mock).spawn({ command: 'sleep 10', dir: '.', timeout: 10, @@ -23,7 +47,7 @@ namespace $ { } catch (e) { message= (e as Error).message } - $mol_assert_equal(message, 'Run error, timeout') + $mol_assert_equal(message, error_message) } } ) diff --git a/run/run.node.ts b/run/run.node.ts index f3e494709ee..ebfcc5abc22 100644 --- a/run/run.node.ts +++ b/run/run.node.ts @@ -32,8 +32,12 @@ namespace $ { export class $mol_run extends $mol_object { + static async_enabled() { + return Boolean(this.$.$mol_env()['MOL_RUN_ASYNC']) + } + static spawn(options: $mol_run_options) { - const sync = ! this.$.$mol_env()['MOL_RUN_ASYNC'] || ! Boolean($mol_wire_auto()) + const sync = ! this.async_enabled() || ! Boolean($mol_wire_auto()) const env = options.env ?? this.$.$mol_env() return $mol_wire_sync(this).spawn_async( { ...options, sync, env } )