tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

io-terminal.js (3211B)


      1 import { OutputBuffer } from './io-output.js';
      2 export class Terminal {
      3     run;
      4     output = new OutputBuffer();
      5     nextAfter = 0;
      6     reader;
      7     buffered = [];
      8     eof = false;
      9     ended = false;
     10     constructor(run) {
     11         this.run = run;
     12         if (!/^[1-9][0-9]*$/.test(run) || run.length > 20 || BigInt(run) > 0xffffffffffffffffn)
     13             throw new Error('invalid terminal run');
     14     }
     15     get waiting() {
     16         return this.reader ? { run: this.run, id: this.reader.id } : undefined;
     17     }
     18     read() {
     19         const reader = this.reader;
     20         if (!reader || !this.buffered.length && !this.eof)
     21             return;
     22         const bytes = this.buffered.splice(0, reader.limit);
     23         this.reader = undefined;
     24         return { run: this.run, id: reader.id, kind: 'read', bytes, eof: bytes.length === 0 };
     25     }
     26     handle(event) {
     27         if (this.ended || event.run !== this.run || !Number.isSafeInteger(event.id) || event.id < 1)
     28             throw new Error('invalid terminal event');
     29         if (event.kind === 'cancel') {
     30             if (event.id > this.nextAfter || !['read', 'write'].includes(event.operation))
     31                 throw new Error('invalid terminal cancellation');
     32             // Completed synchronous writes/reads already have a normal acknowledgement
     33             // queued. A racing abort does not erase it or manufacture a second reply.
     34             if (this.reader?.id !== event.id)
     35                 return;
     36             if (event.operation !== 'read')
     37                 throw new Error('invalid terminal cancellation');
     38             this.reader = undefined;
     39             return { run: this.run, id: event.id, kind: 'cancelled' };
     40         }
     41         if (event.id <= this.nextAfter)
     42             throw new Error('stale terminal event');
     43         if (event.kind === 'read') {
     44             if (this.reader || !Number.isInteger(event.limit) || event.limit < 1 || event.limit > 4096)
     45                 throw new Error('invalid terminal read');
     46             this.reader = { ...event };
     47         }
     48         else if (event.kind === 'write')
     49             this.output.append(event.stream, event.bytes);
     50         else
     51             throw new Error('invalid terminal operation');
     52         this.nextAfter = event.id;
     53         return event.kind === 'read' ? this.read() : { run: this.run, id: event.id, kind: 'write', count: event.bytes.length };
     54     }
     55     // Explicit one-line UI: no CR/LF in the submitted text, append ONE LF; an
     56     // empty line is data, never EOF. Scalars, not UTF-16 code units, are bounded.
     57     line(run, id, value) {
     58         if (run !== this.run || this.reader?.id !== id || typeof value !== 'string' || value.length > 512 ||
     59             /[\r\n\uD800-\uDFFF]/u.test(value) || Array.from(value).length > 256)
     60             return;
     61         this.buffered = [...new TextEncoder().encode(value), 10];
     62         return this.read();
     63     }
     64     endInput(run, id) {
     65         if (run !== this.run || this.reader?.id !== id)
     66             return;
     67         this.eof = true;
     68         return this.read();
     69     }
     70     finish() {
     71         if (this.reader)
     72             throw new Error('terminal input still pending');
     73         this.ended = true;
     74         this.buffered = [];
     75         this.output.finish();
     76     }
     77 }