@object-ui/core 0.3.1 → 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/dist/actions/index.d.ts +1 -1
- package/dist/actions/index.js +1 -1
- package/dist/evaluator/ExpressionCache.d.ts +101 -0
- package/dist/evaluator/ExpressionCache.js +135 -0
- package/dist/evaluator/ExpressionEvaluator.d.ts +20 -2
- package/dist/evaluator/ExpressionEvaluator.js +34 -14
- package/dist/evaluator/index.d.ts +3 -2
- package/dist/evaluator/index.js +3 -2
- package/dist/index.d.ts +10 -7
- package/dist/index.js +9 -7
- 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 +73 -4
- package/dist/registry/Registry.js +112 -7
- package/dist/validation/index.d.ts +9 -0
- package/dist/validation/index.js +9 -0
- 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 +13 -5
- package/src/actions/index.ts +1 -1
- package/src/evaluator/ExpressionCache.ts +192 -0
- package/src/evaluator/ExpressionEvaluator.ts +33 -14
- package/src/evaluator/__tests__/ExpressionCache.test.ts +135 -0
- package/src/evaluator/index.ts +3 -2
- package/src/index.ts +10 -7
- 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 +125 -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/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/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/adapters/index.d.ts +0 -8
- package/src/adapters/index.js +0 -10
- package/src/builder/schema-builder.d.ts +0 -294
- package/src/builder/schema-builder.js +0 -503
- package/src/index.d.ts +0 -13
- package/src/index.js +0 -16
- package/src/registry/Registry.d.ts +0 -56
- package/src/registry/Registry.js +0 -43
- package/src/types/index.d.ts +0 -19
- package/src/types/index.js +0 -8
- package/src/utils/filter-converter.d.ts +0 -57
- package/src/utils/filter-converter.js +0 -100
- package/src/validation/schema-validator.d.ts +0 -94
- package/src/validation/schema-validator.js +0 -278
|
@@ -0,0 +1,268 @@
|
|
|
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
|
+
* Query AST Builder - Converts QuerySchema to AST
|
|
8
|
+
*/
|
|
9
|
+
export class QueryASTBuilder {
|
|
10
|
+
build(query) {
|
|
11
|
+
const ast = {
|
|
12
|
+
select: this.buildSelect(query),
|
|
13
|
+
from: this.buildFrom(query),
|
|
14
|
+
};
|
|
15
|
+
if (query.filter) {
|
|
16
|
+
ast.where = this.buildWhere(query.filter);
|
|
17
|
+
}
|
|
18
|
+
if (query.joins && query.joins.length > 0) {
|
|
19
|
+
ast.joins = query.joins.map(join => this.buildJoin(join));
|
|
20
|
+
}
|
|
21
|
+
if (query.group_by && query.group_by.length > 0) {
|
|
22
|
+
ast.group_by = this.buildGroupBy(query.group_by);
|
|
23
|
+
}
|
|
24
|
+
if (query.sort && query.sort.length > 0) {
|
|
25
|
+
ast.order_by = this.buildOrderBy(query.sort);
|
|
26
|
+
}
|
|
27
|
+
if (query.limit !== undefined) {
|
|
28
|
+
ast.limit = this.buildLimit(query.limit);
|
|
29
|
+
}
|
|
30
|
+
if (query.offset !== undefined) {
|
|
31
|
+
ast.offset = this.buildOffset(query.offset);
|
|
32
|
+
}
|
|
33
|
+
return ast;
|
|
34
|
+
}
|
|
35
|
+
buildSelect(query) {
|
|
36
|
+
const fields = [];
|
|
37
|
+
if (query.fields && query.fields.length > 0) {
|
|
38
|
+
fields.push(...query.fields.map(field => this.buildField(field)));
|
|
39
|
+
}
|
|
40
|
+
else if (!query.aggregations || query.aggregations.length === 0) {
|
|
41
|
+
// Only add '*' if there are no aggregations
|
|
42
|
+
fields.push(this.buildField('*'));
|
|
43
|
+
}
|
|
44
|
+
if (query.aggregations && query.aggregations.length > 0) {
|
|
45
|
+
fields.push(...query.aggregations.map(agg => this.buildAggregation(agg)));
|
|
46
|
+
}
|
|
47
|
+
// Add window functions (ObjectStack Spec v0.7.1)
|
|
48
|
+
if (query.windows && query.windows.length > 0) {
|
|
49
|
+
fields.push(...query.windows.map(win => this.buildWindow(win)));
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
type: 'select',
|
|
53
|
+
fields,
|
|
54
|
+
distinct: false,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
buildFrom(query) {
|
|
58
|
+
return {
|
|
59
|
+
type: 'from',
|
|
60
|
+
table: query.object,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
buildWhere(filter) {
|
|
64
|
+
return {
|
|
65
|
+
type: 'where',
|
|
66
|
+
condition: this.buildFilterCondition(filter),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
buildFilterCondition(filter) {
|
|
70
|
+
const operator = filter.operator || 'and';
|
|
71
|
+
const operands = [];
|
|
72
|
+
if (filter.conditions && filter.conditions.length > 0) {
|
|
73
|
+
operands.push(...filter.conditions.map(cond => this.buildCondition(cond)));
|
|
74
|
+
}
|
|
75
|
+
if (filter.groups && filter.groups.length > 0) {
|
|
76
|
+
operands.push(...filter.groups.map(group => this.buildFilterCondition(group)));
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
type: 'operator',
|
|
80
|
+
operator: operator,
|
|
81
|
+
operands,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
buildCondition(condition) {
|
|
85
|
+
const field = this.buildField(condition.field);
|
|
86
|
+
// Map filter operators to comparison operators
|
|
87
|
+
const operatorMap = {
|
|
88
|
+
'equals': '=',
|
|
89
|
+
'not_equals': '!=',
|
|
90
|
+
'greater_than': '>',
|
|
91
|
+
'greater_than_or_equal': '>=',
|
|
92
|
+
'less_than': '<',
|
|
93
|
+
'less_than_or_equal': '<=',
|
|
94
|
+
'contains': 'contains',
|
|
95
|
+
'not_contains': 'contains',
|
|
96
|
+
'starts_with': 'starts_with',
|
|
97
|
+
'ends_with': 'ends_with',
|
|
98
|
+
'like': 'like',
|
|
99
|
+
'ilike': 'ilike',
|
|
100
|
+
'in': 'in',
|
|
101
|
+
'not_in': 'not_in',
|
|
102
|
+
'is_null': 'is_null',
|
|
103
|
+
'is_not_null': 'is_not_null',
|
|
104
|
+
'between': 'between',
|
|
105
|
+
};
|
|
106
|
+
const operator = operatorMap[condition.operator] || '=';
|
|
107
|
+
// Handle special operators
|
|
108
|
+
if (operator === 'between' && condition.values && condition.values.length === 2) {
|
|
109
|
+
return {
|
|
110
|
+
type: 'operator',
|
|
111
|
+
operator: 'between',
|
|
112
|
+
operands: [
|
|
113
|
+
field,
|
|
114
|
+
this.buildLiteral(condition.values[0]),
|
|
115
|
+
this.buildLiteral(condition.values[1])
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if ((operator === 'in' || operator === 'not_in') && condition.values) {
|
|
120
|
+
return {
|
|
121
|
+
type: 'operator',
|
|
122
|
+
operator: operator,
|
|
123
|
+
operands: [field, ...condition.values.map(v => this.buildLiteral(v))],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (operator === 'is_null' || operator === 'is_not_null') {
|
|
127
|
+
return {
|
|
128
|
+
type: 'operator',
|
|
129
|
+
operator: operator,
|
|
130
|
+
operands: [field],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Standard binary operator
|
|
134
|
+
const value = this.buildLiteral(condition.value);
|
|
135
|
+
return {
|
|
136
|
+
type: 'operator',
|
|
137
|
+
operator: operator,
|
|
138
|
+
operands: [field, value],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
buildJoin(join) {
|
|
142
|
+
const onCondition = {
|
|
143
|
+
type: 'operator',
|
|
144
|
+
operator: '=',
|
|
145
|
+
operands: [
|
|
146
|
+
this.buildField(join.on.local_field),
|
|
147
|
+
this.buildField(join.on.foreign_field, join.alias || join.object),
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
type: 'join',
|
|
152
|
+
join_type: join.type,
|
|
153
|
+
table: join.object,
|
|
154
|
+
alias: join.alias,
|
|
155
|
+
on: onCondition,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
buildGroupBy(fields) {
|
|
159
|
+
return {
|
|
160
|
+
type: 'group_by',
|
|
161
|
+
fields: fields.map(field => this.buildField(field)),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
buildOrderBy(sorts) {
|
|
165
|
+
return {
|
|
166
|
+
type: 'order_by',
|
|
167
|
+
fields: sorts.map(sort => ({
|
|
168
|
+
field: this.buildField(sort.field),
|
|
169
|
+
direction: sort.order,
|
|
170
|
+
})),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
buildLimit(limit) {
|
|
174
|
+
return {
|
|
175
|
+
type: 'limit',
|
|
176
|
+
value: limit,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
buildOffset(offset) {
|
|
180
|
+
return {
|
|
181
|
+
type: 'offset',
|
|
182
|
+
value: offset,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
buildField(field, table) {
|
|
186
|
+
const parts = field.split('.');
|
|
187
|
+
if (parts.length === 2) {
|
|
188
|
+
return {
|
|
189
|
+
type: 'field',
|
|
190
|
+
table: parts[0],
|
|
191
|
+
name: parts[1],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
type: 'field',
|
|
196
|
+
table,
|
|
197
|
+
name: field,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
buildLiteral(value) {
|
|
201
|
+
let dataType = 'string';
|
|
202
|
+
if (value === null || value === undefined) {
|
|
203
|
+
dataType = 'null';
|
|
204
|
+
}
|
|
205
|
+
else if (typeof value === 'number') {
|
|
206
|
+
dataType = 'number';
|
|
207
|
+
}
|
|
208
|
+
else if (typeof value === 'boolean') {
|
|
209
|
+
dataType = 'boolean';
|
|
210
|
+
}
|
|
211
|
+
else if (value instanceof Date) {
|
|
212
|
+
dataType = 'date';
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
type: 'literal',
|
|
216
|
+
value,
|
|
217
|
+
data_type: dataType,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
buildAggregation(agg) {
|
|
221
|
+
return {
|
|
222
|
+
type: 'aggregate',
|
|
223
|
+
function: agg.function,
|
|
224
|
+
field: agg.field ? this.buildField(agg.field) : undefined,
|
|
225
|
+
alias: agg.alias,
|
|
226
|
+
distinct: agg.distinct,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Build window function node (ObjectStack Spec v0.7.1)
|
|
231
|
+
*/
|
|
232
|
+
buildWindow(config) {
|
|
233
|
+
const node = {
|
|
234
|
+
type: 'window',
|
|
235
|
+
function: config.function,
|
|
236
|
+
alias: config.alias,
|
|
237
|
+
};
|
|
238
|
+
if (config.field) {
|
|
239
|
+
node.field = this.buildField(config.field);
|
|
240
|
+
}
|
|
241
|
+
if (config.partitionBy && config.partitionBy.length > 0) {
|
|
242
|
+
node.partitionBy = config.partitionBy.map(field => this.buildField(field));
|
|
243
|
+
}
|
|
244
|
+
if (config.orderBy && config.orderBy.length > 0) {
|
|
245
|
+
node.orderBy = config.orderBy.map(sort => ({
|
|
246
|
+
field: this.buildField(sort.field),
|
|
247
|
+
direction: sort.direction,
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
if (config.frame) {
|
|
251
|
+
node.frame = config.frame;
|
|
252
|
+
}
|
|
253
|
+
if (config.offset !== undefined) {
|
|
254
|
+
node.offset = config.offset;
|
|
255
|
+
}
|
|
256
|
+
if (config.defaultValue !== undefined) {
|
|
257
|
+
node.defaultValue = this.buildLiteral(config.defaultValue);
|
|
258
|
+
}
|
|
259
|
+
return node;
|
|
260
|
+
}
|
|
261
|
+
optimize(ast) {
|
|
262
|
+
return ast;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
export const defaultQueryASTBuilder = new QueryASTBuilder();
|
|
266
|
+
export function buildQueryAST(query) {
|
|
267
|
+
return defaultQueryASTBuilder.build(query);
|
|
268
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
* @object-ui/core - Plugin Scope Implementation
|
|
10
|
+
*
|
|
11
|
+
* Section 3.3: Implementation of scoped plugin system to prevent conflicts.
|
|
12
|
+
* Provides isolated component registration, state management, and event bus.
|
|
13
|
+
*
|
|
14
|
+
* @module plugin-scope-impl
|
|
15
|
+
* @packageDocumentation
|
|
16
|
+
*/
|
|
17
|
+
import type { PluginScope, PluginScopeConfig, PluginEventHandler } from '@object-ui/types';
|
|
18
|
+
import type { Registry } from './Registry.js';
|
|
19
|
+
/**
|
|
20
|
+
* Plugin Scope Implementation
|
|
21
|
+
*
|
|
22
|
+
* Provides isolated access to registry, state, and events for each plugin.
|
|
23
|
+
*/
|
|
24
|
+
export declare class PluginScopeImpl implements PluginScope {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly version: string;
|
|
27
|
+
private registry;
|
|
28
|
+
private state;
|
|
29
|
+
private eventBus;
|
|
30
|
+
private config;
|
|
31
|
+
constructor(name: string, version: string, registry: Registry, config?: PluginScopeConfig);
|
|
32
|
+
/**
|
|
33
|
+
* Register a component in the scoped namespace
|
|
34
|
+
*/
|
|
35
|
+
registerComponent(type: string, component: any, meta?: any): void;
|
|
36
|
+
/**
|
|
37
|
+
* Get a component from the scoped namespace
|
|
38
|
+
*/
|
|
39
|
+
getComponent(type: string): any | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Scoped state management
|
|
42
|
+
*/
|
|
43
|
+
useState<T>(key: string, initialValue: T): [T, (value: T | ((prev: T) => T)) => void];
|
|
44
|
+
/**
|
|
45
|
+
* Get scoped state value
|
|
46
|
+
*/
|
|
47
|
+
getState<T>(key: string): T | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Set scoped state value
|
|
50
|
+
*/
|
|
51
|
+
setState<T>(key: string, value: T): void;
|
|
52
|
+
/**
|
|
53
|
+
* Subscribe to scoped events
|
|
54
|
+
*/
|
|
55
|
+
on(event: string, handler: PluginEventHandler): () => void;
|
|
56
|
+
/**
|
|
57
|
+
* Emit a scoped event
|
|
58
|
+
*/
|
|
59
|
+
emit(event: string, data?: any): void;
|
|
60
|
+
/**
|
|
61
|
+
* Emit a global event
|
|
62
|
+
*/
|
|
63
|
+
emitGlobal(event: string, data?: any): void;
|
|
64
|
+
/**
|
|
65
|
+
* Subscribe to global events
|
|
66
|
+
*/
|
|
67
|
+
onGlobal(event: string, handler: PluginEventHandler): () => void;
|
|
68
|
+
/**
|
|
69
|
+
* Clean up all plugin resources
|
|
70
|
+
*/
|
|
71
|
+
cleanup(): void;
|
|
72
|
+
/**
|
|
73
|
+
* Estimate total state size in bytes
|
|
74
|
+
*/
|
|
75
|
+
private estimateStateSize;
|
|
76
|
+
/**
|
|
77
|
+
* Estimate size of a value in bytes
|
|
78
|
+
*/
|
|
79
|
+
private estimateValueSize;
|
|
80
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
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
|
+
* Event Bus for scoped plugin events
|
|
10
|
+
*/
|
|
11
|
+
class EventBus {
|
|
12
|
+
constructor() {
|
|
13
|
+
Object.defineProperty(this, "listeners", {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
configurable: true,
|
|
16
|
+
writable: true,
|
|
17
|
+
value: new Map()
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
on(event, handler) {
|
|
21
|
+
if (!this.listeners.has(event)) {
|
|
22
|
+
this.listeners.set(event, new Set());
|
|
23
|
+
}
|
|
24
|
+
this.listeners.get(event).add(handler);
|
|
25
|
+
// Return unsubscribe function
|
|
26
|
+
return () => {
|
|
27
|
+
this.listeners.get(event)?.delete(handler);
|
|
28
|
+
if (this.listeners.get(event)?.size === 0) {
|
|
29
|
+
this.listeners.delete(event);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
emit(event, data) {
|
|
34
|
+
const handlers = this.listeners.get(event);
|
|
35
|
+
if (handlers) {
|
|
36
|
+
handlers.forEach(handler => {
|
|
37
|
+
try {
|
|
38
|
+
handler(data);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
console.error(`Error in event handler for "${event}":`, error);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
cleanup() {
|
|
47
|
+
this.listeners.clear();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Global event bus for cross-plugin communication
|
|
52
|
+
*/
|
|
53
|
+
const globalEventBus = new EventBus();
|
|
54
|
+
/**
|
|
55
|
+
* Plugin Scope Implementation
|
|
56
|
+
*
|
|
57
|
+
* Provides isolated access to registry, state, and events for each plugin.
|
|
58
|
+
*/
|
|
59
|
+
export class PluginScopeImpl {
|
|
60
|
+
constructor(name, version, registry, config) {
|
|
61
|
+
Object.defineProperty(this, "name", {
|
|
62
|
+
enumerable: true,
|
|
63
|
+
configurable: true,
|
|
64
|
+
writable: true,
|
|
65
|
+
value: void 0
|
|
66
|
+
});
|
|
67
|
+
Object.defineProperty(this, "version", {
|
|
68
|
+
enumerable: true,
|
|
69
|
+
configurable: true,
|
|
70
|
+
writable: true,
|
|
71
|
+
value: void 0
|
|
72
|
+
});
|
|
73
|
+
Object.defineProperty(this, "registry", {
|
|
74
|
+
enumerable: true,
|
|
75
|
+
configurable: true,
|
|
76
|
+
writable: true,
|
|
77
|
+
value: void 0
|
|
78
|
+
});
|
|
79
|
+
Object.defineProperty(this, "state", {
|
|
80
|
+
enumerable: true,
|
|
81
|
+
configurable: true,
|
|
82
|
+
writable: true,
|
|
83
|
+
value: new Map()
|
|
84
|
+
});
|
|
85
|
+
Object.defineProperty(this, "eventBus", {
|
|
86
|
+
enumerable: true,
|
|
87
|
+
configurable: true,
|
|
88
|
+
writable: true,
|
|
89
|
+
value: new EventBus()
|
|
90
|
+
});
|
|
91
|
+
Object.defineProperty(this, "config", {
|
|
92
|
+
enumerable: true,
|
|
93
|
+
configurable: true,
|
|
94
|
+
writable: true,
|
|
95
|
+
value: void 0
|
|
96
|
+
});
|
|
97
|
+
this.name = name;
|
|
98
|
+
this.version = version;
|
|
99
|
+
this.registry = registry;
|
|
100
|
+
this.config = {
|
|
101
|
+
enableStateIsolation: config?.enableStateIsolation ?? true,
|
|
102
|
+
enableEventIsolation: config?.enableEventIsolation ?? true,
|
|
103
|
+
allowGlobalEvents: config?.allowGlobalEvents ?? true,
|
|
104
|
+
maxStateSize: config?.maxStateSize ?? 5 * 1024 * 1024, // 5MB
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Register a component in the scoped namespace
|
|
109
|
+
*/
|
|
110
|
+
registerComponent(type, component, meta) {
|
|
111
|
+
// Components are registered as "pluginName:type"
|
|
112
|
+
const registryMeta = {
|
|
113
|
+
...meta,
|
|
114
|
+
namespace: this.name,
|
|
115
|
+
};
|
|
116
|
+
this.registry.register(type, component, registryMeta);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get a component from the scoped namespace
|
|
120
|
+
*/
|
|
121
|
+
getComponent(type) {
|
|
122
|
+
// First try scoped lookup
|
|
123
|
+
const scoped = this.registry.get(`${this.name}:${type}`);
|
|
124
|
+
if (scoped) {
|
|
125
|
+
return scoped;
|
|
126
|
+
}
|
|
127
|
+
// Fall back to global lookup
|
|
128
|
+
return this.registry.get(type);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Scoped state management
|
|
132
|
+
*/
|
|
133
|
+
useState(key, initialValue) {
|
|
134
|
+
if (!this.config.enableStateIsolation) {
|
|
135
|
+
throw new Error('State isolation is disabled for this plugin');
|
|
136
|
+
}
|
|
137
|
+
// Initialize state if not present
|
|
138
|
+
if (!this.state.has(key)) {
|
|
139
|
+
this.setState(key, initialValue);
|
|
140
|
+
}
|
|
141
|
+
const currentValue = this.getState(key) ?? initialValue;
|
|
142
|
+
const setValue = (value) => {
|
|
143
|
+
const newValue = typeof value === 'function'
|
|
144
|
+
? value(this.getState(key) ?? initialValue)
|
|
145
|
+
: value;
|
|
146
|
+
this.setState(key, newValue);
|
|
147
|
+
};
|
|
148
|
+
return [currentValue, setValue];
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get scoped state value
|
|
152
|
+
*/
|
|
153
|
+
getState(key) {
|
|
154
|
+
return this.state.get(key);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Set scoped state value
|
|
158
|
+
*/
|
|
159
|
+
setState(key, value) {
|
|
160
|
+
if (!this.config.enableStateIsolation) {
|
|
161
|
+
throw new Error('State isolation is disabled for this plugin');
|
|
162
|
+
}
|
|
163
|
+
// Check state size limit
|
|
164
|
+
const stateSize = this.estimateStateSize();
|
|
165
|
+
const valueSize = this.estimateValueSize(value);
|
|
166
|
+
if (stateSize + valueSize > this.config.maxStateSize) {
|
|
167
|
+
throw new Error(`Plugin "${this.name}" exceeded maximum state size of ${this.config.maxStateSize} bytes`);
|
|
168
|
+
}
|
|
169
|
+
this.state.set(key, value);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Subscribe to scoped events
|
|
173
|
+
*/
|
|
174
|
+
on(event, handler) {
|
|
175
|
+
if (!this.config.enableEventIsolation) {
|
|
176
|
+
// If isolation is disabled, use global event bus
|
|
177
|
+
return this.onGlobal(event, handler);
|
|
178
|
+
}
|
|
179
|
+
// Scoped event: prefix with plugin name
|
|
180
|
+
const scopedEvent = `${this.name}:${event}`;
|
|
181
|
+
return this.eventBus.on(scopedEvent, handler);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Emit a scoped event
|
|
185
|
+
*/
|
|
186
|
+
emit(event, data) {
|
|
187
|
+
if (!this.config.enableEventIsolation) {
|
|
188
|
+
// If isolation is disabled, emit globally
|
|
189
|
+
this.emitGlobal(event, data);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Scoped event: prefix with plugin name
|
|
193
|
+
const scopedEvent = `${this.name}:${event}`;
|
|
194
|
+
this.eventBus.emit(scopedEvent, data);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Emit a global event
|
|
198
|
+
*/
|
|
199
|
+
emitGlobal(event, data) {
|
|
200
|
+
if (!this.config.allowGlobalEvents) {
|
|
201
|
+
throw new Error('Global events are disabled for this plugin');
|
|
202
|
+
}
|
|
203
|
+
globalEventBus.emit(event, data);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Subscribe to global events
|
|
207
|
+
*/
|
|
208
|
+
onGlobal(event, handler) {
|
|
209
|
+
if (!this.config.allowGlobalEvents) {
|
|
210
|
+
throw new Error('Global events are disabled for this plugin');
|
|
211
|
+
}
|
|
212
|
+
return globalEventBus.on(event, handler);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Clean up all plugin resources
|
|
216
|
+
*/
|
|
217
|
+
cleanup() {
|
|
218
|
+
this.state.clear();
|
|
219
|
+
this.eventBus.cleanup();
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Estimate total state size in bytes
|
|
223
|
+
*/
|
|
224
|
+
estimateStateSize() {
|
|
225
|
+
let size = 0;
|
|
226
|
+
for (const value of this.state.values()) {
|
|
227
|
+
size += this.estimateValueSize(value);
|
|
228
|
+
}
|
|
229
|
+
return size;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Estimate size of a value in bytes
|
|
233
|
+
*/
|
|
234
|
+
estimateValueSize(value) {
|
|
235
|
+
try {
|
|
236
|
+
return JSON.stringify(value).length * 2; // UTF-16 encoding
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// If not serializable, use rough estimate
|
|
240
|
+
return 1024; // 1KB default
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
import type { Registry } from './Registry.js';
|
|
9
|
+
import type { PluginScope, PluginScopeConfig } from '@object-ui/types';
|
|
10
|
+
export interface PluginDefinition {
|
|
11
|
+
name: string;
|
|
12
|
+
version: string;
|
|
13
|
+
dependencies?: string[];
|
|
14
|
+
peerDependencies?: string[];
|
|
15
|
+
register: (registry: Registry | PluginScope) => void;
|
|
16
|
+
onLoad?: () => void | Promise<void>;
|
|
17
|
+
onUnload?: () => void | Promise<void>;
|
|
18
|
+
scopeConfig?: PluginScopeConfig;
|
|
19
|
+
}
|
|
20
|
+
export declare class PluginSystem {
|
|
21
|
+
private plugins;
|
|
22
|
+
private loaded;
|
|
23
|
+
private scopes;
|
|
24
|
+
/**
|
|
25
|
+
* Load a plugin into the system with optional scope isolation
|
|
26
|
+
* @param plugin The plugin definition to load
|
|
27
|
+
* @param registry The component registry to use for registration
|
|
28
|
+
* @param useScope Whether to use scoped loading (default: true for better isolation)
|
|
29
|
+
* @throws Error if dependencies are missing
|
|
30
|
+
*/
|
|
31
|
+
loadPlugin(plugin: PluginDefinition, registry: Registry, useScope?: boolean): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Unload a plugin from the system
|
|
34
|
+
* @param name The name of the plugin to unload
|
|
35
|
+
* @throws Error if other plugins depend on this plugin
|
|
36
|
+
*/
|
|
37
|
+
unloadPlugin(name: string): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Get the scope for a loaded plugin
|
|
40
|
+
* @param name The name of the plugin
|
|
41
|
+
* @returns The plugin scope or undefined
|
|
42
|
+
*/
|
|
43
|
+
getScope(name: string): PluginScope | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Check if a plugin is loaded
|
|
46
|
+
* @param name The name of the plugin
|
|
47
|
+
* @returns true if the plugin is loaded
|
|
48
|
+
*/
|
|
49
|
+
isLoaded(name: string): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Get a loaded plugin definition
|
|
52
|
+
* @param name The name of the plugin
|
|
53
|
+
* @returns The plugin definition or undefined
|
|
54
|
+
*/
|
|
55
|
+
getPlugin(name: string): PluginDefinition | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Get all loaded plugin names
|
|
58
|
+
* @returns Array of loaded plugin names
|
|
59
|
+
*/
|
|
60
|
+
getLoadedPlugins(): string[];
|
|
61
|
+
/**
|
|
62
|
+
* Get all plugin definitions
|
|
63
|
+
* @returns Array of all plugin definitions
|
|
64
|
+
*/
|
|
65
|
+
getAllPlugins(): PluginDefinition[];
|
|
66
|
+
}
|