@object-ui/core 0.3.0 → 0.5.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/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +8 -0
- package/dist/actions/ActionRunner.d.ts +40 -0
- package/dist/actions/ActionRunner.js +160 -0
- package/dist/actions/index.d.ts +8 -0
- package/dist/actions/index.js +8 -0
- package/dist/adapters/index.d.ts +7 -0
- package/dist/adapters/index.js +10 -0
- package/dist/builder/schema-builder.d.ts +7 -0
- package/dist/builder/schema-builder.js +4 -6
- package/dist/evaluator/ExpressionCache.d.ts +101 -0
- package/dist/evaluator/ExpressionCache.js +135 -0
- package/dist/evaluator/ExpressionContext.d.ts +51 -0
- package/dist/evaluator/ExpressionContext.js +110 -0
- package/dist/evaluator/ExpressionEvaluator.d.ts +117 -0
- package/dist/evaluator/ExpressionEvaluator.js +220 -0
- package/dist/evaluator/index.d.ts +10 -0
- package/dist/evaluator/index.js +10 -0
- package/dist/index.d.ts +17 -4
- package/dist/index.js +16 -5
- package/dist/query/index.d.ts +6 -0
- package/dist/query/index.js +6 -0
- package/dist/query/query-ast.d.ts +32 -0
- package/dist/query/query-ast.js +268 -0
- package/dist/registry/PluginScopeImpl.d.ts +80 -0
- package/dist/registry/PluginScopeImpl.js +243 -0
- package/dist/registry/PluginSystem.d.ts +66 -0
- package/dist/registry/PluginSystem.js +142 -0
- package/dist/registry/Registry.d.ts +80 -4
- package/dist/registry/Registry.js +119 -7
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.js +7 -0
- package/dist/utils/filter-converter.d.ts +57 -0
- package/dist/utils/filter-converter.js +100 -0
- package/dist/validation/index.d.ts +9 -0
- package/dist/validation/index.js +9 -0
- package/dist/validation/schema-validator.d.ts +7 -0
- package/dist/validation/schema-validator.js +4 -6
- package/dist/validation/validation-engine.d.ts +70 -0
- package/dist/validation/validation-engine.js +363 -0
- package/dist/validation/validators/index.d.ts +16 -0
- package/dist/validation/validators/index.js +16 -0
- package/dist/validation/validators/object-validation-engine.d.ts +118 -0
- package/dist/validation/validators/object-validation-engine.js +538 -0
- package/package.json +26 -7
- package/src/actions/ActionRunner.ts +195 -0
- package/src/actions/index.ts +9 -0
- package/src/adapters/README.md +180 -0
- package/src/adapters/index.ts +10 -0
- package/src/builder/schema-builder.ts +8 -0
- package/src/evaluator/ExpressionCache.ts +192 -0
- package/src/evaluator/ExpressionContext.ts +118 -0
- package/src/evaluator/ExpressionEvaluator.ts +267 -0
- package/src/evaluator/__tests__/ExpressionCache.test.ts +135 -0
- package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +101 -0
- package/src/evaluator/index.ts +11 -0
- package/src/index.test.ts +8 -0
- package/src/index.ts +18 -5
- package/src/query/__tests__/query-ast.test.ts +211 -0
- package/src/query/__tests__/window-functions.test.ts +275 -0
- package/src/query/index.ts +7 -0
- package/src/query/query-ast.ts +341 -0
- package/src/registry/PluginScopeImpl.ts +259 -0
- package/src/registry/PluginSystem.ts +161 -0
- package/src/registry/Registry.ts +133 -8
- package/src/registry/__tests__/PluginSystem.test.ts +226 -0
- package/src/registry/__tests__/Registry.test.ts +293 -0
- package/src/registry/__tests__/plugin-scope-integration.test.ts +283 -0
- package/src/types/index.ts +8 -0
- package/src/utils/__tests__/filter-converter.test.ts +118 -0
- package/src/utils/filter-converter.ts +133 -0
- package/src/validation/__tests__/object-validation-engine.test.ts +567 -0
- package/src/validation/__tests__/validation-engine.test.ts +102 -0
- package/src/validation/index.ts +10 -0
- package/src/validation/schema-validator.ts +8 -0
- package/src/validation/validation-engine.ts +461 -0
- package/src/validation/validators/index.ts +25 -0
- package/src/validation/validators/object-validation-engine.ts +722 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +2 -0
- package/src/builder/schema-builder.d.ts +0 -287
- package/src/builder/schema-builder.js +0 -505
- package/src/index.d.ts +0 -4
- package/src/index.js +0 -7
- package/src/registry/Registry.d.ts +0 -49
- package/src/registry/Registry.js +0 -36
- package/src/types/index.d.ts +0 -12
- package/src/types/index.js +0 -1
- package/src/validation/schema-validator.d.ts +0 -87
- package/src/validation/schema-validator.js +0 -280
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI - Query AST Builder
|
|
3
|
+
* Phase 3.3: QuerySchema AST implementation
|
|
4
|
+
* ObjectStack Spec v0.7.1: Window functions support
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
QueryAST,
|
|
9
|
+
QuerySchema,
|
|
10
|
+
SelectNode,
|
|
11
|
+
FromNode,
|
|
12
|
+
WhereNode,
|
|
13
|
+
JoinNode,
|
|
14
|
+
GroupByNode,
|
|
15
|
+
OrderByNode,
|
|
16
|
+
LimitNode,
|
|
17
|
+
OffsetNode,
|
|
18
|
+
AggregateNode,
|
|
19
|
+
WindowNode,
|
|
20
|
+
WindowFunction,
|
|
21
|
+
WindowFrame,
|
|
22
|
+
WindowConfig,
|
|
23
|
+
FieldNode,
|
|
24
|
+
LiteralNode,
|
|
25
|
+
OperatorNode,
|
|
26
|
+
LogicalOperator,
|
|
27
|
+
AdvancedFilterSchema,
|
|
28
|
+
AdvancedFilterCondition,
|
|
29
|
+
QuerySortConfig,
|
|
30
|
+
JoinConfig,
|
|
31
|
+
AggregationConfig,
|
|
32
|
+
} from '@object-ui/types';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Query AST Builder - Converts QuerySchema to AST
|
|
36
|
+
*/
|
|
37
|
+
export class QueryASTBuilder {
|
|
38
|
+
build(query: QuerySchema): QueryAST {
|
|
39
|
+
const ast: QueryAST = {
|
|
40
|
+
select: this.buildSelect(query),
|
|
41
|
+
from: this.buildFrom(query),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (query.filter) {
|
|
45
|
+
ast.where = this.buildWhere(query.filter);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (query.joins && query.joins.length > 0) {
|
|
49
|
+
ast.joins = query.joins.map(join => this.buildJoin(join));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (query.group_by && query.group_by.length > 0) {
|
|
53
|
+
ast.group_by = this.buildGroupBy(query.group_by);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (query.sort && query.sort.length > 0) {
|
|
57
|
+
ast.order_by = this.buildOrderBy(query.sort);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (query.limit !== undefined) {
|
|
61
|
+
ast.limit = this.buildLimit(query.limit);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (query.offset !== undefined) {
|
|
65
|
+
ast.offset = this.buildOffset(query.offset);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return ast;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private buildSelect(query: QuerySchema): SelectNode {
|
|
72
|
+
const fields: (FieldNode | AggregateNode | WindowNode)[] = [];
|
|
73
|
+
|
|
74
|
+
if (query.fields && query.fields.length > 0) {
|
|
75
|
+
fields.push(...query.fields.map(field => this.buildField(field)));
|
|
76
|
+
} else if (!query.aggregations || query.aggregations.length === 0) {
|
|
77
|
+
// Only add '*' if there are no aggregations
|
|
78
|
+
fields.push(this.buildField('*'));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (query.aggregations && query.aggregations.length > 0) {
|
|
82
|
+
fields.push(...query.aggregations.map(agg => this.buildAggregation(agg)));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Add window functions (ObjectStack Spec v0.7.1)
|
|
86
|
+
if (query.windows && query.windows.length > 0) {
|
|
87
|
+
fields.push(...query.windows.map(win => this.buildWindow(win)));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
type: 'select',
|
|
92
|
+
fields,
|
|
93
|
+
distinct: false,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private buildFrom(query: QuerySchema): FromNode {
|
|
98
|
+
return {
|
|
99
|
+
type: 'from',
|
|
100
|
+
table: query.object,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private buildWhere(filter: AdvancedFilterSchema): WhereNode {
|
|
105
|
+
return {
|
|
106
|
+
type: 'where',
|
|
107
|
+
condition: this.buildFilterCondition(filter),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private buildFilterCondition(filter: AdvancedFilterSchema): OperatorNode {
|
|
112
|
+
const operator = filter.operator || 'and';
|
|
113
|
+
const operands: (OperatorNode | FieldNode | LiteralNode)[] = [];
|
|
114
|
+
|
|
115
|
+
if (filter.conditions && filter.conditions.length > 0) {
|
|
116
|
+
operands.push(...filter.conditions.map(cond => this.buildCondition(cond)));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (filter.groups && filter.groups.length > 0) {
|
|
120
|
+
operands.push(...filter.groups.map(group => this.buildFilterCondition(group)));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
type: 'operator',
|
|
125
|
+
operator: operator as LogicalOperator,
|
|
126
|
+
operands,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private buildCondition(condition: AdvancedFilterCondition): OperatorNode {
|
|
131
|
+
const field = this.buildField(condition.field);
|
|
132
|
+
|
|
133
|
+
// Map filter operators to comparison operators
|
|
134
|
+
const operatorMap: Record<string, string> = {
|
|
135
|
+
'equals': '=',
|
|
136
|
+
'not_equals': '!=',
|
|
137
|
+
'greater_than': '>',
|
|
138
|
+
'greater_than_or_equal': '>=',
|
|
139
|
+
'less_than': '<',
|
|
140
|
+
'less_than_or_equal': '<=',
|
|
141
|
+
'contains': 'contains',
|
|
142
|
+
'not_contains': 'contains',
|
|
143
|
+
'starts_with': 'starts_with',
|
|
144
|
+
'ends_with': 'ends_with',
|
|
145
|
+
'like': 'like',
|
|
146
|
+
'ilike': 'ilike',
|
|
147
|
+
'in': 'in',
|
|
148
|
+
'not_in': 'not_in',
|
|
149
|
+
'is_null': 'is_null',
|
|
150
|
+
'is_not_null': 'is_not_null',
|
|
151
|
+
'between': 'between',
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const operator = operatorMap[condition.operator] || '=';
|
|
155
|
+
|
|
156
|
+
// Handle special operators
|
|
157
|
+
if (operator === 'between' && condition.values && condition.values.length === 2) {
|
|
158
|
+
return {
|
|
159
|
+
type: 'operator',
|
|
160
|
+
operator: 'between' as any,
|
|
161
|
+
operands: [
|
|
162
|
+
field,
|
|
163
|
+
this.buildLiteral(condition.values[0]),
|
|
164
|
+
this.buildLiteral(condition.values[1])
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if ((operator === 'in' || operator === 'not_in') && condition.values) {
|
|
170
|
+
return {
|
|
171
|
+
type: 'operator',
|
|
172
|
+
operator: operator as any,
|
|
173
|
+
operands: [field, ...condition.values.map(v => this.buildLiteral(v))],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (operator === 'is_null' || operator === 'is_not_null') {
|
|
178
|
+
return {
|
|
179
|
+
type: 'operator',
|
|
180
|
+
operator: operator as any,
|
|
181
|
+
operands: [field],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Standard binary operator
|
|
186
|
+
const value = this.buildLiteral(condition.value);
|
|
187
|
+
return {
|
|
188
|
+
type: 'operator',
|
|
189
|
+
operator: operator as any,
|
|
190
|
+
operands: [field, value],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private buildJoin(join: JoinConfig): JoinNode {
|
|
195
|
+
const onCondition: OperatorNode = {
|
|
196
|
+
type: 'operator',
|
|
197
|
+
operator: '=',
|
|
198
|
+
operands: [
|
|
199
|
+
this.buildField(join.on.local_field),
|
|
200
|
+
this.buildField(join.on.foreign_field, join.alias || join.object),
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
type: 'join',
|
|
206
|
+
join_type: join.type,
|
|
207
|
+
table: join.object,
|
|
208
|
+
alias: join.alias,
|
|
209
|
+
on: onCondition,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private buildGroupBy(fields: string[]): GroupByNode {
|
|
214
|
+
return {
|
|
215
|
+
type: 'group_by',
|
|
216
|
+
fields: fields.map(field => this.buildField(field)),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private buildOrderBy(sorts: QuerySortConfig[]): OrderByNode {
|
|
221
|
+
return {
|
|
222
|
+
type: 'order_by',
|
|
223
|
+
fields: sorts.map(sort => ({
|
|
224
|
+
field: this.buildField(sort.field),
|
|
225
|
+
direction: sort.order,
|
|
226
|
+
})),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private buildLimit(limit: number): LimitNode {
|
|
231
|
+
return {
|
|
232
|
+
type: 'limit',
|
|
233
|
+
value: limit,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private buildOffset(offset: number): OffsetNode {
|
|
238
|
+
return {
|
|
239
|
+
type: 'offset',
|
|
240
|
+
value: offset,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private buildField(field: string, table?: string): FieldNode {
|
|
245
|
+
const parts = field.split('.');
|
|
246
|
+
|
|
247
|
+
if (parts.length === 2) {
|
|
248
|
+
return {
|
|
249
|
+
type: 'field',
|
|
250
|
+
table: parts[0],
|
|
251
|
+
name: parts[1],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
type: 'field',
|
|
257
|
+
table,
|
|
258
|
+
name: field,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private buildLiteral(value: any): LiteralNode {
|
|
263
|
+
let dataType: 'string' | 'number' | 'boolean' | 'date' | 'null' = 'string';
|
|
264
|
+
|
|
265
|
+
if (value === null || value === undefined) {
|
|
266
|
+
dataType = 'null';
|
|
267
|
+
} else if (typeof value === 'number') {
|
|
268
|
+
dataType = 'number';
|
|
269
|
+
} else if (typeof value === 'boolean') {
|
|
270
|
+
dataType = 'boolean';
|
|
271
|
+
} else if (value instanceof Date) {
|
|
272
|
+
dataType = 'date';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
type: 'literal',
|
|
277
|
+
value,
|
|
278
|
+
data_type: dataType,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private buildAggregation(agg: AggregationConfig): AggregateNode {
|
|
283
|
+
return {
|
|
284
|
+
type: 'aggregate',
|
|
285
|
+
function: agg.function,
|
|
286
|
+
field: agg.field ? this.buildField(agg.field) : undefined,
|
|
287
|
+
alias: agg.alias,
|
|
288
|
+
distinct: agg.distinct,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Build window function node (ObjectStack Spec v0.7.1)
|
|
294
|
+
*/
|
|
295
|
+
private buildWindow(config: WindowConfig): WindowNode {
|
|
296
|
+
const node: WindowNode = {
|
|
297
|
+
type: 'window',
|
|
298
|
+
function: config.function,
|
|
299
|
+
alias: config.alias,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
if (config.field) {
|
|
303
|
+
node.field = this.buildField(config.field);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (config.partitionBy && config.partitionBy.length > 0) {
|
|
307
|
+
node.partitionBy = config.partitionBy.map(field => this.buildField(field));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (config.orderBy && config.orderBy.length > 0) {
|
|
311
|
+
node.orderBy = config.orderBy.map(sort => ({
|
|
312
|
+
field: this.buildField(sort.field),
|
|
313
|
+
direction: sort.direction,
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (config.frame) {
|
|
318
|
+
node.frame = config.frame;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (config.offset !== undefined) {
|
|
322
|
+
node.offset = config.offset;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (config.defaultValue !== undefined) {
|
|
326
|
+
node.defaultValue = this.buildLiteral(config.defaultValue);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return node;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
optimize(ast: QueryAST): QueryAST {
|
|
333
|
+
return ast;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export const defaultQueryASTBuilder = new QueryASTBuilder();
|
|
338
|
+
|
|
339
|
+
export function buildQueryAST(query: QuerySchema): QueryAST {
|
|
340
|
+
return defaultQueryASTBuilder.build(query);
|
|
341
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @object-ui/core - Plugin Scope Implementation
|
|
11
|
+
*
|
|
12
|
+
* Section 3.3: Implementation of scoped plugin system to prevent conflicts.
|
|
13
|
+
* Provides isolated component registration, state management, and event bus.
|
|
14
|
+
*
|
|
15
|
+
* @module plugin-scope-impl
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type {
|
|
20
|
+
PluginScope,
|
|
21
|
+
PluginScopeConfig,
|
|
22
|
+
PluginEventHandler
|
|
23
|
+
} from '@object-ui/types';
|
|
24
|
+
import type { Registry, ComponentMeta as RegistryComponentMeta } from './Registry.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Event Bus for scoped plugin events
|
|
28
|
+
*/
|
|
29
|
+
class EventBus {
|
|
30
|
+
private listeners = new Map<string, Set<PluginEventHandler>>();
|
|
31
|
+
|
|
32
|
+
on(event: string, handler: PluginEventHandler): () => void {
|
|
33
|
+
if (!this.listeners.has(event)) {
|
|
34
|
+
this.listeners.set(event, new Set());
|
|
35
|
+
}
|
|
36
|
+
this.listeners.get(event)!.add(handler);
|
|
37
|
+
|
|
38
|
+
// Return unsubscribe function
|
|
39
|
+
return () => {
|
|
40
|
+
this.listeners.get(event)?.delete(handler);
|
|
41
|
+
if (this.listeners.get(event)?.size === 0) {
|
|
42
|
+
this.listeners.delete(event);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
emit(event: string, data?: any): void {
|
|
48
|
+
const handlers = this.listeners.get(event);
|
|
49
|
+
if (handlers) {
|
|
50
|
+
handlers.forEach(handler => {
|
|
51
|
+
try {
|
|
52
|
+
handler(data);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(`Error in event handler for "${event}":`, error);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
cleanup(): void {
|
|
61
|
+
this.listeners.clear();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Global event bus for cross-plugin communication
|
|
67
|
+
*/
|
|
68
|
+
const globalEventBus = new EventBus();
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Plugin Scope Implementation
|
|
72
|
+
*
|
|
73
|
+
* Provides isolated access to registry, state, and events for each plugin.
|
|
74
|
+
*/
|
|
75
|
+
export class PluginScopeImpl implements PluginScope {
|
|
76
|
+
public readonly name: string;
|
|
77
|
+
public readonly version: string;
|
|
78
|
+
|
|
79
|
+
private registry: Registry;
|
|
80
|
+
private state = new Map<string, any>();
|
|
81
|
+
private eventBus = new EventBus();
|
|
82
|
+
private config: Required<PluginScopeConfig>;
|
|
83
|
+
|
|
84
|
+
constructor(
|
|
85
|
+
name: string,
|
|
86
|
+
version: string,
|
|
87
|
+
registry: Registry,
|
|
88
|
+
config?: PluginScopeConfig
|
|
89
|
+
) {
|
|
90
|
+
this.name = name;
|
|
91
|
+
this.version = version;
|
|
92
|
+
this.registry = registry;
|
|
93
|
+
this.config = {
|
|
94
|
+
enableStateIsolation: config?.enableStateIsolation ?? true,
|
|
95
|
+
enableEventIsolation: config?.enableEventIsolation ?? true,
|
|
96
|
+
allowGlobalEvents: config?.allowGlobalEvents ?? true,
|
|
97
|
+
maxStateSize: config?.maxStateSize ?? 5 * 1024 * 1024, // 5MB
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Register a component in the scoped namespace
|
|
103
|
+
*/
|
|
104
|
+
registerComponent(type: string, component: any, meta?: any): void {
|
|
105
|
+
// Components are registered as "pluginName:type"
|
|
106
|
+
const registryMeta: RegistryComponentMeta = {
|
|
107
|
+
...meta,
|
|
108
|
+
namespace: this.name,
|
|
109
|
+
};
|
|
110
|
+
this.registry.register(type, component, registryMeta);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get a component from the scoped namespace
|
|
115
|
+
*/
|
|
116
|
+
getComponent(type: string): any | undefined {
|
|
117
|
+
// First try scoped lookup
|
|
118
|
+
const scoped = this.registry.get(`${this.name}:${type}`);
|
|
119
|
+
if (scoped) {
|
|
120
|
+
return scoped;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Fall back to global lookup
|
|
124
|
+
return this.registry.get(type);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Scoped state management
|
|
129
|
+
*/
|
|
130
|
+
useState<T>(key: string, initialValue: T): [T, (value: T | ((prev: T) => T)) => void] {
|
|
131
|
+
if (!this.config.enableStateIsolation) {
|
|
132
|
+
throw new Error('State isolation is disabled for this plugin');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Initialize state if not present
|
|
136
|
+
if (!this.state.has(key)) {
|
|
137
|
+
this.setState(key, initialValue);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const currentValue = this.getState<T>(key) ?? initialValue;
|
|
141
|
+
|
|
142
|
+
const setValue = (value: T | ((prev: T) => T)) => {
|
|
143
|
+
const newValue = typeof value === 'function'
|
|
144
|
+
? (value as (prev: T) => T)(this.getState<T>(key) ?? initialValue)
|
|
145
|
+
: value;
|
|
146
|
+
this.setState(key, newValue);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return [currentValue, setValue];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get scoped state value
|
|
154
|
+
*/
|
|
155
|
+
getState<T>(key: string): T | undefined {
|
|
156
|
+
return this.state.get(key);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Set scoped state value
|
|
161
|
+
*/
|
|
162
|
+
setState<T>(key: string, value: T): void {
|
|
163
|
+
if (!this.config.enableStateIsolation) {
|
|
164
|
+
throw new Error('State isolation is disabled for this plugin');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Check state size limit
|
|
168
|
+
const stateSize = this.estimateStateSize();
|
|
169
|
+
const valueSize = this.estimateValueSize(value);
|
|
170
|
+
|
|
171
|
+
if (stateSize + valueSize > this.config.maxStateSize) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Plugin "${this.name}" exceeded maximum state size of ${this.config.maxStateSize} bytes`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.state.set(key, value);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Subscribe to scoped events
|
|
182
|
+
*/
|
|
183
|
+
on(event: string, handler: PluginEventHandler): () => void {
|
|
184
|
+
if (!this.config.enableEventIsolation) {
|
|
185
|
+
// If isolation is disabled, use global event bus
|
|
186
|
+
return this.onGlobal(event, handler);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Scoped event: prefix with plugin name
|
|
190
|
+
const scopedEvent = `${this.name}:${event}`;
|
|
191
|
+
return this.eventBus.on(scopedEvent, handler);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Emit a scoped event
|
|
196
|
+
*/
|
|
197
|
+
emit(event: string, data?: any): void {
|
|
198
|
+
if (!this.config.enableEventIsolation) {
|
|
199
|
+
// If isolation is disabled, emit globally
|
|
200
|
+
this.emitGlobal(event, data);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Scoped event: prefix with plugin name
|
|
205
|
+
const scopedEvent = `${this.name}:${event}`;
|
|
206
|
+
this.eventBus.emit(scopedEvent, data);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Emit a global event
|
|
211
|
+
*/
|
|
212
|
+
emitGlobal(event: string, data?: any): void {
|
|
213
|
+
if (!this.config.allowGlobalEvents) {
|
|
214
|
+
throw new Error('Global events are disabled for this plugin');
|
|
215
|
+
}
|
|
216
|
+
globalEventBus.emit(event, data);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Subscribe to global events
|
|
221
|
+
*/
|
|
222
|
+
onGlobal(event: string, handler: PluginEventHandler): () => void {
|
|
223
|
+
if (!this.config.allowGlobalEvents) {
|
|
224
|
+
throw new Error('Global events are disabled for this plugin');
|
|
225
|
+
}
|
|
226
|
+
return globalEventBus.on(event, handler);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Clean up all plugin resources
|
|
231
|
+
*/
|
|
232
|
+
cleanup(): void {
|
|
233
|
+
this.state.clear();
|
|
234
|
+
this.eventBus.cleanup();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Estimate total state size in bytes
|
|
239
|
+
*/
|
|
240
|
+
private estimateStateSize(): number {
|
|
241
|
+
let size = 0;
|
|
242
|
+
for (const value of this.state.values()) {
|
|
243
|
+
size += this.estimateValueSize(value);
|
|
244
|
+
}
|
|
245
|
+
return size;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Estimate size of a value in bytes
|
|
250
|
+
*/
|
|
251
|
+
private estimateValueSize(value: any): number {
|
|
252
|
+
try {
|
|
253
|
+
return JSON.stringify(value).length * 2; // UTF-16 encoding
|
|
254
|
+
} catch {
|
|
255
|
+
// If not serializable, use rough estimate
|
|
256
|
+
return 1024; // 1KB default
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|