Setting the file. One moment.
Write Verification State · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page async function publish
— line 100
This file
Number 45.169
Position 169 of 182
Type JavaScript
Size 8 KB
Lines 124 lib/ write-verification-state.js
JavaScript · 124 lines · 8 KB
'./write-verification'
);
7 const file = projectDir => path. join (projectDir, 'state' , 'write-verification.json' );
8 const journalDir = projectDir => path. join (projectDir, 'state' , 'write-verification-journal' );
9 async function load ( projectDir , siteId ) {
10 let state;
11 try { state = JSON . parse ( await fs. readFile ( file (projectDir), 'utf8' )); }
12 catch (e) { if (e.code !== 'ENOENT' ) throw e; state = { schemaVersion: 1 , siteId, sequence: 0 , rows: {} }; }
13 if (state.schemaVersion !== 1 || state.siteId !== siteId || ! state.rows || Array. isArray (state.rows)) throw new Error ( 'Invalid or wrong-site write verification state' );
14 state.sequence ||= 0 ;
15 let names;
16 try { names = await fs. readdir ( journalDir (projectDir)); } catch (e) { if (e.code !== 'ENOENT' ) throw e; names = []; }
17 for ( const name of names. filter ( n => n. endsWith ( '.json' )). sort ()) {
18 const delta = JSON . parse ( await fs. readFile (path. join ( journalDir (projectDir), name), 'utf8' ));
19 if (delta.siteId !== siteId || ! Number. isSafeInteger (delta.sequence) || ! delta.rows) throw new Error ( 'Invalid verification checkpoint' );
20 if (delta.sequence > state.sequence) { Object. assign (state.rows, delta.rows); state.sequence = delta.sequence; }
21 }
22 return state;
23 }
24 async function atomicJson ( dest , value ) {
25 const tmp = `${ dest }.${ crypto . randomUUID () }.tmp` ;
26 await fs. mkdir (path. dirname (dest), { recursive: true });
27 try { await fs. writeFile (tmp, JSON . stringify (value) + ' \n ' , { mode: 0o600 }); await fs. rename (tmp, dest); }
28 finally { await fs. rm (tmp, { force: true }); }
29 }
30 // Each checkpoint writes only changed rows. A crash before rename leaves no visible
31 // partial transaction; a crash after it is replayed before any retry can write.
32 async function checkpoint ( projectDir , state , rows ) {
33 if ( ! rows. length ) return ;
34 const sequence = (state.sequence || 0 ) + 1 ;
35 await atomicJson (path. join ( journalDir (projectDir), `${ String ( sequence ). padStart ( 16 , '0' ) }.json` ),
36 { siteId: state.siteId, sequence, rows: Object. fromEntries (rows. map ( r => [r.sourceKey, state.rows[r.sourceKey]])) });
37 state.sequence = sequence;
38 }
39 async function persist ( projectDir , state ) {
40 await atomicJson ( file (projectDir), state);
41 // Snapshot includes the sequence. Old journals are harmless if cleanup is interrupted.
42 let names;
43 try { names = await fs. readdir ( journalDir (projectDir)); } catch (e) { if (e.code !== 'ENOENT' ) throw e; return ; }
44 for ( const name of names) if ( / ^ \d + \. json $ / . test (name) && Number (name. slice ( 0 , - 5 )) <= state.sequence) await fs. rm (path. join ( journalDir (projectDir), name), { force: true });
45 }
46 function reference ( receipt ) {
47 return { storage: 'write-verification' , schemaVersion: 1 , siteId: receipt.siteId, sourceKey: receipt.sourceKey,
48 targetId: receipt.targetId, kind: receipt.kind, receiptDigest: verification. digest (receipt) };
49 }
50 async function validateReference ( projectDir , row ) {
51 const ref = row.verification;
52 if (ref?.storage !== 'write-verification' ) return verification. validReceipt (ref, { sourceKey: row.sourceStableKey, targetId: row.targetId });
53 const state = await load (projectDir, ref.siteId);
54 const candidate = state.rows[row.sourceStableKey];
55 if (candidate?.identityConflict) return false ;
56 const receipt = [candidate?.receipt, candidate?.lastVerifiedReceipt]. find ( r => r && ref.receiptDigest === verification. digest (r));
57 return Boolean (receipt) && ref.receiptDigest === verification. digest (receipt) && verification. validReceipt (receipt,
58 { siteId: ref.siteId, sourceKey: row.sourceStableKey, targetId: row.targetId, kind: ref.kind });
59 }
60 // The full verified receipts behind crosswalk rows, by source key. A row holding a digest reference
61 // resolves it from the protected state (as validateReference does); a revoked, missing, wrong-site
62 // or invalid receipt is simply absent. One state load per site for the whole set.
63 const UNREADABLE = Symbol ( 'unreadable-verification-state' );
64 async function verifiedReceipts ( projectDir , rows ) {
65 const states = new Map (), out = new Map ();
66 for ( const row of rows) {
67 const ref = row.verification;
68 let receipt = ref;
69 // Protected state is authoritative for revocation, whether the row holds a reference or the
70 // receipt inline. Missing state reads as empty (load); state that cannot be read or validated
71 // can never vouch that a receipt was not revoked, so it admits none for that site.
72 const siteId = ref?.siteId;
73 if ( typeof siteId === 'string' && siteId && ! states. has (siteId)) states. set (siteId, await load (projectDir, siteId). catch (() => UNREADABLE ));
74 const state = states. get (siteId), candidate = state === UNREADABLE ? null : state?.rows[row.sourceStableKey];
75 if (state === UNREADABLE || ! state || candidate?.identityConflict) receipt = null ;
76 else if (ref?.storage === 'write-verification' ) {
77 receipt = [candidate?.receipt, candidate?.lastVerifiedReceipt]. find ( r => r && ref.receiptDigest === verification. digest (r));
78 if (receipt && (receipt.siteId !== ref.siteId || receipt.kind !== ref.kind)) receipt = null ;
79 }
80 if (receipt && verification. validReceipt (receipt, { sourceKey: row.sourceStableKey, targetId: row.targetId })) out. set (row.sourceStableKey, receipt);
81 }
82 return out;
83 }
84 function publication ( row , receipt , { baseline = null , confirmedCreate = false , sourceVersion , sourceVersionField , sourceHash , sourceHashDefinition } = {}) {
85 if ( ! verification. validReceipt (receipt, { sourceKey: row.sourceStableKey, targetId: row.targetId })) throw new Error ( 'Cannot publish an unverified mapping' );
86 // A read-only reconciliation must preserve the last confirmed write's version pair.
87 // In particular, never pair a refreshed source version with a newer read-only revision.
88 let published = { ... (baseline || {}), ... row, verification: reference (receipt) };
89 if (confirmedCreate && ! baseline) {
90 const next = local. nextCrosswalkBaseline ({ existingRow: published, outcome: 'created' ,
91 sourceVersion, sourceVersionField, sourceHash, sourceHashDefinition, targetRevision: receipt.actual?.revision });
92 if (next.advanced) published = next.row;
93 }
94 return published;
95 }
96 async function publishMany ( projectDir , entries , revoke = []) {
97 const rows = entries. map (({ row , receipt , options }) => publication (row, receipt, options));
98 await local. updateCrosswalkRows (projectDir, rows, { remove: revoke });
99 }
100 async function publish ( projectDir , row , receipt , options ) {
101 await publishMany (projectDir, [{ row, receipt, options }]);
102 }
103 function report ( rows , siteId ) {
104 const targetIds = new Set ();
105 const checked = rows. map ( row => {
106 const valid = verification. validReceipt (row.receipt, { siteId, sourceKey: row.sourceKey, targetId: row.targetId });
107 const duplicate = valid && targetIds. has (row.targetId);
108 if (valid) targetIds. add (row.targetId);
109 return { ... row, verified: valid && ! duplicate };
110 });
111 // All participants in a collision fail, not only the last one.
112 for ( const row of checked) if (checked. filter ( r => r.targetId && r.targetId === row.targetId). length > 1 ) row.verified = false ;
113 return { siteId, intended: rows. length , rows: checked, verified: checked. filter ( r => r.verified). length ,
114 status: ! rows. length ? 'no-data' : checked. every ( r => r.verified) ? 'complete' : 'partial' };
115 }
116 // A crosswalk row's sourceId is the raw source id: dependency readiness matches it against the
117 // extract's id, so the stable key ("<entity>:<id>") would leave every dependant deferred as
118 // "partial". A caller that omits it gets it from the key. Entity names carry no colon; ids may.
119 function crosswalkSourceId ( row ) {
120 if (row.sourceId != null && row.sourceId !== '' ) return String (row.sourceId);
121 const key = String (row.sourceKey), at = key. indexOf ( ':' );
122 return at > 0 ? key. slice (at + 1 ) : key;
123 }
124 module . exports = { crosswalkSourceId, load, checkpoint, persist, publish, publishMany, report, validateReference, verifiedReceipts };