Skip to content

Commit 06f3ca3

Browse files
author
DavidQ
committed
Add overlay plugin lifecycle.
PR Details: - Standardizes plugin lifecycle behavior
1 parent 14cf416 commit 06f3ca3

7 files changed

Lines changed: 272 additions & 56 deletions

File tree

docs/dev/CODEX_COMMANDS.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ MODEL: GPT-5.4
22
REASONING: medium
33

44
COMMAND:
5-
Implement overlay plugin registry:
6-
- Add registration system
7-
- Validate with one plugin
5+
Implement plugin lifecycle:
6+
- Add init/activate/deactivate/destroy phases
7+
- Ensure safe transitions
8+
- Validate with test plugin
89
- Update roadmap status only
9-
- Package ZIP

docs/dev/COMMIT_COMMENT.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Add overlay plugin registry.
1+
Add overlay plugin lifecycle.
22

33
PR Details:
4-
- Enables structured overlay registration
4+
- Standardizes plugin lifecycle behavior
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[ ] Plugin registers
2-
[ ] Plugin activates
3-
[ ] Plugin removes cleanly
1+
[ ] Init works
2+
[ ] Activate/deactivate stable
3+
[ ] Destroy cleans up
44
[ ] Roadmap updated

docs/dev/roadmaps/MASTER_ROADMAP_HIGH_LEVEL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -819,7 +819,7 @@
819819

820820
### Track G — Extensibility Readiness
821821
- [x] validate plugin/extension patterns
822-
- [ ] validate adding new systems is clean
822+
- [.] validate adding new systems is clean
823823
- [ ] validate external integration points
824824
- [ ] ensure future phases can build cleanly
825825

docs/pr/BUILD_PR.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
# BUILD_PR_LEVEL_20_2_OVERLAY_PLUGIN_REGISTRY
1+
# BUILD_PR_LEVEL_20_3_OVERLAY_PLUGIN_LIFECYCLE
22

33
## Purpose
4-
Introduce a registry for managing overlay plugins.
4+
Define lifecycle for overlay plugins from init to teardown.
55

66
## Roadmap Improvement
7-
Enables structured registration and discovery of overlays.
7+
Completes plugin system foundation by standardizing lifecycle behavior.
88

99
## Scope
10-
- Define plugin registry
11-
- Allow registration/unregistration
12-
- Validate with one plugin
10+
- Define init, activate, deactivate, destroy phases
11+
- Ensure safe transitions between states
12+
- Validate lifecycle with one plugin
1313

1414
## Test Steps
1515
1. Register plugin
16-
2. Activate overlay
17-
3. Remove plugin
16+
2. Activate/deactivate multiple times
17+
3. Destroy plugin
1818

1919
## Expected
20-
- Plugins managed cleanly
20+
- Clean lifecycle transitions
21+
- No memory or state leaks

samples/phase-19/shared/overlay/createPhase19OverlayPluginRegistry.js

Lines changed: 173 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ createPhase19OverlayPluginRegistry.js
66
*/
77
import createPhase19OverlayExpansionFramework from '/samples/phase-19/shared/overlay/createPhase19OverlayExpansionFramework.js';
88

9+
const PLUGIN_STATES = Object.freeze({
10+
REGISTERED: 'registered',
11+
INITIALIZED: 'initialized',
12+
ACTIVE: 'active',
13+
INACTIVE: 'inactive',
14+
});
15+
916
function normalizePluginId(pluginId) {
1017
return String(pluginId || '').trim();
1118
}
@@ -35,6 +42,16 @@ function normalizePluginDefinition(plugin) {
3542
id,
3643
version: String(plugin.version || '').trim(),
3744
metadata: Object.freeze({ ...(plugin.metadata || {}) }),
45+
init: typeof plugin.init === 'function' ? plugin.init : (typeof plugin.onInit === 'function' ? plugin.onInit : null),
46+
activate: typeof plugin.activate === 'function'
47+
? plugin.activate
48+
: (typeof plugin.onActivate === 'function' ? plugin.onActivate : null),
49+
deactivate: typeof plugin.deactivate === 'function'
50+
? plugin.deactivate
51+
: (typeof plugin.onDeactivate === 'function' ? plugin.onDeactivate : null),
52+
destroy: typeof plugin.destroy === 'function'
53+
? plugin.destroy
54+
: (typeof plugin.onDestroy === 'function' ? plugin.onDestroy : null),
3855
createOverlayExtension,
3956
extension,
4057
});
@@ -58,14 +75,133 @@ export default function createPhase19OverlayPluginRegistry({
5875
expansionFramework = createPhase19OverlayExpansionFramework(),
5976
plugins = [],
6077
} = {}) {
61-
const pluginMap = new Map();
62-
const pluginExtensionMap = new Map();
78+
const pluginRecordMap = new Map();
6379
let api = null;
6480

65-
function registerPlugin(plugin, context = {}) {
81+
function getPluginRecord(pluginId) {
82+
const normalizedPluginId = normalizePluginId(pluginId);
83+
if (!normalizedPluginId) {
84+
return null;
85+
}
86+
return pluginRecordMap.get(normalizedPluginId) ?? null;
87+
}
88+
89+
function runLifecycleHook(record, phase, context = {}) {
90+
const hook = record?.plugin?.[phase];
91+
if (typeof hook !== 'function') {
92+
return true;
93+
}
94+
try {
95+
hook({
96+
...context,
97+
phase,
98+
pluginId: record.plugin.id,
99+
extensionId: record.extension.id,
100+
registry: api,
101+
expansionFramework,
102+
});
103+
return true;
104+
} catch {
105+
return false;
106+
}
107+
}
108+
109+
function initPlugin(pluginId, context = {}) {
110+
const record = getPluginRecord(pluginId);
111+
if (!record) {
112+
return false;
113+
}
114+
if (record.state === PLUGIN_STATES.INITIALIZED || record.state === PLUGIN_STATES.ACTIVE || record.state === PLUGIN_STATES.INACTIVE) {
115+
return false;
116+
}
117+
if (record.state !== PLUGIN_STATES.REGISTERED) {
118+
return false;
119+
}
120+
if (!runLifecycleHook(record, 'init', context)) {
121+
return false;
122+
}
123+
record.state = PLUGIN_STATES.INITIALIZED;
124+
return true;
125+
}
126+
127+
function activatePlugin(pluginId, context = {}) {
128+
const record = getPluginRecord(pluginId);
129+
if (!record) {
130+
return false;
131+
}
132+
if (record.state === PLUGIN_STATES.ACTIVE) {
133+
return false;
134+
}
135+
if (record.state === PLUGIN_STATES.REGISTERED) {
136+
if (!initPlugin(pluginId, context)) {
137+
return false;
138+
}
139+
}
140+
if (record.state !== PLUGIN_STATES.INITIALIZED && record.state !== PLUGIN_STATES.INACTIVE) {
141+
return false;
142+
}
143+
144+
const registered = expansionFramework.registerExtension(record.extension);
145+
if (!registered || !registered.id) {
146+
return false;
147+
}
148+
149+
if (!runLifecycleHook(record, 'activate', context)) {
150+
expansionFramework.unregisterExtension(record.extension.id);
151+
return false;
152+
}
153+
154+
record.state = PLUGIN_STATES.ACTIVE;
155+
return true;
156+
}
157+
158+
function deactivatePlugin(pluginId, context = {}) {
159+
const record = getPluginRecord(pluginId);
160+
if (!record) {
161+
return false;
162+
}
163+
if (record.state !== PLUGIN_STATES.ACTIVE) {
164+
return false;
165+
}
166+
if (!runLifecycleHook(record, 'deactivate', context)) {
167+
return false;
168+
}
169+
170+
expansionFramework.unregisterExtension(record.extension.id);
171+
record.state = PLUGIN_STATES.INACTIVE;
172+
return true;
173+
}
174+
175+
function destroyPlugin(pluginId, context = {}) {
176+
const normalizedPluginId = normalizePluginId(pluginId);
177+
const record = getPluginRecord(normalizedPluginId);
178+
if (!record) {
179+
return false;
180+
}
181+
if (record.state === PLUGIN_STATES.ACTIVE) {
182+
if (!deactivatePlugin(normalizedPluginId, context)) {
183+
return false;
184+
}
185+
}
186+
if (!runLifecycleHook(record, 'destroy', context)) {
187+
return false;
188+
}
189+
190+
expansionFramework.unregisterExtension(record.extension.id);
191+
pluginRecordMap.delete(normalizedPluginId);
192+
return true;
193+
}
194+
195+
function registerPlugin(plugin, options = {}) {
196+
const context = options?.context || {};
197+
const autoActivate = options?.autoActivate !== false;
66198
const normalizedPlugin = normalizePluginDefinition(plugin);
67199
const pluginId = normalizedPlugin.id;
68200

201+
if (pluginRecordMap.has(pluginId)) {
202+
destroyPlugin(pluginId, { ...context, reason: 're-register' });
203+
}
204+
69205
const resolvedExtension = normalizePluginExtension(
70206
normalizedPlugin,
71207
normalizedPlugin.createOverlayExtension
@@ -78,70 +214,70 @@ export default function createPhase19OverlayPluginRegistry({
78214
: normalizedPlugin.extension
79215
);
80216

81-
const existingExtensionId = pluginExtensionMap.get(pluginId);
82-
if (existingExtensionId) {
83-
expansionFramework.unregisterExtension(existingExtensionId);
84-
}
217+
const record = {
218+
plugin: normalizedPlugin,
219+
extension: resolvedExtension,
220+
state: PLUGIN_STATES.REGISTERED,
221+
};
222+
pluginRecordMap.set(pluginId, record);
85223

86-
const registeredExtension = expansionFramework.registerExtension(resolvedExtension);
87-
pluginMap.set(pluginId, normalizedPlugin);
88-
pluginExtensionMap.set(pluginId, registeredExtension.id);
224+
if (autoActivate) {
225+
if (!activatePlugin(pluginId, context)) {
226+
pluginRecordMap.delete(pluginId);
227+
throw new Error(`Overlay plugin "${pluginId}" failed lifecycle activation.`);
228+
}
229+
}
89230

90231
return Object.freeze({
91232
pluginId,
92-
extensionId: registeredExtension.id,
233+
extensionId: resolvedExtension.id,
93234
});
94235
}
95236

96-
function unregisterPlugin(pluginId) {
97-
const normalizedPluginId = normalizePluginId(pluginId);
98-
if (!normalizedPluginId || !pluginMap.has(normalizedPluginId)) {
99-
return false;
100-
}
101-
102-
const extensionId = pluginExtensionMap.get(normalizedPluginId);
103-
if (extensionId) {
104-
expansionFramework.unregisterExtension(extensionId);
105-
}
106-
pluginExtensionMap.delete(normalizedPluginId);
107-
pluginMap.delete(normalizedPluginId);
108-
return true;
237+
function unregisterPlugin(pluginId, context = {}) {
238+
return destroyPlugin(pluginId, context);
109239
}
110240

111241
function getPlugin(pluginId) {
112-
const normalizedPluginId = normalizePluginId(pluginId);
113-
if (!normalizedPluginId) {
114-
return null;
115-
}
116-
return pluginMap.get(normalizedPluginId) ?? null;
242+
const record = getPluginRecord(pluginId);
243+
return record ? record.plugin : null;
244+
}
245+
246+
function getPluginState(pluginId) {
247+
const record = getPluginRecord(pluginId);
248+
return record?.state || '';
117249
}
118250

119251
function getPluginExtensionId(pluginId) {
120-
const normalizedPluginId = normalizePluginId(pluginId);
121-
if (!normalizedPluginId) {
122-
return '';
123-
}
124-
return pluginExtensionMap.get(normalizedPluginId) || '';
252+
const record = getPluginRecord(pluginId);
253+
return record?.extension?.id || '';
125254
}
126255

127256
function listPlugins() {
128257
const entries = [];
129-
for (const [pluginId, plugin] of pluginMap.entries()) {
258+
for (const [pluginId, record] of pluginRecordMap.entries()) {
130259
entries.push({
131260
id: pluginId,
132-
version: plugin.version,
133-
extensionId: pluginExtensionMap.get(pluginId) || '',
261+
version: record.plugin.version,
262+
extensionId: record.extension.id,
263+
state: record.state,
134264
});
135265
}
136266
return Object.freeze(entries);
137267
}
138268

139269
api = {
140270
registerPlugin,
271+
initPlugin,
272+
activatePlugin,
273+
deactivatePlugin,
274+
destroyPlugin,
141275
unregisterPlugin,
142276
getPlugin,
277+
getPluginState,
143278
getPluginExtensionId,
144279
listPlugins,
280+
states: PLUGIN_STATES,
145281
getFramework() {
146282
return expansionFramework;
147283
},

0 commit comments

Comments
 (0)