3434 * Every row drives BOTH faces through the real stack — the real
3535 * `SecurityPlugin`, a real `ObjectQL` over a real SQL driver (better-sqlite3,
3636 * one in-memory database per rig) — and asserts enforcement's own outcome as
37- * well, so a row cannot go green by both faces drifting together. The rows are
37+ * well, so a row cannot go green by both faces drifting together. A row that
38+ * can write boots its own rig; the principal rows, which only read, share one
39+ * rig per posture source ({@link withPrincipal}). The rows are
3840 * the explain-versus-enforce family's shapes so far: the record-grained write
3941 * verdict's inputs (#19963), the record-grained read verdict's read depth
4042 * (#19986), a shared dependency that throws (#20002), the record matcher under
7577 * built `dist/` here, as this package's other suites read them.
7678 */
7779
78- import { describe , it , expect , vi } from 'vitest' ;
80+ import { describe , it , expect , vi , afterAll } from 'vitest' ;
81+ import { AsyncLocalStorage } from 'node:async_hooks' ;
7982import { ObjectQL } from '@objectstack/objectql' ;
8083import { SqlDriver } from '@objectstack/driver-sql' ;
8184import { resolveAuthzContext , assembleExecutionContext } from '@objectstack/core' ;
@@ -95,6 +98,22 @@ import { defaultPermissionSets } from './objects/default-permission-sets.js';
9598const SYS = { context : { isSystem : true } } as never ;
9699const MEMBER_DEFAULT = defaultPermissionSets . find ( ( p ) => p . name === 'member_default' ) ! ;
97100
101+ /**
102+ * Releases the `AsyncLocalStorage` instances a torn-down rig's engine and
103+ * plugin hold. On Node 22 a storage runs on `async_hooks`: once it has served a
104+ * `run()` it stays on a process-wide list that EVERY later promise creation
105+ * walks, until `disable()`. `ObjectQL` holds three and `SecurityPlugin` one,
106+ * and neither `destroy()` disables them, so each rig this table tore down left
107+ * its storages behind and every later row paid to walk all of them. Called
108+ * after the rig's last row is judged, on objects nothing uses again, so it
109+ * changes nothing any row observes.
110+ */
111+ const releaseAsyncContexts = ( ...owners : object [ ] ) : void => {
112+ for ( const owner of owners ) {
113+ for ( const value of Object . values ( owner ) ) if ( value instanceof AsyncLocalStorage ) value . disable ( ) ;
114+ }
115+ } ;
116+
98117// ── the two faces ─────────────────────────────────────────────────────────
99118
100119/**
@@ -322,7 +341,10 @@ async function bootRls(predicate: string, opts: { grantCrud?: boolean } = {}) {
322341 readAs : async ( filter : unknown ) =>
323342 ( ( await engine . find ( OBJ , { ...( filter ? { where : filter } : { } ) , context : { isSystem : true } } as never ) ) as Array < { id : string } > )
324343 . map ( ( r ) => String ( r . id ) ) . sort ( ) ,
325- teardown : async ( ) => { try { await engine . destroy ( ) ; } catch { /* noop */ } } ,
344+ teardown : async ( ) => {
345+ try { await engine . destroy ( ) ; } catch { /* noop */ }
346+ releaseAsyncContexts ( engine , plugin ) ;
347+ } ,
326348 } ;
327349}
328350
@@ -519,7 +541,10 @@ async function bootPrincipal(source: PostureSource) {
519541 ( ( await engine . find ( objects [ object ] , { ...( filter ? { where : filter } : { } ) , context : { isSystem : true } } as never ) ) as Array < { id : string } > )
520542 . map ( ( r ) => String ( r . id ) ) . sort ( ) ,
521543 tenantOf : async ( userId : string ) => ( await enforced ( userId ) ) . tenantId ,
522- teardown : async ( ) => { try { await engine . destroy ( ) ; } catch { /* noop */ } } ,
544+ teardown : async ( ) => {
545+ try { await engine . destroy ( ) ; } catch { /* noop */ }
546+ releaseAsyncContexts ( engine , plugin ) ;
547+ } ,
523548 } ;
524549}
525550
@@ -622,7 +647,10 @@ async function bootSharing(opts: { faultReadFilter?: boolean } = {}) {
622647 find : ( caller : object , id : string ) => rowsOf ( engine . find ( LINE , { where : { id } , context : { ...caller } } as never ) ) ,
623648 update : ( caller : object , id : string ) =>
624649 landed ( engine . update ( LINE , { name : 'renamed' } , { where : { id } , context : { ...caller } } as never ) ) ,
625- teardown : async ( ) => { try { await engine . destroy ( ) ; } catch { /* noop */ } } ,
650+ teardown : async ( ) => {
651+ try { await engine . destroy ( ) ; } catch { /* noop */ }
652+ releaseAsyncContexts ( engine , plugin ) ;
653+ } ,
626654 } ;
627655}
628656
@@ -650,7 +678,11 @@ interface Row {
650678 run : RowRun ;
651679}
652680
653- /** A row's run: boots its own rig, and hands back the teardown the table calls once the row is judged. */
681+ /**
682+ * A row's run: boots its own rig, or takes its posture source's shared one (a
683+ * principal row), and hands back the teardown the table calls once the row is
684+ * judged: the rig's own, or none for the shared rig, torn down after the table.
685+ */
654686type RowRun = ( ) => Promise < RowFaces & { teardown : ( ) => Promise < void > } > ;
655687interface RowFaces {
656688 explain : Explained ;
@@ -671,9 +703,31 @@ const withSharing = (
671703 const rig = await bootSharing ( opts ) ;
672704 try { return { ...( await f ( rig ) ) , teardown : rig . teardown } ; } catch ( e ) { await rig . teardown ( ) ; throw e ; }
673705} ;
706+ /**
707+ * One principal rig per posture source, booted by the source's first row and
708+ * shared by the rest. Every principal row only READS: explain, the explained
709+ * user's own find, the sets enforcement resolves, and a system read of an
710+ * explained filter. None of them writes a row, a grant or a policy, so a row
711+ * reads the state the rig was booted with, as if the rig were its own. A row
712+ * that can write (the RLS and sharing rigs) still boots its own. Booting all 60
713+ * principal rows' rigs was most of this file's wall.
714+ */
715+ const principalRigs = new Map < string , Promise < PrincipalRig > > ( ) ;
716+ const sharedPrincipalRig = ( source : PostureSource ) : Promise < PrincipalRig > => {
717+ const key = JSON . stringify ( source ) ;
718+ let rig = principalRigs . get ( key ) ;
719+ if ( ! rig ) {
720+ rig = bootPrincipal ( source ) ;
721+ principalRigs . set ( key , rig ) ;
722+ }
723+ return rig ;
724+ } ;
725+ afterAll ( async ( ) => {
726+ for ( const rig of principalRigs . values ( ) ) await ( await rig . catch ( ( ) => undefined ) ) ?. teardown ( ) ;
727+ } ) ;
674728const withPrincipal = ( source : PostureSource , f : ( rig : PrincipalRig ) => Promise < RowFaces > ) : RowRun => async ( ) => {
675- const rig = await bootPrincipal ( source ) ;
676- try { return { ...( await f ( rig ) ) , teardown : rig . teardown } ; } catch ( e ) { await rig . teardown ( ) ; throw e ; }
729+ const rig = await sharedPrincipalRig ( source ) ;
730+ return { ...( await f ( rig ) ) , teardown : async ( ) => undefined } ;
677731} ;
678732
679733const REFUSED_INVALID : Enforced = { kind : 'refused' , ...INVALID } ;
0 commit comments