@fluado/enmetu 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +209 -2
- package/dist/adapters/clock/systemClockAdapter.d.ts +6 -0
- package/dist/adapters/clock/systemClockAdapter.js +12 -0
- package/dist/adapters/fs/nodeFileSystemAdapter.d.ts +18 -0
- package/dist/adapters/fs/nodeFileSystemAdapter.js +230 -0
- package/dist/adapters/git/gitFacade.d.ts +148 -0
- package/dist/adapters/git/gitFacade.js +822 -0
- package/dist/adapters/git/gitLocatorFacade.d.ts +8 -0
- package/dist/adapters/git/gitLocatorFacade.js +75 -0
- package/dist/adapters/git/gitProcessAdapter.d.ts +46 -0
- package/dist/adapters/git/gitProcessAdapter.js +261 -0
- package/dist/adapters/log/nullLogger.d.ts +9 -0
- package/dist/adapters/log/nullLogger.js +10 -0
- package/dist/adapters/log/stderrLoggerAdapter.d.ts +10 -0
- package/dist/adapters/log/stderrLoggerAdapter.js +13 -0
- package/dist/adapters/process/nodeProcessAdapter.d.ts +9 -0
- package/dist/adapters/process/nodeProcessAdapter.js +56 -0
- package/dist/adapters/watch/fsWatchAdapter.d.ts +24 -0
- package/dist/adapters/watch/fsWatchAdapter.js +47 -0
- package/dist/cli/commands/hookCommand.d.ts +29 -0
- package/dist/cli/commands/hookCommand.js +92 -0
- package/dist/cli/commands/resumeCommand.d.ts +48 -0
- package/dist/cli/commands/resumeCommand.js +124 -0
- package/dist/cli/commands/runCommand.d.ts +34 -0
- package/dist/cli/commands/runCommand.js +79 -0
- package/dist/cli/commands/statusCommand.d.ts +71 -0
- package/dist/cli/commands/statusCommand.js +249 -0
- package/dist/cli/exitCodes.d.ts +7 -0
- package/dist/cli/exitCodes.js +38 -0
- package/dist/cli/helpText.d.ts +1 -0
- package/dist/cli/helpText.js +16 -0
- package/dist/cli/main.d.ts +8 -0
- package/dist/cli/main.js +49 -0
- package/dist/cli/parseArgs.d.ts +19 -0
- package/dist/cli/parseArgs.js +106 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +32 -0
- package/dist/commit/commitLocalChangesController.d.ts +132 -0
- package/dist/commit/commitLocalChangesController.js +357 -0
- package/dist/commit/gitIndexLock.d.ts +36 -0
- package/dist/commit/gitIndexLock.js +90 -0
- package/dist/commit/repairStaleIndexController.d.ts +90 -0
- package/dist/commit/repairStaleIndexController.js +150 -0
- package/dist/commit/stageGuardedChanges.d.ts +30 -0
- package/dist/commit/stageGuardedChanges.js +52 -0
- package/dist/contracts/IClock.d.ts +7 -0
- package/dist/contracts/IClock.js +1 -0
- package/dist/contracts/IDaemon.d.ts +26 -0
- package/dist/contracts/IDaemon.js +1 -0
- package/dist/contracts/IFileSystem.d.ts +50 -0
- package/dist/contracts/IFileSystem.js +1 -0
- package/dist/contracts/IGitCommitter.d.ts +60 -0
- package/dist/contracts/IGitCommitter.js +1 -0
- package/dist/contracts/IGitConfigReader.d.ts +5 -0
- package/dist/contracts/IGitConfigReader.js +1 -0
- package/dist/contracts/IGitIndexLock.d.ts +44 -0
- package/dist/contracts/IGitIndexLock.js +1 -0
- package/dist/contracts/IGitReader.d.ts +81 -0
- package/dist/contracts/IGitReader.js +1 -0
- package/dist/contracts/IGitRunner.d.ts +49 -0
- package/dist/contracts/IGitRunner.js +1 -0
- package/dist/contracts/IGitWriter.d.ts +39 -0
- package/dist/contracts/IGitWriter.js +1 -0
- package/dist/contracts/IHaltStore.d.ts +14 -0
- package/dist/contracts/IHaltStore.js +1 -0
- package/dist/contracts/IHookInstaller.d.ts +14 -0
- package/dist/contracts/IHookInstaller.js +1 -0
- package/dist/contracts/IInstanceLock.d.ts +26 -0
- package/dist/contracts/IInstanceLock.js +1 -0
- package/dist/contracts/ILogger.d.ts +11 -0
- package/dist/contracts/ILogger.js +1 -0
- package/dist/contracts/IOperationJournal.d.ts +12 -0
- package/dist/contracts/IOperationJournal.js +1 -0
- package/dist/contracts/IPathQuarantine.d.ts +67 -0
- package/dist/contracts/IPathQuarantine.js +1 -0
- package/dist/contracts/IProcessHost.d.ts +11 -0
- package/dist/contracts/IProcessHost.js +1 -0
- package/dist/contracts/IRepositoryLocator.d.ts +32 -0
- package/dist/contracts/IRepositoryLocator.js +1 -0
- package/dist/contracts/IStateRecorder.d.ts +68 -0
- package/dist/contracts/IStateRecorder.js +1 -0
- package/dist/contracts/IStateStore.d.ts +88 -0
- package/dist/contracts/IStateStore.js +1 -0
- package/dist/contracts/ISyncCycle.d.ts +23 -0
- package/dist/contracts/ISyncCycle.js +1 -0
- package/dist/contracts/ISyncCycleRunner.d.ts +4 -0
- package/dist/contracts/ISyncCycleRunner.js +1 -0
- package/dist/contracts/ISyncScheduler.d.ts +6 -0
- package/dist/contracts/ISyncScheduler.js +1 -0
- package/dist/contracts/ITextSink.d.ts +3 -0
- package/dist/contracts/ITextSink.js +1 -0
- package/dist/contracts/IWatcher.d.ts +9 -0
- package/dist/contracts/IWatcher.js +1 -0
- package/dist/contracts/config.d.ts +26 -0
- package/dist/contracts/config.js +1 -0
- package/dist/contracts/errors.d.ts +39 -0
- package/dist/contracts/errors.js +64 -0
- package/dist/contracts/exitCode.d.ts +10 -0
- package/dist/contracts/exitCode.js +17 -0
- package/dist/contracts/gitTypes.d.ts +113 -0
- package/dist/contracts/gitTypes.js +1 -0
- package/dist/contracts/library.d.ts +35 -0
- package/dist/contracts/library.js +1 -0
- package/dist/contracts/logCodes.d.ts +1 -0
- package/dist/contracts/logCodes.js +1 -0
- package/dist/contracts/runtime.d.ts +16 -0
- package/dist/contracts/runtime.js +1 -0
- package/dist/contracts/syncTypes.d.ts +75 -0
- package/dist/contracts/syncTypes.js +6 -0
- package/dist/core/commit/commitMemory.d.ts +3 -0
- package/dist/core/commit/commitMemory.js +9 -0
- package/dist/core/commit/commitMessage.d.ts +6 -0
- package/dist/core/commit/commitMessage.js +12 -0
- package/dist/core/config/parseConfig.d.ts +8 -0
- package/dist/core/config/parseConfig.js +176 -0
- package/dist/core/content/contentRules.d.ts +11 -0
- package/dist/core/content/contentRules.js +50 -0
- package/dist/core/errors/errorMessage.d.ts +6 -0
- package/dist/core/errors/errorMessage.js +14 -0
- package/dist/core/errors/withCycleId.d.ts +7 -0
- package/dist/core/errors/withCycleId.js +38 -0
- package/dist/core/git/enmetuRefs.d.ts +9 -0
- package/dist/core/git/enmetuRefs.js +22 -0
- package/dist/core/git/porcelain.d.ts +52 -0
- package/dist/core/git/porcelain.js +219 -0
- package/dist/core/git/rebaseOwnership.d.ts +28 -0
- package/dist/core/git/rebaseOwnership.js +29 -0
- package/dist/core/git/stoppedRebaseWrites.d.ts +10 -0
- package/dist/core/git/stoppedRebaseWrites.js +22 -0
- package/dist/core/halt/resumeHint.d.ts +6 -0
- package/dist/core/halt/resumeHint.js +9 -0
- package/dist/core/log/formatLogLine.d.ts +3 -0
- package/dist/core/log/formatLogLine.js +11 -0
- package/dist/core/quarantine/keptBlobMessage.d.ts +5 -0
- package/dist/core/quarantine/keptBlobMessage.js +8 -0
- package/dist/core/quarantine/restoreWarnings.d.ts +16 -0
- package/dist/core/quarantine/restoreWarnings.js +36 -0
- package/dist/core/schedule/schedulerStep.d.ts +63 -0
- package/dist/core/schedule/schedulerStep.js +144 -0
- package/dist/core/state/stateGuards.d.ts +21 -0
- package/dist/core/state/stateGuards.js +295 -0
- package/dist/core/state/stateLiveness.d.ts +10 -0
- package/dist/core/state/stateLiveness.js +21 -0
- package/dist/core/state/stateRecordStep.d.ts +28 -0
- package/dist/core/state/stateRecordStep.js +203 -0
- package/dist/core/status/conflictGuide.d.ts +30 -0
- package/dist/core/status/conflictGuide.js +86 -0
- package/dist/core/status/orphanedCommits.d.ts +15 -0
- package/dist/core/status/orphanedCommits.js +24 -0
- package/dist/core/status/statusHeadline.d.ts +46 -0
- package/dist/core/status/statusHeadline.js +130 -0
- package/dist/core/watch/watchFilter.d.ts +2 -0
- package/dist/core/watch/watchFilter.js +34 -0
- package/dist/daemon/daemon.d.ts +55 -0
- package/dist/daemon/daemon.js +277 -0
- package/dist/enmetuInstances.d.ts +65 -0
- package/dist/enmetuInstances.js +332 -0
- package/dist/halt/haltGuardGitWriter.d.ts +23 -0
- package/dist/halt/haltGuardGitWriter.js +61 -0
- package/dist/halt/haltStore.d.ts +13 -0
- package/dist/halt/haltStore.js +49 -0
- package/dist/halt/recordHalt.d.ts +19 -0
- package/dist/halt/recordHalt.js +20 -0
- package/dist/hooks/hookScript.d.ts +22 -0
- package/dist/hooks/hookScript.js +52 -0
- package/dist/hooks/nullHookInstaller.d.ts +5 -0
- package/dist/hooks/nullHookInstaller.js +6 -0
- package/dist/hooks/preCommitCheckController.d.ts +23 -0
- package/dist/hooks/preCommitCheckController.js +22 -0
- package/dist/hooks/preCommitHookInstaller.d.ts +32 -0
- package/dist/hooks/preCommitHookInstaller.js +123 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +38 -0
- package/dist/library/enmetuLibrary.d.ts +16 -0
- package/dist/library/enmetuLibrary.js +62 -0
- package/dist/library/readStateController.d.ts +28 -0
- package/dist/library/readStateController.js +32 -0
- package/dist/library/requestedStopProcessHost.d.ts +28 -0
- package/dist/library/requestedStopProcessHost.js +58 -0
- package/dist/lock/instanceLock.d.ts +26 -0
- package/dist/lock/instanceLock.js +125 -0
- package/dist/preflight/repositoryPreflight.d.ts +20 -0
- package/dist/preflight/repositoryPreflight.js +42 -0
- package/dist/quarantine/pathQuarantine.d.ts +113 -0
- package/dist/quarantine/pathQuarantine.js +504 -0
- package/dist/recovery/enmetuRebaseAbort.d.ts +108 -0
- package/dist/recovery/enmetuRebaseAbort.js +302 -0
- package/dist/recovery/operationJournal.d.ts +13 -0
- package/dist/recovery/operationJournal.js +37 -0
- package/dist/recovery/recoveryController.d.ts +48 -0
- package/dist/recovery/recoveryController.js +112 -0
- package/dist/state/stateFileStore.d.ts +18 -0
- package/dist/state/stateFileStore.js +73 -0
- package/dist/state/stateRecorder.d.ts +26 -0
- package/dist/state/stateRecorder.js +69 -0
- package/dist/state/stateRecordingSyncCycle.d.ts +24 -0
- package/dist/state/stateRecordingSyncCycle.js +71 -0
- package/dist/sync/hookAbortingSyncScheduler.d.ts +16 -0
- package/dist/sync/hookAbortingSyncScheduler.js +27 -0
- package/dist/sync/integrateUpstreamController.d.ts +95 -0
- package/dist/sync/integrateUpstreamController.js +353 -0
- package/dist/sync/syncCycleController.d.ts +71 -0
- package/dist/sync/syncCycleController.js +345 -0
- package/dist/sync/syncCycleRunner.d.ts +24 -0
- package/dist/sync/syncCycleRunner.js +169 -0
- package/dist/sync/syncScheduler.d.ts +32 -0
- package/dist/sync/syncScheduler.js +97 -0
- package/package.json +42 -4
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
export const INITIAL_SCHEDULER_STATE = { kind: 'idle' };
|
|
2
|
+
/**
|
|
3
|
+
* The transitions of design 1.15, one function per state. Every event a
|
|
4
|
+
* state does not draw returns that state unchanged with no effects.
|
|
5
|
+
*/
|
|
6
|
+
export function schedulerStep(state, event, timing) {
|
|
7
|
+
switch (state.kind) {
|
|
8
|
+
case 'idle':
|
|
9
|
+
return fromIdle(state, event, timing);
|
|
10
|
+
case 'waiting':
|
|
11
|
+
return fromWaiting(state, event, timing);
|
|
12
|
+
case 'syncing':
|
|
13
|
+
return fromSyncing(state, event, timing);
|
|
14
|
+
case 'stopping':
|
|
15
|
+
return fromStopping(state, event);
|
|
16
|
+
case 'stopped':
|
|
17
|
+
case 'halted':
|
|
18
|
+
return unchanged(state);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function fromIdle(state, event, timing) {
|
|
22
|
+
switch (event.type) {
|
|
23
|
+
case 'started':
|
|
24
|
+
return runCycle(false);
|
|
25
|
+
case 'changed':
|
|
26
|
+
return {
|
|
27
|
+
state: {
|
|
28
|
+
kind: 'waiting',
|
|
29
|
+
firstChangeAt: event.at,
|
|
30
|
+
lastChangeAt: event.at,
|
|
31
|
+
},
|
|
32
|
+
effects: [{ type: 'armQuietTimer', fireAt: event.at + timing.quietMs }],
|
|
33
|
+
};
|
|
34
|
+
case 'pollDue':
|
|
35
|
+
return runCycle(false);
|
|
36
|
+
case 'stopRequested':
|
|
37
|
+
return endNow('stopped');
|
|
38
|
+
case 'quietElapsed':
|
|
39
|
+
case 'cycleFinished':
|
|
40
|
+
return unchanged(state);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function fromWaiting(state, event, timing) {
|
|
44
|
+
switch (event.type) {
|
|
45
|
+
// The change that put us here becomes a follow-up change cycle, so it
|
|
46
|
+
// cannot swallow the startup cycle (D5, F14).
|
|
47
|
+
case 'started':
|
|
48
|
+
return runCycle(true);
|
|
49
|
+
case 'changed':
|
|
50
|
+
return {
|
|
51
|
+
state: {
|
|
52
|
+
kind: 'waiting',
|
|
53
|
+
firstChangeAt: state.firstChangeAt,
|
|
54
|
+
lastChangeAt: event.at,
|
|
55
|
+
},
|
|
56
|
+
effects: [
|
|
57
|
+
{
|
|
58
|
+
type: 'armQuietTimer',
|
|
59
|
+
// maxDelayMs bounds how long a continuous writer can postpone the
|
|
60
|
+
// commit and, through the deferred poll, every incoming change.
|
|
61
|
+
fireAt: Math.min(event.at + timing.quietMs, state.firstChangeAt + timing.maxDelayMs),
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
case 'pollDue':
|
|
66
|
+
return {
|
|
67
|
+
state,
|
|
68
|
+
effects: [{ type: 'armPollTimer', fireAt: event.at + timing.pollMs }],
|
|
69
|
+
};
|
|
70
|
+
case 'quietElapsed':
|
|
71
|
+
return runCycle(false);
|
|
72
|
+
case 'stopRequested':
|
|
73
|
+
return endNow('stopped');
|
|
74
|
+
case 'cycleFinished':
|
|
75
|
+
return unchanged(state);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function fromSyncing(state, event, timing) {
|
|
79
|
+
switch (event.type) {
|
|
80
|
+
case 'changed':
|
|
81
|
+
return {
|
|
82
|
+
state: { kind: 'syncing', changedDuringSync: true },
|
|
83
|
+
effects: [],
|
|
84
|
+
};
|
|
85
|
+
case 'cycleFinished':
|
|
86
|
+
return afterCycle(state, event.at, event.outcome, timing);
|
|
87
|
+
case 'stopRequested':
|
|
88
|
+
return {
|
|
89
|
+
state: { kind: 'stopping' },
|
|
90
|
+
effects: [{ type: 'cancelTimers' }],
|
|
91
|
+
};
|
|
92
|
+
case 'started':
|
|
93
|
+
case 'quietElapsed':
|
|
94
|
+
case 'pollDue':
|
|
95
|
+
return unchanged(state);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function afterCycle(state, at, outcome, timing) {
|
|
99
|
+
if (outcome === 'halted') {
|
|
100
|
+
return endNow('halted');
|
|
101
|
+
}
|
|
102
|
+
// A change seen during the cycle, or a commit deferred because git was
|
|
103
|
+
// busy, re-arms the quiet window instead of being dropped (D7).
|
|
104
|
+
if (state.changedDuringSync || outcome === 'busy') {
|
|
105
|
+
return {
|
|
106
|
+
state: { kind: 'waiting', firstChangeAt: at, lastChangeAt: at },
|
|
107
|
+
effects: [
|
|
108
|
+
{ type: 'armQuietTimer', fireAt: at + timing.quietMs },
|
|
109
|
+
{ type: 'armPollTimer', fireAt: at + timing.pollMs },
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
state: { kind: 'idle' },
|
|
115
|
+
effects: [{ type: 'armPollTimer', fireAt: at + timing.pollMs }],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function fromStopping(state, event) {
|
|
119
|
+
if (event.type === 'cycleFinished') {
|
|
120
|
+
// A halt set by the last cycle outranks the stop: the marker refuses the
|
|
121
|
+
// next start, so the run ends with the halt's exit code (Decision 54).
|
|
122
|
+
const reason = event.outcome === 'halted' ? 'halted' : 'stopped';
|
|
123
|
+
return {
|
|
124
|
+
state: { kind: reason },
|
|
125
|
+
effects: [{ type: 'end', reason }],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return unchanged(state);
|
|
129
|
+
}
|
|
130
|
+
function runCycle(changedDuringSync) {
|
|
131
|
+
return {
|
|
132
|
+
state: { kind: 'syncing', changedDuringSync },
|
|
133
|
+
effects: [{ type: 'runCycle' }],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function endNow(reason) {
|
|
137
|
+
return {
|
|
138
|
+
state: { kind: reason },
|
|
139
|
+
effects: [{ type: 'cancelTimers' }, { type: 'end', reason }],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function unchanged(state) {
|
|
143
|
+
return { state, effects: [] };
|
|
144
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { HaltRecord } from '../../contracts/IHaltStore.js';
|
|
2
|
+
import type { RebaseJournal } from '../../contracts/IOperationJournal.js';
|
|
3
|
+
import type { QuarantineMemento } from '../../contracts/IPathQuarantine.js';
|
|
4
|
+
import type { StateRecord } from '../../contracts/IStateStore.js';
|
|
5
|
+
/** instance.lock content; the token identifies the writing process for release. */
|
|
6
|
+
export interface LockRecord {
|
|
7
|
+
readonly pid: number;
|
|
8
|
+
readonly hostname: string;
|
|
9
|
+
readonly startedAt: string;
|
|
10
|
+
readonly token: string;
|
|
11
|
+
}
|
|
12
|
+
/** Each parser returns null for text that is not JSON or not the expected shape. */
|
|
13
|
+
export declare function parseHaltRecord(text: string): HaltRecord | null;
|
|
14
|
+
export declare function parseLockRecord(text: string): LockRecord | null;
|
|
15
|
+
export declare function parseRebaseJournal(text: string): RebaseJournal | null;
|
|
16
|
+
export declare function parseQuarantineMemento(text: string): QuarantineMemento | null;
|
|
17
|
+
/**
|
|
18
|
+
* state.json (design 12.1): null unless every field has its type, version is
|
|
19
|
+
* 1, state and reason are known values and history holds valid transitions.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseStateRecord(text: string): StateRecord | null;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
const HALT_CODES = [
|
|
2
|
+
'REBASE_ABORT_UNVERIFIED',
|
|
3
|
+
'UPSTREAM_REWRITTEN',
|
|
4
|
+
'QUARANTINE_RESTORE_FAILED',
|
|
5
|
+
'HALT_MARKER_UNREADABLE',
|
|
6
|
+
];
|
|
7
|
+
/** Each parser returns null for text that is not JSON or not the expected shape. */
|
|
8
|
+
export function parseHaltRecord(text) {
|
|
9
|
+
const value = parseJsonObject(text);
|
|
10
|
+
if (value === null) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const code = HALT_CODES.find((candidate) => candidate === value.code);
|
|
14
|
+
const { message, cycleId, at } = value;
|
|
15
|
+
if (code === undefined ||
|
|
16
|
+
typeof message !== 'string' ||
|
|
17
|
+
!(cycleId === null || typeof cycleId === 'string') ||
|
|
18
|
+
typeof at !== 'string') {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return { code, message, cycleId, at };
|
|
22
|
+
}
|
|
23
|
+
export function parseLockRecord(text) {
|
|
24
|
+
const value = parseJsonObject(text);
|
|
25
|
+
if (value === null) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const { pid, hostname, startedAt, token } = value;
|
|
29
|
+
if (typeof pid !== 'number' ||
|
|
30
|
+
!Number.isInteger(pid) ||
|
|
31
|
+
pid <= 0 ||
|
|
32
|
+
typeof hostname !== 'string' ||
|
|
33
|
+
typeof startedAt !== 'string' ||
|
|
34
|
+
!isNonEmptyString(token)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return { pid, hostname, startedAt, token };
|
|
38
|
+
}
|
|
39
|
+
export function parseRebaseJournal(text) {
|
|
40
|
+
const value = parseJsonObject(text);
|
|
41
|
+
if (value === null) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const { origHead, onto, cycleId, at } = value;
|
|
45
|
+
if (!isNonEmptyString(origHead) ||
|
|
46
|
+
!isNonEmptyString(onto) ||
|
|
47
|
+
typeof cycleId !== 'string' ||
|
|
48
|
+
typeof at !== 'string') {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return { origHead, onto, cycleId, at };
|
|
52
|
+
}
|
|
53
|
+
export function parseQuarantineMemento(text) {
|
|
54
|
+
const value = parseJsonObject(text);
|
|
55
|
+
if (value === null || value.version !== 1 || !Array.isArray(value.entries)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const items = value.entries;
|
|
59
|
+
const entries = [];
|
|
60
|
+
for (const item of items) {
|
|
61
|
+
const entry = toQuarantineEntry(item);
|
|
62
|
+
if (entry === null) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
entries.push(entry);
|
|
66
|
+
}
|
|
67
|
+
return { version: 1, entries };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* state.json (design 12.1): null unless every field has its type, version is
|
|
71
|
+
* 1, state and reason are known values and history holds valid transitions.
|
|
72
|
+
*/
|
|
73
|
+
export function parseStateRecord(text) {
|
|
74
|
+
const value = parseJsonObject(text);
|
|
75
|
+
if (value === null || value.version !== 1) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const instance = toStateInstance(value.instance);
|
|
79
|
+
const state = DAEMON_STATES.find((candidate) => candidate === value.state);
|
|
80
|
+
const reason = toStateReason(value.reason);
|
|
81
|
+
const lastCycle = toLastCycle(value.lastCycle);
|
|
82
|
+
const conflict = toConflictKey(value.conflict);
|
|
83
|
+
const history = toHistory(value.history);
|
|
84
|
+
const orphaned = toOrphaned(value.orphaned);
|
|
85
|
+
const { message, since, pushed, unpushed } = value;
|
|
86
|
+
if (instance === null ||
|
|
87
|
+
state === undefined ||
|
|
88
|
+
reason === undefined ||
|
|
89
|
+
typeof message !== 'string' ||
|
|
90
|
+
typeof since !== 'string' ||
|
|
91
|
+
!(pushed === null || isNonEmptyString(pushed)) ||
|
|
92
|
+
!(unpushed === null || isCount(unpushed)) ||
|
|
93
|
+
lastCycle === undefined ||
|
|
94
|
+
conflict === undefined ||
|
|
95
|
+
history === null ||
|
|
96
|
+
orphaned === null) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
version: 1,
|
|
101
|
+
instance,
|
|
102
|
+
state,
|
|
103
|
+
reason,
|
|
104
|
+
message,
|
|
105
|
+
since,
|
|
106
|
+
pushed,
|
|
107
|
+
unpushed,
|
|
108
|
+
lastCycle,
|
|
109
|
+
conflict,
|
|
110
|
+
history,
|
|
111
|
+
orphaned,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* null: malformed. Absent is [] (a file written before design 14.15 must
|
|
116
|
+
* keep its record, like heldOver in a memento).
|
|
117
|
+
*/
|
|
118
|
+
function toOrphaned(item) {
|
|
119
|
+
if (item === undefined) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
if (!Array.isArray(item)) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const items = item;
|
|
126
|
+
const orphaned = [];
|
|
127
|
+
for (const entry of items) {
|
|
128
|
+
if (!isRecord(entry)) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const { commit, base, subject, at } = entry;
|
|
132
|
+
if (!isObjectId(commit) ||
|
|
133
|
+
!isObjectId(base) ||
|
|
134
|
+
typeof subject !== 'string' ||
|
|
135
|
+
typeof at !== 'string') {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
orphaned.push({ commit, base, subject, at });
|
|
139
|
+
}
|
|
140
|
+
return orphaned;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* A full object id (SHA-1 or SHA-256): status hands it to git in a revision
|
|
144
|
+
* argument, so a hand-edited file cannot slip an option into the argv.
|
|
145
|
+
*/
|
|
146
|
+
function isObjectId(value) {
|
|
147
|
+
return (typeof value === 'string' && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(value));
|
|
148
|
+
}
|
|
149
|
+
const DAEMON_STATES = [
|
|
150
|
+
'starting',
|
|
151
|
+
'synced',
|
|
152
|
+
'waiting',
|
|
153
|
+
'conflict',
|
|
154
|
+
'paused',
|
|
155
|
+
'busy',
|
|
156
|
+
'offline',
|
|
157
|
+
'push-failed',
|
|
158
|
+
'halted',
|
|
159
|
+
'stopped',
|
|
160
|
+
];
|
|
161
|
+
const STATE_REASONS = [
|
|
162
|
+
'FOREIGN_OPERATION',
|
|
163
|
+
'DETACHED_HEAD',
|
|
164
|
+
'NO_UPSTREAM',
|
|
165
|
+
'UPSTREAM_NOT_REMOTE',
|
|
166
|
+
'UPSTREAM_GONE',
|
|
167
|
+
'BRANCH_CHANGED',
|
|
168
|
+
'QUARANTINE_UNSUPPORTED',
|
|
169
|
+
'REBASE_REFUSED',
|
|
170
|
+
'PARTIAL_SELECTION_STAGED',
|
|
171
|
+
'COMMIT_HOOK_REJECTED',
|
|
172
|
+
...HALT_CODES,
|
|
173
|
+
];
|
|
174
|
+
const OUTCOME_KINDS = [
|
|
175
|
+
'synced',
|
|
176
|
+
'paused',
|
|
177
|
+
'busy',
|
|
178
|
+
'offline',
|
|
179
|
+
'conflict',
|
|
180
|
+
'push-failed',
|
|
181
|
+
'halted',
|
|
182
|
+
'waiting',
|
|
183
|
+
'failed',
|
|
184
|
+
];
|
|
185
|
+
function toStateInstance(item) {
|
|
186
|
+
if (!isRecord(item)) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const { pid, hostname, startedAt } = item;
|
|
190
|
+
// pid 0 is the instance of a record no started event began (stateRecordStep).
|
|
191
|
+
if (!isCount(pid) ||
|
|
192
|
+
typeof hostname !== 'string' ||
|
|
193
|
+
typeof startedAt !== 'string') {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
return { pid, hostname, startedAt };
|
|
197
|
+
}
|
|
198
|
+
/** undefined: not a reason; null: no reason. */
|
|
199
|
+
function toStateReason(item) {
|
|
200
|
+
if (item === null) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return STATE_REASONS.find((candidate) => candidate === item);
|
|
204
|
+
}
|
|
205
|
+
/** undefined: malformed; null: no cycle yet. */
|
|
206
|
+
function toLastCycle(item) {
|
|
207
|
+
if (item === null) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
if (!isRecord(item)) {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
const { cycleId, at } = item;
|
|
214
|
+
const outcome = OUTCOME_KINDS.find((candidate) => candidate === item.outcome);
|
|
215
|
+
if (typeof cycleId !== 'string' ||
|
|
216
|
+
outcome === undefined ||
|
|
217
|
+
typeof at !== 'string') {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
return { cycleId, outcome, at };
|
|
221
|
+
}
|
|
222
|
+
/** undefined: malformed; null: no conflict held. */
|
|
223
|
+
function toConflictKey(item) {
|
|
224
|
+
if (item === null) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
if (!isRecord(item)) {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
const { head, upstream } = item;
|
|
231
|
+
if (!isNonEmptyString(head) || !isNonEmptyString(upstream)) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
return { head, upstream };
|
|
235
|
+
}
|
|
236
|
+
function toHistory(item) {
|
|
237
|
+
if (!Array.isArray(item)) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const items = item;
|
|
241
|
+
const history = [];
|
|
242
|
+
for (const entry of items) {
|
|
243
|
+
const transition = toTransition(entry);
|
|
244
|
+
if (transition === null) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
history.push(transition);
|
|
248
|
+
}
|
|
249
|
+
return history;
|
|
250
|
+
}
|
|
251
|
+
function toTransition(item) {
|
|
252
|
+
if (!isRecord(item)) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const state = DAEMON_STATES.find((candidate) => candidate === item.state);
|
|
256
|
+
const reason = toStateReason(item.reason);
|
|
257
|
+
const { at } = item;
|
|
258
|
+
if (state === undefined || reason === undefined || typeof at !== 'string') {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
return { state, reason, at };
|
|
262
|
+
}
|
|
263
|
+
function isCount(value) {
|
|
264
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0;
|
|
265
|
+
}
|
|
266
|
+
function toQuarantineEntry(item) {
|
|
267
|
+
if (!isRecord(item)) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
const { path, blobId, executable, heldOver } = item;
|
|
271
|
+
if (!isNonEmptyString(path) ||
|
|
272
|
+
!(blobId === null || isNonEmptyString(blobId)) ||
|
|
273
|
+
typeof executable !== 'boolean' ||
|
|
274
|
+
!(heldOver === undefined || heldOver === null || isNonEmptyString(heldOver))) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
// A memento written before heldOver existed may still be pending after an upgrade.
|
|
278
|
+
return { path, blobId, executable, heldOver: heldOver ?? null };
|
|
279
|
+
}
|
|
280
|
+
function parseJsonObject(text) {
|
|
281
|
+
let parsed;
|
|
282
|
+
try {
|
|
283
|
+
parsed = JSON.parse(text);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
return isRecord(parsed) ? parsed : null;
|
|
289
|
+
}
|
|
290
|
+
function isRecord(value) {
|
|
291
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
292
|
+
}
|
|
293
|
+
function isNonEmptyString(value) {
|
|
294
|
+
return typeof value === 'string' && value !== '';
|
|
295
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LockHolder } from '../../contracts/IInstanceLock.js';
|
|
2
|
+
import type { StateRecord } from '../../contracts/IStateStore.js';
|
|
3
|
+
import type { StateLiveness } from '../../contracts/library.js';
|
|
4
|
+
/**
|
|
5
|
+
* One rule for status and the library (design 12.1, Decision 64): the record
|
|
6
|
+
* is the live instance's when the lock's live holder has its pid and
|
|
7
|
+
* startedAt. The lock already judges liveness (a dead pid is no holder), so
|
|
8
|
+
* no heartbeat threshold is needed.
|
|
9
|
+
*/
|
|
10
|
+
export declare function stateLiveness(record: StateRecord | null, liveHolder: LockHolder | null): StateLiveness;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One rule for status and the library (design 12.1, Decision 64): the record
|
|
3
|
+
* is the live instance's when the lock's live holder has its pid and
|
|
4
|
+
* startedAt. The lock already judges liveness (a dead pid is no holder), so
|
|
5
|
+
* no heartbeat threshold is needed.
|
|
6
|
+
*/
|
|
7
|
+
export function stateLiveness(record, liveHolder) {
|
|
8
|
+
if (liveHolder === null) {
|
|
9
|
+
if (record === null) {
|
|
10
|
+
return 'never';
|
|
11
|
+
}
|
|
12
|
+
return record.state === 'stopped' ? 'stopped' : 'crashed';
|
|
13
|
+
}
|
|
14
|
+
if (record === null ||
|
|
15
|
+
record.instance.pid !== liveHolder.pid ||
|
|
16
|
+
record.instance.startedAt !== liveHolder.startedAt) {
|
|
17
|
+
return 'unrecorded';
|
|
18
|
+
}
|
|
19
|
+
// The milliseconds between writing stopped and releasing the lock.
|
|
20
|
+
return record.state === 'stopped' ? 'stopped' : 'running';
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { StateEvent } from '../../contracts/IStateRecorder.js';
|
|
2
|
+
import type { StateRecord } from '../../contracts/IStateStore.js';
|
|
3
|
+
/** The hard maximum of stateHistory: a typo or a library caller cannot make state.json large. */
|
|
4
|
+
export declare const MAX_STATE_HISTORY = 100;
|
|
5
|
+
/**
|
|
6
|
+
* A longer message (a halt's) is cut to this many characters and ends in
|
|
7
|
+
* "...". Characters are code points (Array.from), so an astral character is
|
|
8
|
+
* never split into a lone surrogate.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MAX_STATE_MESSAGE_CHARACTERS = 300;
|
|
11
|
+
/**
|
|
12
|
+
* How many orphaned commits the record keeps, newest last (design 14.15). An
|
|
13
|
+
* orphan needs a person's commit inside enmetu's stopped rebase, so five
|
|
14
|
+
* pending at once is already far beyond what the window produces; the cap
|
|
15
|
+
* keeps state.json bounded (Decision 63) whatever happens.
|
|
16
|
+
*/
|
|
17
|
+
export declare const MAX_ORPHANED_COMMITS = 5;
|
|
18
|
+
/** An orphaned commit's subject is cut to this many characters, like a message (git's conventional subject width). */
|
|
19
|
+
export declare const MAX_ORPHAN_SUBJECT_CHARACTERS = 72;
|
|
20
|
+
/**
|
|
21
|
+
* Functional core of the state record (design 12.1): the record after an
|
|
22
|
+
* event. A transition is a change of (state, reason); it moves `since` and is
|
|
23
|
+
* appended to the history, which keeps the newest min(historyCap,
|
|
24
|
+
* MAX_STATE_HISTORY) entries. `started` begins a fresh record for the new
|
|
25
|
+
* instance and carries the previous history across the restart; any other
|
|
26
|
+
* event without a previous record applies to EMPTY_RECORD.
|
|
27
|
+
*/
|
|
28
|
+
export declare function stateRecordStep(previous: StateRecord | null, event: StateEvent, at: string, historyCap: number): StateRecord;
|