@0xobelisk/ecs 1.2.0-pre.24
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 +92 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +2346 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2287 -0
- package/dist/index.mjs.map +1 -0
- package/dist/query.d.ts +154 -0
- package/dist/subscription.d.ts +123 -0
- package/dist/types.d.ts +124 -0
- package/dist/utils.d.ts +92 -0
- package/dist/world.d.ts +290 -0
- package/package.json +155 -0
- package/src/index.ts +58 -0
- package/src/query.ts +810 -0
- package/src/subscription.ts +910 -0
- package/src/types.ts +179 -0
- package/src/utils.ts +291 -0
- package/src/world.ts +1223 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2287 @@
|
|
|
1
|
+
// src/utils.ts
|
|
2
|
+
function extractEntityIds(connection, options) {
|
|
3
|
+
const { idFields = ["nodeId", "entityId"], composite = false } = options || {};
|
|
4
|
+
return connection.edges.map((edge) => {
|
|
5
|
+
const node = edge.node;
|
|
6
|
+
if (composite) {
|
|
7
|
+
const idParts = idFields.map((field) => node[field] || "").filter(Boolean);
|
|
8
|
+
return idParts.join("|");
|
|
9
|
+
} else {
|
|
10
|
+
for (const field of idFields) {
|
|
11
|
+
if (node[field] !== void 0 && node[field] !== null) {
|
|
12
|
+
return node[field];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return Object.values(node)[0] || "";
|
|
16
|
+
}
|
|
17
|
+
}).filter(Boolean);
|
|
18
|
+
}
|
|
19
|
+
function calculateDelta(oldResults, newResults) {
|
|
20
|
+
const oldSet = new Set(oldResults);
|
|
21
|
+
const newSet = new Set(newResults);
|
|
22
|
+
const added = newResults.filter((id) => !oldSet.has(id));
|
|
23
|
+
const removed = oldResults.filter((id) => !newSet.has(id));
|
|
24
|
+
return {
|
|
25
|
+
added,
|
|
26
|
+
removed,
|
|
27
|
+
current: newResults
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function findEntityIntersection(entitySets) {
|
|
31
|
+
if (entitySets.length === 0)
|
|
32
|
+
return [];
|
|
33
|
+
if (entitySets.length === 1)
|
|
34
|
+
return entitySets[0];
|
|
35
|
+
return entitySets.reduce((intersection, currentSet) => {
|
|
36
|
+
const currentSetLookup = new Set(currentSet);
|
|
37
|
+
return intersection.filter((id) => currentSetLookup.has(id));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function findEntityUnion(entitySets) {
|
|
41
|
+
const unionSet = /* @__PURE__ */ new Set();
|
|
42
|
+
entitySets.forEach((set) => {
|
|
43
|
+
set.forEach((id) => unionSet.add(id));
|
|
44
|
+
});
|
|
45
|
+
return Array.from(unionSet);
|
|
46
|
+
}
|
|
47
|
+
function extractIntersectionFromBatchResult(batchResult, componentTypes, options) {
|
|
48
|
+
const entitySets = componentTypes.map((type) => {
|
|
49
|
+
const connection = batchResult[type];
|
|
50
|
+
return connection ? extractEntityIds(connection, options) : [];
|
|
51
|
+
});
|
|
52
|
+
return findEntityIntersection(entitySets);
|
|
53
|
+
}
|
|
54
|
+
function extractUnionFromBatchResult(batchResult, componentTypes, options) {
|
|
55
|
+
const entitySets = componentTypes.map((type) => {
|
|
56
|
+
const connection = batchResult[type];
|
|
57
|
+
return connection ? extractEntityIds(connection, options) : [];
|
|
58
|
+
});
|
|
59
|
+
return findEntityUnion(entitySets);
|
|
60
|
+
}
|
|
61
|
+
function debounce(func, waitMs) {
|
|
62
|
+
let timeoutId = null;
|
|
63
|
+
return (...args) => {
|
|
64
|
+
if (timeoutId) {
|
|
65
|
+
clearTimeout(timeoutId);
|
|
66
|
+
}
|
|
67
|
+
timeoutId = setTimeout(() => {
|
|
68
|
+
func(...args);
|
|
69
|
+
timeoutId = null;
|
|
70
|
+
}, waitMs);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function normalizeComponentType(componentType) {
|
|
74
|
+
const singular = componentType.endsWith("s") ? componentType.slice(0, -1) : componentType;
|
|
75
|
+
const plural = componentType.endsWith("s") ? componentType : componentType + "s";
|
|
76
|
+
return { singular, plural };
|
|
77
|
+
}
|
|
78
|
+
function createCacheKey(operation, componentTypes, options) {
|
|
79
|
+
const sortedTypes = [...componentTypes].sort();
|
|
80
|
+
const optionsStr = options ? JSON.stringify(options) : "";
|
|
81
|
+
return `${operation}:${sortedTypes.join(",")}:${optionsStr}`;
|
|
82
|
+
}
|
|
83
|
+
function isValidEntityId(entityId) {
|
|
84
|
+
return typeof entityId === "string" && entityId.length > 0;
|
|
85
|
+
}
|
|
86
|
+
function isValidComponentType(componentType) {
|
|
87
|
+
return typeof componentType === "string" && componentType.length > 0;
|
|
88
|
+
}
|
|
89
|
+
function deepEqual(obj1, obj2) {
|
|
90
|
+
if (obj1 === obj2)
|
|
91
|
+
return true;
|
|
92
|
+
if (obj1 == null || obj2 == null)
|
|
93
|
+
return false;
|
|
94
|
+
if (typeof obj1 !== typeof obj2)
|
|
95
|
+
return false;
|
|
96
|
+
if (typeof obj1 !== "object")
|
|
97
|
+
return false;
|
|
98
|
+
const keys1 = Object.keys(obj1);
|
|
99
|
+
const keys2 = Object.keys(obj2);
|
|
100
|
+
if (keys1.length !== keys2.length)
|
|
101
|
+
return false;
|
|
102
|
+
for (const key of keys1) {
|
|
103
|
+
if (!keys2.includes(key))
|
|
104
|
+
return false;
|
|
105
|
+
if (!deepEqual(obj1[key], obj2[key]))
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
function safeJsonParse(json, defaultValue) {
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(json);
|
|
113
|
+
} catch {
|
|
114
|
+
return defaultValue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function formatError(error) {
|
|
118
|
+
if (error instanceof Error) {
|
|
119
|
+
return error.message;
|
|
120
|
+
}
|
|
121
|
+
if (typeof error === "string") {
|
|
122
|
+
return error;
|
|
123
|
+
}
|
|
124
|
+
return JSON.stringify(error);
|
|
125
|
+
}
|
|
126
|
+
function createTimestamp() {
|
|
127
|
+
return Date.now();
|
|
128
|
+
}
|
|
129
|
+
function limitArray(array, limit) {
|
|
130
|
+
return limit > 0 ? array.slice(0, limit) : array;
|
|
131
|
+
}
|
|
132
|
+
function paginateArray(array, page, pageSize) {
|
|
133
|
+
const startIndex = (page - 1) * pageSize;
|
|
134
|
+
const endIndex = startIndex + pageSize;
|
|
135
|
+
const items = array.slice(startIndex, endIndex);
|
|
136
|
+
return {
|
|
137
|
+
items,
|
|
138
|
+
totalCount: array.length,
|
|
139
|
+
hasMore: endIndex < array.length,
|
|
140
|
+
page,
|
|
141
|
+
pageSize
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/query.ts
|
|
146
|
+
var ECSQuery = class {
|
|
147
|
+
constructor(graphqlClient, componentDiscoverer) {
|
|
148
|
+
this.queryCache = /* @__PURE__ */ new Map();
|
|
149
|
+
this.cacheTimeout = 5e3;
|
|
150
|
+
// 5 second cache timeout
|
|
151
|
+
this.availableComponents = [];
|
|
152
|
+
this.componentDiscoverer = null;
|
|
153
|
+
// Component primary key cache - pre-parsed during initialization
|
|
154
|
+
this.componentPrimaryKeys = /* @__PURE__ */ new Map();
|
|
155
|
+
this.graphqlClient = graphqlClient;
|
|
156
|
+
this.componentDiscoverer = componentDiscoverer || null;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Set available component list
|
|
160
|
+
*/
|
|
161
|
+
setAvailableComponents(componentTypes) {
|
|
162
|
+
this.availableComponents = componentTypes;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Pre-parse and cache all component primary key information
|
|
166
|
+
*/
|
|
167
|
+
initializeComponentMetadata(componentMetadataList) {
|
|
168
|
+
this.componentPrimaryKeys.clear();
|
|
169
|
+
for (const metadata of componentMetadataList) {
|
|
170
|
+
if (metadata.primaryKeys.length === 1) {
|
|
171
|
+
this.componentPrimaryKeys.set(metadata.name, metadata.primaryKeys[0]);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Get component's primary key field name (quickly retrieve from cache)
|
|
177
|
+
*/
|
|
178
|
+
getComponentPrimaryKeyField(componentType) {
|
|
179
|
+
return this.componentPrimaryKeys.get(componentType) || "entityId";
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Set component discoverer
|
|
183
|
+
*/
|
|
184
|
+
setComponentDiscoverer(discoverer) {
|
|
185
|
+
this.componentDiscoverer = discoverer;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get component field information
|
|
189
|
+
*/
|
|
190
|
+
async getComponentFields(componentType) {
|
|
191
|
+
if (this.componentDiscoverer) {
|
|
192
|
+
try {
|
|
193
|
+
const metadata = this.componentDiscoverer.getComponentMetadata(componentType);
|
|
194
|
+
if (metadata) {
|
|
195
|
+
return metadata.fields.map((field) => field.name);
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Unable to get field information for component ${componentType}. Please explicitly specify fields in QueryOptions or ensure component discoverer is properly configured.`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Get component's primary key fields
|
|
206
|
+
*/
|
|
207
|
+
async getComponentPrimaryKeys(componentType) {
|
|
208
|
+
if (this.componentDiscoverer) {
|
|
209
|
+
try {
|
|
210
|
+
const metadata = this.componentDiscoverer.getComponentMetadata(componentType);
|
|
211
|
+
if (metadata && metadata.primaryKeys.length > 0) {
|
|
212
|
+
return metadata.primaryKeys;
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Unable to get primary key information for component ${componentType}. Please explicitly specify idFields in QueryOptions or ensure component discoverer is properly configured.`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get fields to use for queries (priority: user specified > dubhe config auto-parsed)
|
|
223
|
+
*/
|
|
224
|
+
async getQueryFields(componentType, userFields) {
|
|
225
|
+
if (userFields && userFields.length > 0) {
|
|
226
|
+
return userFields;
|
|
227
|
+
}
|
|
228
|
+
return this.getComponentFields(componentType);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Check if entity exists
|
|
232
|
+
*/
|
|
233
|
+
async hasEntity(entityId) {
|
|
234
|
+
if (!isValidEntityId(entityId))
|
|
235
|
+
return false;
|
|
236
|
+
try {
|
|
237
|
+
const tables = await this.getAvailableComponents();
|
|
238
|
+
for (const table of tables) {
|
|
239
|
+
try {
|
|
240
|
+
const condition = this.buildEntityCondition(table, entityId);
|
|
241
|
+
const component = await this.graphqlClient.getTableByCondition(
|
|
242
|
+
table,
|
|
243
|
+
condition
|
|
244
|
+
);
|
|
245
|
+
if (component)
|
|
246
|
+
return true;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Get all entity IDs (collected from all component tables)
|
|
257
|
+
*/
|
|
258
|
+
async getAllEntities() {
|
|
259
|
+
try {
|
|
260
|
+
const tables = await this.getAvailableComponents();
|
|
261
|
+
const queries = await Promise.all(
|
|
262
|
+
tables.map(async (table) => {
|
|
263
|
+
const fields = await this.getQueryFields(table);
|
|
264
|
+
const primaryKey = this.componentPrimaryKeys.get(table) || "entityId";
|
|
265
|
+
return {
|
|
266
|
+
key: table,
|
|
267
|
+
tableName: table,
|
|
268
|
+
params: {
|
|
269
|
+
fields,
|
|
270
|
+
filter: {}
|
|
271
|
+
},
|
|
272
|
+
primaryKey
|
|
273
|
+
// Use cached primary key information
|
|
274
|
+
};
|
|
275
|
+
})
|
|
276
|
+
);
|
|
277
|
+
const batchResult = await this.graphqlClient.batchQuery(
|
|
278
|
+
queries.map((q) => ({
|
|
279
|
+
key: q.key,
|
|
280
|
+
tableName: q.tableName,
|
|
281
|
+
params: q.params
|
|
282
|
+
}))
|
|
283
|
+
);
|
|
284
|
+
return extractUnionFromBatchResult(batchResult, tables, {
|
|
285
|
+
idFields: void 0,
|
|
286
|
+
// Let extractEntityIds auto-infer
|
|
287
|
+
composite: false
|
|
288
|
+
});
|
|
289
|
+
} catch (error) {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Get entity count
|
|
295
|
+
*/
|
|
296
|
+
async getEntityCount() {
|
|
297
|
+
const entities = await this.getAllEntities();
|
|
298
|
+
return entities.length;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Check if entity has specific component
|
|
302
|
+
*/
|
|
303
|
+
async hasComponent(entityId, componentType) {
|
|
304
|
+
if (!isValidEntityId(entityId) || !isValidComponentType(componentType)) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
if (!this.isECSComponent(componentType)) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
const condition = this.buildEntityCondition(componentType, entityId);
|
|
312
|
+
const component = await this.graphqlClient.getTableByCondition(
|
|
313
|
+
componentType,
|
|
314
|
+
condition
|
|
315
|
+
);
|
|
316
|
+
return component !== null;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Get specific component data of entity
|
|
323
|
+
*/
|
|
324
|
+
async getComponent(entityId, componentType) {
|
|
325
|
+
if (!isValidEntityId(entityId) || !isValidComponentType(componentType)) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
if (!this.isECSComponent(componentType)) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
const condition = this.buildEntityCondition(componentType, entityId);
|
|
333
|
+
const component = await this.graphqlClient.getTableByCondition(
|
|
334
|
+
componentType,
|
|
335
|
+
condition
|
|
336
|
+
);
|
|
337
|
+
return component;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Get all component types that entity has
|
|
344
|
+
*/
|
|
345
|
+
async getComponents(entityId) {
|
|
346
|
+
if (!isValidEntityId(entityId))
|
|
347
|
+
return [];
|
|
348
|
+
try {
|
|
349
|
+
const tables = await this.getAvailableComponents();
|
|
350
|
+
const componentTypes = [];
|
|
351
|
+
await Promise.all(
|
|
352
|
+
tables.map(async (table) => {
|
|
353
|
+
const hasComp = await this.hasComponent(entityId, table);
|
|
354
|
+
if (hasComp) {
|
|
355
|
+
componentTypes.push(table);
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
);
|
|
359
|
+
return componentTypes;
|
|
360
|
+
} catch (error) {
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Validate if component type is ECS-compliant
|
|
366
|
+
*/
|
|
367
|
+
isECSComponent(componentType) {
|
|
368
|
+
return this.availableComponents.includes(componentType);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Build entity query condition (using cached primary key field name)
|
|
372
|
+
*/
|
|
373
|
+
buildEntityCondition(componentType, entityId) {
|
|
374
|
+
const primaryKeyField = this.componentPrimaryKeys.get(componentType);
|
|
375
|
+
if (primaryKeyField) {
|
|
376
|
+
return { [primaryKeyField]: entityId };
|
|
377
|
+
} else {
|
|
378
|
+
return { entityId };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Filter and validate component type list, keeping only ECS-compliant components
|
|
383
|
+
*/
|
|
384
|
+
filterValidECSComponents(componentTypes) {
|
|
385
|
+
const validComponents = componentTypes.filter((componentType) => {
|
|
386
|
+
if (!isValidComponentType(componentType)) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
if (!this.isECSComponent(componentType)) {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
return true;
|
|
393
|
+
});
|
|
394
|
+
return validComponents;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Query all entities that have a specific component
|
|
398
|
+
*/
|
|
399
|
+
async queryWith(componentType, options) {
|
|
400
|
+
if (!isValidComponentType(componentType))
|
|
401
|
+
return [];
|
|
402
|
+
if (!this.isECSComponent(componentType)) {
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
const cacheKey = createCacheKey("queryWith", [componentType], options);
|
|
406
|
+
const cached = this.getCachedResult(cacheKey);
|
|
407
|
+
if (cached && options?.cache !== false)
|
|
408
|
+
return cached;
|
|
409
|
+
try {
|
|
410
|
+
const queryFields = await this.getQueryFields(
|
|
411
|
+
componentType,
|
|
412
|
+
options?.fields
|
|
413
|
+
);
|
|
414
|
+
const primaryKeys = await this.getComponentPrimaryKeys(componentType);
|
|
415
|
+
const connection = await this.graphqlClient.getAllTables(componentType, {
|
|
416
|
+
first: options?.limit,
|
|
417
|
+
fields: queryFields,
|
|
418
|
+
orderBy: options?.orderBy
|
|
419
|
+
});
|
|
420
|
+
const result = extractEntityIds(connection, {
|
|
421
|
+
idFields: options?.idFields || primaryKeys,
|
|
422
|
+
composite: options?.compositeId
|
|
423
|
+
});
|
|
424
|
+
this.setCachedResult(cacheKey, result);
|
|
425
|
+
return result;
|
|
426
|
+
} catch (error) {
|
|
427
|
+
return [];
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Query entities that have all specified components (intersection)
|
|
432
|
+
*/
|
|
433
|
+
async queryWithAll(componentTypes, options) {
|
|
434
|
+
if (componentTypes.length === 0)
|
|
435
|
+
return [];
|
|
436
|
+
if (componentTypes.length === 1)
|
|
437
|
+
return this.queryWith(componentTypes[0], options);
|
|
438
|
+
const validTypes = this.filterValidECSComponents(componentTypes);
|
|
439
|
+
if (validTypes.length === 0)
|
|
440
|
+
return [];
|
|
441
|
+
const cacheKey = createCacheKey("queryWithAll", validTypes, options);
|
|
442
|
+
const cached = this.getCachedResult(cacheKey);
|
|
443
|
+
if (cached && options?.cache !== false)
|
|
444
|
+
return cached;
|
|
445
|
+
try {
|
|
446
|
+
const queries = await Promise.all(
|
|
447
|
+
validTypes.map(async (type) => {
|
|
448
|
+
const queryFields = await this.getQueryFields(type, options?.fields);
|
|
449
|
+
return {
|
|
450
|
+
key: type,
|
|
451
|
+
tableName: type,
|
|
452
|
+
params: {
|
|
453
|
+
fields: queryFields,
|
|
454
|
+
first: options?.limit,
|
|
455
|
+
orderBy: options?.orderBy
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
})
|
|
459
|
+
);
|
|
460
|
+
const batchResult = await this.graphqlClient.batchQuery(queries);
|
|
461
|
+
let idFields = options?.idFields;
|
|
462
|
+
if (!idFields && validTypes.length > 0) {
|
|
463
|
+
try {
|
|
464
|
+
idFields = await this.getComponentPrimaryKeys(validTypes[0]);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const result = extractIntersectionFromBatchResult(
|
|
469
|
+
batchResult,
|
|
470
|
+
validTypes,
|
|
471
|
+
{
|
|
472
|
+
idFields,
|
|
473
|
+
composite: options?.compositeId
|
|
474
|
+
}
|
|
475
|
+
);
|
|
476
|
+
this.setCachedResult(cacheKey, result);
|
|
477
|
+
return result;
|
|
478
|
+
} catch (error) {
|
|
479
|
+
return [];
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Query entities that have any of the specified components (union)
|
|
484
|
+
*/
|
|
485
|
+
async queryWithAny(componentTypes, options) {
|
|
486
|
+
if (componentTypes.length === 0)
|
|
487
|
+
return [];
|
|
488
|
+
if (componentTypes.length === 1)
|
|
489
|
+
return this.queryWith(componentTypes[0], options);
|
|
490
|
+
const validTypes = this.filterValidECSComponents(componentTypes);
|
|
491
|
+
if (validTypes.length === 0)
|
|
492
|
+
return [];
|
|
493
|
+
const cacheKey = createCacheKey("queryWithAny", validTypes, options);
|
|
494
|
+
const cached = this.getCachedResult(cacheKey);
|
|
495
|
+
if (cached && options?.cache !== false)
|
|
496
|
+
return cached;
|
|
497
|
+
try {
|
|
498
|
+
const queries = await Promise.all(
|
|
499
|
+
validTypes.map(async (type) => {
|
|
500
|
+
const queryFields = await this.getQueryFields(type, options?.fields);
|
|
501
|
+
return {
|
|
502
|
+
key: type,
|
|
503
|
+
tableName: type,
|
|
504
|
+
params: {
|
|
505
|
+
fields: queryFields,
|
|
506
|
+
first: options?.limit,
|
|
507
|
+
orderBy: options?.orderBy
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
})
|
|
511
|
+
);
|
|
512
|
+
const batchResult = await this.graphqlClient.batchQuery(queries);
|
|
513
|
+
let idFields = options?.idFields;
|
|
514
|
+
if (!idFields && validTypes.length > 0) {
|
|
515
|
+
try {
|
|
516
|
+
idFields = await this.getComponentPrimaryKeys(validTypes[0]);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const result = extractUnionFromBatchResult(batchResult, validTypes, {
|
|
521
|
+
idFields,
|
|
522
|
+
composite: options?.compositeId
|
|
523
|
+
});
|
|
524
|
+
this.setCachedResult(cacheKey, result);
|
|
525
|
+
return result;
|
|
526
|
+
} catch (error) {
|
|
527
|
+
return [];
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Query entities that have include components but not exclude components
|
|
532
|
+
*/
|
|
533
|
+
async queryWithout(includeTypes, excludeTypes, options) {
|
|
534
|
+
if (includeTypes.length === 0)
|
|
535
|
+
return [];
|
|
536
|
+
const validIncludeTypes = this.filterValidECSComponents(includeTypes);
|
|
537
|
+
if (validIncludeTypes.length === 0)
|
|
538
|
+
return [];
|
|
539
|
+
const validExcludeTypes = this.filterValidECSComponents(excludeTypes);
|
|
540
|
+
try {
|
|
541
|
+
const includedEntities = await this.queryWithAll(
|
|
542
|
+
validIncludeTypes,
|
|
543
|
+
options
|
|
544
|
+
);
|
|
545
|
+
if (validExcludeTypes.length === 0)
|
|
546
|
+
return includedEntities;
|
|
547
|
+
const excludedEntities = await this.queryWithAny(validExcludeTypes);
|
|
548
|
+
const excludedSet = new Set(excludedEntities);
|
|
549
|
+
return includedEntities.filter((entityId) => !excludedSet.has(entityId));
|
|
550
|
+
} catch (error) {
|
|
551
|
+
return [];
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Query components based on conditions
|
|
556
|
+
*/
|
|
557
|
+
async queryWhere(componentType, predicate, options) {
|
|
558
|
+
if (!isValidComponentType(componentType))
|
|
559
|
+
return [];
|
|
560
|
+
if (!this.isECSComponent(componentType)) {
|
|
561
|
+
return [];
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
const queryFields = await this.getQueryFields(
|
|
565
|
+
componentType,
|
|
566
|
+
options?.fields
|
|
567
|
+
);
|
|
568
|
+
const primaryKeys = await this.getComponentPrimaryKeys(componentType);
|
|
569
|
+
const connection = await this.graphqlClient.getAllTables(componentType, {
|
|
570
|
+
filter: predicate,
|
|
571
|
+
first: options?.limit,
|
|
572
|
+
fields: queryFields,
|
|
573
|
+
orderBy: options?.orderBy
|
|
574
|
+
});
|
|
575
|
+
return extractEntityIds(connection, {
|
|
576
|
+
idFields: options?.idFields || primaryKeys,
|
|
577
|
+
composite: options?.compositeId
|
|
578
|
+
});
|
|
579
|
+
} catch (error) {
|
|
580
|
+
return [];
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Range query
|
|
585
|
+
*/
|
|
586
|
+
async queryRange(componentType, field, min, max, options) {
|
|
587
|
+
if (!isValidComponentType(componentType))
|
|
588
|
+
return [];
|
|
589
|
+
if (!this.isECSComponent(componentType)) {
|
|
590
|
+
return [];
|
|
591
|
+
}
|
|
592
|
+
const predicate = {
|
|
593
|
+
[field]: {
|
|
594
|
+
greaterThanOrEqualTo: min,
|
|
595
|
+
lessThanOrEqualTo: max
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
return this.queryWhere(componentType, predicate, options);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Paginated query
|
|
602
|
+
*/
|
|
603
|
+
async queryPaged(componentTypes, page, pageSize) {
|
|
604
|
+
try {
|
|
605
|
+
const allResults = componentTypes.length === 1 ? await this.queryWith(componentTypes[0]) : await this.queryWithAll(componentTypes);
|
|
606
|
+
return paginateArray(allResults, page, pageSize);
|
|
607
|
+
} catch (error) {
|
|
608
|
+
return {
|
|
609
|
+
items: [],
|
|
610
|
+
totalCount: 0,
|
|
611
|
+
hasMore: false,
|
|
612
|
+
page,
|
|
613
|
+
pageSize
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Create query builder
|
|
619
|
+
*/
|
|
620
|
+
query() {
|
|
621
|
+
return new ECSQueryBuilder(this);
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Get cached result
|
|
625
|
+
*/
|
|
626
|
+
getCachedResult(cacheKey) {
|
|
627
|
+
const cached = this.queryCache.get(cacheKey);
|
|
628
|
+
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
|
|
629
|
+
return cached.result;
|
|
630
|
+
}
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Set cached result
|
|
635
|
+
*/
|
|
636
|
+
setCachedResult(cacheKey, result) {
|
|
637
|
+
this.queryCache.set(cacheKey, {
|
|
638
|
+
result,
|
|
639
|
+
timestamp: Date.now()
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Clean expired cache
|
|
644
|
+
*/
|
|
645
|
+
cleanExpiredCache() {
|
|
646
|
+
const now = Date.now();
|
|
647
|
+
for (const [key, cached] of this.queryCache.entries()) {
|
|
648
|
+
if (now - cached.timestamp >= this.cacheTimeout) {
|
|
649
|
+
this.queryCache.delete(key);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Get available component list
|
|
655
|
+
*/
|
|
656
|
+
async getAvailableComponents() {
|
|
657
|
+
if (this.availableComponents.length > 0) {
|
|
658
|
+
return this.availableComponents;
|
|
659
|
+
}
|
|
660
|
+
return [];
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Dispose resources
|
|
664
|
+
*/
|
|
665
|
+
dispose() {
|
|
666
|
+
this.queryCache.clear();
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
var ECSQueryBuilder = class {
|
|
670
|
+
constructor(ecsQuery) {
|
|
671
|
+
this.includeTypes = [];
|
|
672
|
+
this.excludeTypes = [];
|
|
673
|
+
this.whereConditions = [];
|
|
674
|
+
this.orderByOptions = [];
|
|
675
|
+
this.ecsQuery = ecsQuery;
|
|
676
|
+
}
|
|
677
|
+
with(...componentTypes) {
|
|
678
|
+
this.includeTypes.push(...componentTypes);
|
|
679
|
+
return this;
|
|
680
|
+
}
|
|
681
|
+
without(...componentTypes) {
|
|
682
|
+
this.excludeTypes.push(...componentTypes);
|
|
683
|
+
return this;
|
|
684
|
+
}
|
|
685
|
+
where(componentType, predicate) {
|
|
686
|
+
this.whereConditions.push({ componentType, predicate });
|
|
687
|
+
return this;
|
|
688
|
+
}
|
|
689
|
+
orderBy(componentType, field, direction = "ASC") {
|
|
690
|
+
this.orderByOptions.push({ componentType, field, direction });
|
|
691
|
+
return this;
|
|
692
|
+
}
|
|
693
|
+
limit(count) {
|
|
694
|
+
this.limitValue = count;
|
|
695
|
+
return this;
|
|
696
|
+
}
|
|
697
|
+
offset(count) {
|
|
698
|
+
this.offsetValue = count;
|
|
699
|
+
return this;
|
|
700
|
+
}
|
|
701
|
+
async execute() {
|
|
702
|
+
try {
|
|
703
|
+
const options = {
|
|
704
|
+
limit: this.limitValue,
|
|
705
|
+
offset: this.offsetValue,
|
|
706
|
+
orderBy: this.orderByOptions.map((order) => ({
|
|
707
|
+
field: order.field,
|
|
708
|
+
direction: order.direction
|
|
709
|
+
}))
|
|
710
|
+
};
|
|
711
|
+
if (this.whereConditions.length > 0) {
|
|
712
|
+
const filteredResults = [];
|
|
713
|
+
for (const condition of this.whereConditions) {
|
|
714
|
+
const result = await this.ecsQuery.queryWhere(
|
|
715
|
+
condition.componentType,
|
|
716
|
+
condition.predicate,
|
|
717
|
+
options
|
|
718
|
+
);
|
|
719
|
+
filteredResults.push(result);
|
|
720
|
+
}
|
|
721
|
+
const intersection = filteredResults.reduce((acc, current) => {
|
|
722
|
+
const currentSet = new Set(current);
|
|
723
|
+
return acc.filter((id) => currentSet.has(id));
|
|
724
|
+
});
|
|
725
|
+
return intersection;
|
|
726
|
+
}
|
|
727
|
+
if (this.excludeTypes.length > 0) {
|
|
728
|
+
return this.ecsQuery.queryWithout(
|
|
729
|
+
this.includeTypes,
|
|
730
|
+
this.excludeTypes,
|
|
731
|
+
options
|
|
732
|
+
);
|
|
733
|
+
} else {
|
|
734
|
+
return this.ecsQuery.queryWithAll(this.includeTypes, options);
|
|
735
|
+
}
|
|
736
|
+
} catch (error) {
|
|
737
|
+
return [];
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
// src/subscription.ts
|
|
743
|
+
import { Observable } from "@apollo/client";
|
|
744
|
+
import pluralize from "pluralize";
|
|
745
|
+
var ECSSubscription = class {
|
|
746
|
+
constructor(graphqlClient, componentDiscoverer) {
|
|
747
|
+
this.subscriptions = /* @__PURE__ */ new Map();
|
|
748
|
+
this.queryWatchers = /* @__PURE__ */ new Map();
|
|
749
|
+
this.componentDiscoverer = null;
|
|
750
|
+
this.availableComponents = [];
|
|
751
|
+
// Component primary key cache - consistent with implementation in query.ts
|
|
752
|
+
this.componentPrimaryKeys = /* @__PURE__ */ new Map();
|
|
753
|
+
this.graphqlClient = graphqlClient;
|
|
754
|
+
this.componentDiscoverer = componentDiscoverer || null;
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Set available component list
|
|
758
|
+
*/
|
|
759
|
+
setAvailableComponents(componentTypes) {
|
|
760
|
+
this.availableComponents = componentTypes;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Pre-parse and cache all component primary key information (consistent with query.ts)
|
|
764
|
+
*/
|
|
765
|
+
initializeComponentMetadata(componentMetadataList) {
|
|
766
|
+
this.componentPrimaryKeys.clear();
|
|
767
|
+
for (const metadata of componentMetadataList) {
|
|
768
|
+
if (metadata.primaryKeys.length === 1) {
|
|
769
|
+
this.componentPrimaryKeys.set(metadata.name, metadata.primaryKeys[0]);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Get component's primary key field name (quickly retrieve from cache, consistent with query.ts)
|
|
775
|
+
*/
|
|
776
|
+
getComponentPrimaryKeyField(componentType) {
|
|
777
|
+
return this.componentPrimaryKeys.get(componentType) || "entityId";
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Set component discoverer
|
|
781
|
+
*/
|
|
782
|
+
setComponentDiscoverer(discoverer) {
|
|
783
|
+
this.componentDiscoverer = discoverer;
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Validate if component type is ECS-compliant
|
|
787
|
+
*/
|
|
788
|
+
isECSComponent(componentType) {
|
|
789
|
+
return this.availableComponents.includes(componentType);
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Get component field information (intelligent parsing)
|
|
793
|
+
*/
|
|
794
|
+
async getComponentFields(componentType) {
|
|
795
|
+
if (this.componentDiscoverer) {
|
|
796
|
+
try {
|
|
797
|
+
const metadata = this.componentDiscoverer.getComponentMetadata(componentType);
|
|
798
|
+
if (metadata) {
|
|
799
|
+
return metadata.fields.map((field) => field.name);
|
|
800
|
+
}
|
|
801
|
+
} catch (error) {
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return ["createdAt", "updatedAt"];
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Get fields to use for queries (priority: user specified > dubhe config auto-parsed > default fields)
|
|
808
|
+
*/
|
|
809
|
+
async getQueryFields(componentType, userFields) {
|
|
810
|
+
if (userFields && userFields.length > 0) {
|
|
811
|
+
return userFields;
|
|
812
|
+
}
|
|
813
|
+
return this.getComponentFields(componentType);
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Listen to component added events
|
|
817
|
+
*/
|
|
818
|
+
onComponentAdded(componentType, options) {
|
|
819
|
+
if (!isValidComponentType(componentType)) {
|
|
820
|
+
return new Observable((observer) => {
|
|
821
|
+
observer.error(new Error(`Invalid component type: ${componentType}`));
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
if (!this.isECSComponent(componentType)) {
|
|
825
|
+
return new Observable((observer) => {
|
|
826
|
+
observer.error(
|
|
827
|
+
new Error(
|
|
828
|
+
`Component type ${componentType} is not ECS-compliant or not available`
|
|
829
|
+
)
|
|
830
|
+
);
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
return new Observable((observer) => {
|
|
834
|
+
let subscription = null;
|
|
835
|
+
this.getQueryFields(componentType, options?.fields).then((subscriptionFields) => {
|
|
836
|
+
const debouncedEmit = options?.debounceMs ? debounce(
|
|
837
|
+
(result) => observer.next(result),
|
|
838
|
+
options.debounceMs
|
|
839
|
+
) : (result) => observer.next(result);
|
|
840
|
+
const observable = this.graphqlClient.subscribeToTableChanges(
|
|
841
|
+
componentType,
|
|
842
|
+
{
|
|
843
|
+
initialEvent: options?.initialEvent ?? false,
|
|
844
|
+
fields: subscriptionFields,
|
|
845
|
+
onData: (data) => {
|
|
846
|
+
try {
|
|
847
|
+
const pluralTableName = this.getPluralTableName(componentType);
|
|
848
|
+
const nodes = data?.listen?.query?.[pluralTableName]?.nodes;
|
|
849
|
+
if (nodes && Array.isArray(nodes)) {
|
|
850
|
+
nodes.forEach((node) => {
|
|
851
|
+
if (node) {
|
|
852
|
+
const entityId = node.entityId || this.extractEntityId(node, componentType);
|
|
853
|
+
if (entityId) {
|
|
854
|
+
const result = {
|
|
855
|
+
entityId,
|
|
856
|
+
data: node,
|
|
857
|
+
changeType: "added",
|
|
858
|
+
timestamp: Date.now()
|
|
859
|
+
};
|
|
860
|
+
debouncedEmit(result);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
} catch (error) {
|
|
866
|
+
observer.error(error);
|
|
867
|
+
}
|
|
868
|
+
},
|
|
869
|
+
onError: (error) => {
|
|
870
|
+
observer.error(error);
|
|
871
|
+
},
|
|
872
|
+
onComplete: () => {
|
|
873
|
+
observer.complete();
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
);
|
|
877
|
+
subscription = observable.subscribe({});
|
|
878
|
+
}).catch((error) => {
|
|
879
|
+
observer.error(error);
|
|
880
|
+
});
|
|
881
|
+
return () => {
|
|
882
|
+
if (subscription) {
|
|
883
|
+
subscription.unsubscribe();
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Listen to component removed events
|
|
890
|
+
*/
|
|
891
|
+
onComponentRemoved(componentType, options) {
|
|
892
|
+
if (!isValidComponentType(componentType)) {
|
|
893
|
+
return new Observable((observer) => {
|
|
894
|
+
observer.error(new Error(`Invalid component type: ${componentType}`));
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
if (!this.isECSComponent(componentType)) {
|
|
898
|
+
return new Observable((observer) => {
|
|
899
|
+
observer.error(
|
|
900
|
+
new Error(
|
|
901
|
+
`Component type ${componentType} is not ECS-compliant or not available`
|
|
902
|
+
)
|
|
903
|
+
);
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
return new Observable((observer) => {
|
|
907
|
+
let subscription = null;
|
|
908
|
+
let lastKnownEntities = /* @__PURE__ */ new Set();
|
|
909
|
+
try {
|
|
910
|
+
const debouncedEmit = options?.debounceMs ? debounce(
|
|
911
|
+
(result) => observer.next(result),
|
|
912
|
+
options.debounceMs
|
|
913
|
+
) : (result) => observer.next(result);
|
|
914
|
+
this.initializeLastKnownEntities(componentType, lastKnownEntities);
|
|
915
|
+
const observable = this.graphqlClient.subscribeToTableChanges(
|
|
916
|
+
componentType,
|
|
917
|
+
{
|
|
918
|
+
initialEvent: false,
|
|
919
|
+
fields: ["updatedAt"],
|
|
920
|
+
// Removal detection only needs basic fields
|
|
921
|
+
onData: (data) => {
|
|
922
|
+
try {
|
|
923
|
+
const pluralTableName = this.getPluralTableName(componentType);
|
|
924
|
+
const nodes = data?.listen?.query?.[pluralTableName]?.nodes || [];
|
|
925
|
+
const currentEntities = new Set(
|
|
926
|
+
nodes.map((node) => {
|
|
927
|
+
const entityId = node.entityId || this.extractEntityId(node, componentType);
|
|
928
|
+
return entityId;
|
|
929
|
+
}).filter(Boolean)
|
|
930
|
+
);
|
|
931
|
+
const removedEntities = Array.from(lastKnownEntities).filter(
|
|
932
|
+
(entityId) => !currentEntities.has(entityId)
|
|
933
|
+
);
|
|
934
|
+
removedEntities.forEach((entityId) => {
|
|
935
|
+
const result = {
|
|
936
|
+
entityId,
|
|
937
|
+
data: null,
|
|
938
|
+
changeType: "removed",
|
|
939
|
+
timestamp: Date.now()
|
|
940
|
+
};
|
|
941
|
+
debouncedEmit(result);
|
|
942
|
+
});
|
|
943
|
+
lastKnownEntities = currentEntities;
|
|
944
|
+
} catch (error) {
|
|
945
|
+
observer.error(error);
|
|
946
|
+
}
|
|
947
|
+
},
|
|
948
|
+
onError: (error) => {
|
|
949
|
+
observer.error(error);
|
|
950
|
+
},
|
|
951
|
+
onComplete: () => {
|
|
952
|
+
observer.complete();
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
);
|
|
956
|
+
subscription = observable.subscribe({});
|
|
957
|
+
} catch (error) {
|
|
958
|
+
observer.error(error);
|
|
959
|
+
}
|
|
960
|
+
return () => {
|
|
961
|
+
if (subscription) {
|
|
962
|
+
subscription.unsubscribe();
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Listen to component changed events (added, removed, modified)
|
|
969
|
+
*/
|
|
970
|
+
onComponentChanged(componentType, options) {
|
|
971
|
+
if (!isValidComponentType(componentType)) {
|
|
972
|
+
return new Observable((observer) => {
|
|
973
|
+
observer.error(new Error(`Invalid component type: ${componentType}`));
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
if (!this.isECSComponent(componentType)) {
|
|
977
|
+
return new Observable((observer) => {
|
|
978
|
+
observer.error(
|
|
979
|
+
new Error(
|
|
980
|
+
`Component type ${componentType} is not ECS-compliant or not available`
|
|
981
|
+
)
|
|
982
|
+
);
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
return new Observable((observer) => {
|
|
986
|
+
let subscription = null;
|
|
987
|
+
this.getQueryFields(componentType, options?.fields).then((subscriptionFields) => {
|
|
988
|
+
const debouncedEmit = options?.debounceMs ? debounce(
|
|
989
|
+
(result) => observer.next(result),
|
|
990
|
+
options.debounceMs
|
|
991
|
+
) : (result) => observer.next(result);
|
|
992
|
+
const observable = this.graphqlClient.subscribeToTableChanges(
|
|
993
|
+
componentType,
|
|
994
|
+
{
|
|
995
|
+
initialEvent: options?.initialEvent ?? false,
|
|
996
|
+
fields: subscriptionFields,
|
|
997
|
+
onData: (data) => {
|
|
998
|
+
try {
|
|
999
|
+
const pluralTableName = this.getPluralTableName(componentType);
|
|
1000
|
+
const nodes = data?.listen?.query?.[pluralTableName]?.nodes;
|
|
1001
|
+
if (nodes && Array.isArray(nodes)) {
|
|
1002
|
+
nodes.forEach((node) => {
|
|
1003
|
+
if (node) {
|
|
1004
|
+
const entityId = node.entityId || this.extractEntityId(node, componentType);
|
|
1005
|
+
if (entityId) {
|
|
1006
|
+
const result = {
|
|
1007
|
+
entityId,
|
|
1008
|
+
data: node,
|
|
1009
|
+
changeType: "updated",
|
|
1010
|
+
timestamp: Date.now()
|
|
1011
|
+
};
|
|
1012
|
+
debouncedEmit(result);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
observer.error(error);
|
|
1019
|
+
}
|
|
1020
|
+
},
|
|
1021
|
+
onError: (error) => {
|
|
1022
|
+
observer.error(error);
|
|
1023
|
+
},
|
|
1024
|
+
onComplete: () => {
|
|
1025
|
+
observer.complete();
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
);
|
|
1029
|
+
subscription = observable.subscribe({});
|
|
1030
|
+
}).catch((error) => {
|
|
1031
|
+
observer.error(error);
|
|
1032
|
+
});
|
|
1033
|
+
return () => {
|
|
1034
|
+
if (subscription) {
|
|
1035
|
+
subscription.unsubscribe();
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Listen to component changes with specific conditions
|
|
1042
|
+
*/
|
|
1043
|
+
onComponentCondition(componentType, filter, options) {
|
|
1044
|
+
if (!isValidComponentType(componentType)) {
|
|
1045
|
+
return new Observable((observer) => {
|
|
1046
|
+
observer.error(new Error(`Invalid component type: ${componentType}`));
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
if (!this.isECSComponent(componentType)) {
|
|
1050
|
+
return new Observable((observer) => {
|
|
1051
|
+
observer.error(
|
|
1052
|
+
new Error(
|
|
1053
|
+
`Component type ${componentType} is not ECS-compliant or not available`
|
|
1054
|
+
)
|
|
1055
|
+
);
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
return new Observable((observer) => {
|
|
1059
|
+
let subscription = null;
|
|
1060
|
+
this.getQueryFields(componentType, options?.fields).then((subscriptionFields) => {
|
|
1061
|
+
const debouncedEmit = options?.debounceMs ? debounce(
|
|
1062
|
+
(result) => observer.next(result),
|
|
1063
|
+
options.debounceMs
|
|
1064
|
+
) : (result) => observer.next(result);
|
|
1065
|
+
const observable = this.graphqlClient.subscribeToFilteredTableChanges(
|
|
1066
|
+
componentType,
|
|
1067
|
+
filter,
|
|
1068
|
+
{
|
|
1069
|
+
initialEvent: options?.initialEvent ?? false,
|
|
1070
|
+
fields: subscriptionFields,
|
|
1071
|
+
onData: (data) => {
|
|
1072
|
+
try {
|
|
1073
|
+
const pluralTableName = this.getPluralTableName(componentType);
|
|
1074
|
+
const nodes = data?.listen?.query?.[pluralTableName]?.nodes || [];
|
|
1075
|
+
nodes.forEach((node) => {
|
|
1076
|
+
if (node) {
|
|
1077
|
+
const entityId = node.entityId || this.extractEntityId(node, componentType);
|
|
1078
|
+
if (entityId) {
|
|
1079
|
+
const result = {
|
|
1080
|
+
entityId,
|
|
1081
|
+
data: node,
|
|
1082
|
+
changeType: "updated",
|
|
1083
|
+
timestamp: Date.now()
|
|
1084
|
+
};
|
|
1085
|
+
debouncedEmit(result);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
observer.error(error);
|
|
1091
|
+
}
|
|
1092
|
+
},
|
|
1093
|
+
onError: (error) => {
|
|
1094
|
+
observer.error(error);
|
|
1095
|
+
},
|
|
1096
|
+
onComplete: () => {
|
|
1097
|
+
observer.complete();
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
);
|
|
1101
|
+
subscription = observable.subscribe({});
|
|
1102
|
+
}).catch((error) => {
|
|
1103
|
+
observer.error(error);
|
|
1104
|
+
});
|
|
1105
|
+
return () => {
|
|
1106
|
+
if (subscription) {
|
|
1107
|
+
subscription.unsubscribe();
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Listen to query result changes
|
|
1114
|
+
*/
|
|
1115
|
+
watchQuery(componentTypes, options) {
|
|
1116
|
+
const validTypes = componentTypes.filter(isValidComponentType);
|
|
1117
|
+
if (validTypes.length === 0) {
|
|
1118
|
+
return new Observable((observer) => {
|
|
1119
|
+
observer.error(
|
|
1120
|
+
new Error("No valid component types for query watching")
|
|
1121
|
+
);
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
return new Observable((observer) => {
|
|
1125
|
+
const watcher = new QueryWatcherImpl(
|
|
1126
|
+
this.graphqlClient,
|
|
1127
|
+
validTypes,
|
|
1128
|
+
(changes) => {
|
|
1129
|
+
const result = {
|
|
1130
|
+
changes
|
|
1131
|
+
};
|
|
1132
|
+
if (options?.debounceMs) {
|
|
1133
|
+
const debouncedEmit = debounce(
|
|
1134
|
+
() => observer.next(result),
|
|
1135
|
+
options.debounceMs
|
|
1136
|
+
);
|
|
1137
|
+
debouncedEmit();
|
|
1138
|
+
} else {
|
|
1139
|
+
observer.next(result);
|
|
1140
|
+
}
|
|
1141
|
+
},
|
|
1142
|
+
options
|
|
1143
|
+
);
|
|
1144
|
+
return () => {
|
|
1145
|
+
watcher.dispose();
|
|
1146
|
+
};
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Create real-time data stream
|
|
1151
|
+
*/
|
|
1152
|
+
createRealTimeStream(componentType, initialFilter) {
|
|
1153
|
+
if (!isValidComponentType(componentType)) {
|
|
1154
|
+
return new Observable(
|
|
1155
|
+
(observer) => {
|
|
1156
|
+
observer.error(new Error(`Invalid component type: ${componentType}`));
|
|
1157
|
+
}
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
return new Observable(
|
|
1161
|
+
(observer) => {
|
|
1162
|
+
try {
|
|
1163
|
+
const subscription = this.graphqlClient.createRealTimeDataStream(
|
|
1164
|
+
componentType,
|
|
1165
|
+
{ filter: initialFilter }
|
|
1166
|
+
);
|
|
1167
|
+
const streamSubscription = subscription.subscribe({
|
|
1168
|
+
next: (connection) => {
|
|
1169
|
+
const results = connection.edges.map((edge) => {
|
|
1170
|
+
const node = edge.node;
|
|
1171
|
+
const entityId = node.nodeId || node.entityId || Object.values(node)[0] || "";
|
|
1172
|
+
return {
|
|
1173
|
+
entityId,
|
|
1174
|
+
data: node
|
|
1175
|
+
};
|
|
1176
|
+
}).filter((result) => result.entityId);
|
|
1177
|
+
observer.next(results);
|
|
1178
|
+
},
|
|
1179
|
+
error: (error) => {
|
|
1180
|
+
observer.error(error);
|
|
1181
|
+
},
|
|
1182
|
+
complete: () => {
|
|
1183
|
+
observer.complete();
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
return () => {
|
|
1187
|
+
streamSubscription.unsubscribe();
|
|
1188
|
+
};
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
observer.error(error);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Initialize known entity list (for deletion detection)
|
|
1197
|
+
*/
|
|
1198
|
+
async initializeLastKnownEntities(componentType, lastKnownEntities) {
|
|
1199
|
+
try {
|
|
1200
|
+
const connection = await this.graphqlClient.getAllTables(componentType, {
|
|
1201
|
+
fields: ["updatedAt"]
|
|
1202
|
+
});
|
|
1203
|
+
connection.edges.forEach((edge) => {
|
|
1204
|
+
const node = edge.node;
|
|
1205
|
+
const entityId = node.nodeId || node.entityId || Object.values(node)[0];
|
|
1206
|
+
if (entityId) {
|
|
1207
|
+
lastKnownEntities.add(entityId);
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Convert singular table name to plural form (using pluralize library for correctness)
|
|
1215
|
+
*/
|
|
1216
|
+
getPluralTableName(tableName) {
|
|
1217
|
+
const camelCaseName = this.toCamelCase(tableName);
|
|
1218
|
+
return pluralize.plural(camelCaseName);
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Convert snake_case to camelCase
|
|
1222
|
+
*/
|
|
1223
|
+
toCamelCase(str) {
|
|
1224
|
+
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Extract entity ID from node (using component's primary key field information)
|
|
1228
|
+
*/
|
|
1229
|
+
extractEntityId(node, componentType) {
|
|
1230
|
+
if (!node || typeof node !== "object") {
|
|
1231
|
+
return "";
|
|
1232
|
+
}
|
|
1233
|
+
const primaryKeyField = this.getComponentPrimaryKeyField(componentType);
|
|
1234
|
+
if (node[primaryKeyField] && typeof node[primaryKeyField] === "string") {
|
|
1235
|
+
return node[primaryKeyField];
|
|
1236
|
+
}
|
|
1237
|
+
if (primaryKeyField !== "entityId" && node.entityId && typeof node.entityId === "string") {
|
|
1238
|
+
return node.entityId;
|
|
1239
|
+
}
|
|
1240
|
+
const values = Object.values(node);
|
|
1241
|
+
for (const value of values) {
|
|
1242
|
+
if (typeof value === "string" && value.length > 0) {
|
|
1243
|
+
return value;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return "";
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Unsubscribe all subscriptions
|
|
1250
|
+
*/
|
|
1251
|
+
unsubscribeAll() {
|
|
1252
|
+
this.subscriptions.forEach((subscription) => {
|
|
1253
|
+
try {
|
|
1254
|
+
subscription?.unsubscribe();
|
|
1255
|
+
} catch (error) {
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
this.subscriptions.clear();
|
|
1259
|
+
this.queryWatchers.forEach((watcher) => {
|
|
1260
|
+
try {
|
|
1261
|
+
watcher.dispose();
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
this.queryWatchers.clear();
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Dispose resources
|
|
1269
|
+
*/
|
|
1270
|
+
dispose() {
|
|
1271
|
+
this.unsubscribeAll();
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
var QueryWatcherImpl = class {
|
|
1275
|
+
constructor(graphqlClient, componentTypes, callback, options) {
|
|
1276
|
+
this.subscriptions = [];
|
|
1277
|
+
this.currentResults = [];
|
|
1278
|
+
this.isInitialized = false;
|
|
1279
|
+
this.graphqlClient = graphqlClient;
|
|
1280
|
+
this.componentTypes = componentTypes;
|
|
1281
|
+
this.callback = callback;
|
|
1282
|
+
this.options = options;
|
|
1283
|
+
this.initialize();
|
|
1284
|
+
}
|
|
1285
|
+
async initialize() {
|
|
1286
|
+
try {
|
|
1287
|
+
await this.updateCurrentResults();
|
|
1288
|
+
this.componentTypes.forEach((componentType) => {
|
|
1289
|
+
const observable = this.graphqlClient.subscribeToTableChanges(
|
|
1290
|
+
componentType,
|
|
1291
|
+
{
|
|
1292
|
+
initialEvent: false,
|
|
1293
|
+
onData: () => {
|
|
1294
|
+
this.handleDataChange();
|
|
1295
|
+
},
|
|
1296
|
+
onError: (error) => {
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
);
|
|
1300
|
+
const actualSubscription = observable.subscribe({});
|
|
1301
|
+
this.subscriptions.push(actualSubscription);
|
|
1302
|
+
});
|
|
1303
|
+
this.isInitialized = true;
|
|
1304
|
+
if (this.options?.initialEvent && this.currentResults.length > 0) {
|
|
1305
|
+
this.callback({
|
|
1306
|
+
added: this.currentResults,
|
|
1307
|
+
removed: [],
|
|
1308
|
+
current: this.currentResults
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
async handleDataChange() {
|
|
1315
|
+
if (!this.isInitialized)
|
|
1316
|
+
return;
|
|
1317
|
+
try {
|
|
1318
|
+
const oldResults = [...this.currentResults];
|
|
1319
|
+
await this.updateCurrentResults();
|
|
1320
|
+
const changes = calculateDelta(oldResults, this.currentResults);
|
|
1321
|
+
if (changes.added.length > 0 || changes.removed.length > 0) {
|
|
1322
|
+
const debouncedCallback = this.options?.debounceMs ? debounce(this.callback, this.options.debounceMs) : this.callback;
|
|
1323
|
+
debouncedCallback(changes);
|
|
1324
|
+
}
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
async updateCurrentResults() {
|
|
1329
|
+
try {
|
|
1330
|
+
if (this.componentTypes.length === 1) {
|
|
1331
|
+
const connection = await this.graphqlClient.getAllTables(
|
|
1332
|
+
this.componentTypes[0],
|
|
1333
|
+
{ fields: ["updatedAt"] }
|
|
1334
|
+
);
|
|
1335
|
+
this.currentResults = connection.edges.map((edge) => {
|
|
1336
|
+
const node = edge.node;
|
|
1337
|
+
return node.nodeId || node.entityId || Object.values(node)[0] || "";
|
|
1338
|
+
}).filter(Boolean);
|
|
1339
|
+
} else {
|
|
1340
|
+
const queries = this.componentTypes.map((type) => ({
|
|
1341
|
+
key: type,
|
|
1342
|
+
tableName: type,
|
|
1343
|
+
params: {
|
|
1344
|
+
fields: ["updatedAt"],
|
|
1345
|
+
filter: {}
|
|
1346
|
+
}
|
|
1347
|
+
}));
|
|
1348
|
+
const batchResult = await this.graphqlClient.batchQuery(queries);
|
|
1349
|
+
const entitySets = this.componentTypes.map((type) => {
|
|
1350
|
+
const connection = batchResult[type];
|
|
1351
|
+
return connection ? connection.edges.map((edge) => {
|
|
1352
|
+
const node = edge.node;
|
|
1353
|
+
return node.nodeId || node.entityId || Object.values(node)[0] || "";
|
|
1354
|
+
}).filter(Boolean) : [];
|
|
1355
|
+
});
|
|
1356
|
+
this.currentResults = entitySets.reduce((intersection, currentSet) => {
|
|
1357
|
+
const currentSetLookup = new Set(currentSet);
|
|
1358
|
+
return intersection.filter((id) => currentSetLookup.has(id));
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
this.currentResults = [];
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
getCurrentResults() {
|
|
1366
|
+
return [...this.currentResults];
|
|
1367
|
+
}
|
|
1368
|
+
dispose() {
|
|
1369
|
+
this.subscriptions.forEach((subscription) => {
|
|
1370
|
+
try {
|
|
1371
|
+
subscription?.unsubscribe();
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
this.subscriptions = [];
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
// src/world.ts
|
|
1380
|
+
var ComponentDiscoverer = class {
|
|
1381
|
+
constructor(graphqlClient, dubheMetadata) {
|
|
1382
|
+
this.componentMetadataMap = /* @__PURE__ */ new Map();
|
|
1383
|
+
this.componentTypes = [];
|
|
1384
|
+
this.graphqlClient = graphqlClient;
|
|
1385
|
+
this.dubheMetadata = dubheMetadata;
|
|
1386
|
+
const components = [];
|
|
1387
|
+
const errors = [];
|
|
1388
|
+
this.parseFromDubheMetadata(components, errors);
|
|
1389
|
+
const result = {
|
|
1390
|
+
components,
|
|
1391
|
+
discoveredAt: Date.now(),
|
|
1392
|
+
errors: errors.length > 0 ? errors : void 0,
|
|
1393
|
+
totalDiscovered: components.length,
|
|
1394
|
+
fromDubheMetadata: true
|
|
1395
|
+
};
|
|
1396
|
+
this.discoveryResult = result;
|
|
1397
|
+
this.componentTypes = components.map((comp) => comp.name);
|
|
1398
|
+
components.forEach((comp) => {
|
|
1399
|
+
this.componentMetadataMap.set(comp.name, comp);
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Parse components from DubheMetadata JSON format
|
|
1404
|
+
*/
|
|
1405
|
+
parseFromDubheMetadata(components, errors) {
|
|
1406
|
+
if (!this.dubheMetadata?.components) {
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
for (const componentRecord of this.dubheMetadata.components) {
|
|
1410
|
+
for (const [componentName, componentConfig] of Object.entries(
|
|
1411
|
+
componentRecord
|
|
1412
|
+
)) {
|
|
1413
|
+
const componentType = this.tableNameToComponentName(componentName);
|
|
1414
|
+
try {
|
|
1415
|
+
const fields = [];
|
|
1416
|
+
const primaryKeys = [];
|
|
1417
|
+
const enumFields = [];
|
|
1418
|
+
if (componentConfig.fields && Array.isArray(componentConfig.fields)) {
|
|
1419
|
+
for (const fieldRecord of componentConfig.fields) {
|
|
1420
|
+
for (const [fieldName, fieldType] of Object.entries(
|
|
1421
|
+
fieldRecord
|
|
1422
|
+
)) {
|
|
1423
|
+
const camelFieldName = this.snakeToCamel(fieldName);
|
|
1424
|
+
const typeStr = String(fieldType);
|
|
1425
|
+
const isCustomKey = componentConfig.keys && componentConfig.keys.includes(fieldName);
|
|
1426
|
+
fields.push({
|
|
1427
|
+
name: camelFieldName,
|
|
1428
|
+
type: this.dubheTypeToGraphQLType(typeStr),
|
|
1429
|
+
nullable: !isCustomKey,
|
|
1430
|
+
isPrimaryKey: isCustomKey,
|
|
1431
|
+
isEnum: this.isEnumType(typeStr)
|
|
1432
|
+
});
|
|
1433
|
+
if (isCustomKey) {
|
|
1434
|
+
primaryKeys.push(camelFieldName);
|
|
1435
|
+
}
|
|
1436
|
+
if (this.isEnumType(typeStr)) {
|
|
1437
|
+
enumFields.push(camelFieldName);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (primaryKeys.length === 0) {
|
|
1443
|
+
fields.unshift({
|
|
1444
|
+
name: "entityId",
|
|
1445
|
+
type: "String",
|
|
1446
|
+
nullable: false,
|
|
1447
|
+
isPrimaryKey: true,
|
|
1448
|
+
isEnum: false
|
|
1449
|
+
});
|
|
1450
|
+
primaryKeys.push("entityId");
|
|
1451
|
+
}
|
|
1452
|
+
fields.push(
|
|
1453
|
+
{
|
|
1454
|
+
name: "createdAt",
|
|
1455
|
+
type: "String",
|
|
1456
|
+
nullable: false,
|
|
1457
|
+
isPrimaryKey: false,
|
|
1458
|
+
isEnum: false
|
|
1459
|
+
},
|
|
1460
|
+
{
|
|
1461
|
+
name: "updatedAt",
|
|
1462
|
+
type: "String",
|
|
1463
|
+
nullable: false,
|
|
1464
|
+
isPrimaryKey: false,
|
|
1465
|
+
isEnum: false
|
|
1466
|
+
}
|
|
1467
|
+
);
|
|
1468
|
+
if (primaryKeys.length !== 1) {
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
const metadata = {
|
|
1472
|
+
name: componentType,
|
|
1473
|
+
tableName: componentName,
|
|
1474
|
+
fields,
|
|
1475
|
+
primaryKeys,
|
|
1476
|
+
hasDefaultId: primaryKeys.includes("entityId"),
|
|
1477
|
+
enumFields,
|
|
1478
|
+
lastUpdated: Date.now(),
|
|
1479
|
+
description: `Auto-discovered component: ${componentName}`
|
|
1480
|
+
};
|
|
1481
|
+
components.push(metadata);
|
|
1482
|
+
} catch (error) {
|
|
1483
|
+
const errorMsg = `Component ${componentType} validation failed: ${formatError(error)}`;
|
|
1484
|
+
errors.push(errorMsg);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
getComponentTypes() {
|
|
1490
|
+
return this.componentTypes;
|
|
1491
|
+
}
|
|
1492
|
+
getComponentMetadata(componentType) {
|
|
1493
|
+
return this.componentMetadataMap.get(componentType) || null;
|
|
1494
|
+
}
|
|
1495
|
+
snakeToCamel(str) {
|
|
1496
|
+
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
1497
|
+
}
|
|
1498
|
+
dubheTypeToGraphQLType(dubheType) {
|
|
1499
|
+
if (dubheType.startsWith("vector<") && dubheType.endsWith(">")) {
|
|
1500
|
+
return "String";
|
|
1501
|
+
}
|
|
1502
|
+
switch (dubheType) {
|
|
1503
|
+
case "u8":
|
|
1504
|
+
case "u16":
|
|
1505
|
+
case "u32":
|
|
1506
|
+
case "u64":
|
|
1507
|
+
case "u128":
|
|
1508
|
+
case "i8":
|
|
1509
|
+
case "i16":
|
|
1510
|
+
case "i32":
|
|
1511
|
+
case "i64":
|
|
1512
|
+
case "i128":
|
|
1513
|
+
return "Int";
|
|
1514
|
+
case "address":
|
|
1515
|
+
case "string":
|
|
1516
|
+
return "String";
|
|
1517
|
+
case "bool":
|
|
1518
|
+
return "Boolean";
|
|
1519
|
+
case "enum":
|
|
1520
|
+
return "String";
|
|
1521
|
+
default:
|
|
1522
|
+
return "String";
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
componentNameToTableName(componentName) {
|
|
1526
|
+
if (!componentName.endsWith("s")) {
|
|
1527
|
+
return componentName + "s";
|
|
1528
|
+
}
|
|
1529
|
+
return componentName;
|
|
1530
|
+
}
|
|
1531
|
+
tableNameToComponentName(tableName) {
|
|
1532
|
+
if (tableName.endsWith("s") && tableName.length > 1) {
|
|
1533
|
+
return tableName.slice(0, -1);
|
|
1534
|
+
}
|
|
1535
|
+
return tableName;
|
|
1536
|
+
}
|
|
1537
|
+
isEnumType(typeStr) {
|
|
1538
|
+
return this.dubheMetadata.enums.some(
|
|
1539
|
+
(enumDef) => typeof enumDef === "object" && enumDef[typeStr]
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
var ResourceDiscoverer = class {
|
|
1544
|
+
constructor(graphqlClient, dubheMetadata) {
|
|
1545
|
+
this.resourceMetadataMap = /* @__PURE__ */ new Map();
|
|
1546
|
+
this.resourceTypes = [];
|
|
1547
|
+
this.graphqlClient = graphqlClient;
|
|
1548
|
+
this.dubheMetadata = dubheMetadata;
|
|
1549
|
+
const resources = [];
|
|
1550
|
+
const errors = [];
|
|
1551
|
+
this.parseFromDubheMetadata(resources, errors);
|
|
1552
|
+
const result = {
|
|
1553
|
+
resources,
|
|
1554
|
+
discoveredAt: Date.now(),
|
|
1555
|
+
errors: errors.length > 0 ? errors : void 0,
|
|
1556
|
+
totalDiscovered: resources.length,
|
|
1557
|
+
fromDubheMetadata: true
|
|
1558
|
+
};
|
|
1559
|
+
this.discoveryResult = result;
|
|
1560
|
+
this.resourceTypes = resources.map((res) => res.name);
|
|
1561
|
+
resources.forEach((res) => {
|
|
1562
|
+
this.resourceMetadataMap.set(res.name, res);
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Parse resources from DubheMetadata JSON format
|
|
1567
|
+
*/
|
|
1568
|
+
parseFromDubheMetadata(resources, errors) {
|
|
1569
|
+
if (!this.dubheMetadata?.resources) {
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
for (const resourceRecord of this.dubheMetadata.resources) {
|
|
1573
|
+
for (const [resourceName, resourceConfig] of Object.entries(
|
|
1574
|
+
resourceRecord
|
|
1575
|
+
)) {
|
|
1576
|
+
try {
|
|
1577
|
+
const fields = [];
|
|
1578
|
+
const primaryKeys = [];
|
|
1579
|
+
const enumFields = [];
|
|
1580
|
+
if (resourceConfig.fields && Array.isArray(resourceConfig.fields)) {
|
|
1581
|
+
for (const fieldRecord of resourceConfig.fields) {
|
|
1582
|
+
for (const [fieldName, fieldType] of Object.entries(
|
|
1583
|
+
fieldRecord
|
|
1584
|
+
)) {
|
|
1585
|
+
const camelFieldName = this.snakeToCamel(fieldName);
|
|
1586
|
+
const typeStr = String(fieldType);
|
|
1587
|
+
const isCustomKey = resourceConfig.keys && resourceConfig.keys.includes(fieldName);
|
|
1588
|
+
fields.push({
|
|
1589
|
+
name: camelFieldName,
|
|
1590
|
+
type: this.dubheTypeToGraphQLType(typeStr),
|
|
1591
|
+
nullable: !isCustomKey,
|
|
1592
|
+
isPrimaryKey: isCustomKey,
|
|
1593
|
+
isEnum: this.isEnumType(typeStr)
|
|
1594
|
+
});
|
|
1595
|
+
if (isCustomKey) {
|
|
1596
|
+
primaryKeys.push(camelFieldName);
|
|
1597
|
+
}
|
|
1598
|
+
if (this.isEnumType(typeStr)) {
|
|
1599
|
+
enumFields.push(camelFieldName);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
fields.push(
|
|
1605
|
+
{
|
|
1606
|
+
name: "createdAt",
|
|
1607
|
+
type: "String",
|
|
1608
|
+
nullable: false,
|
|
1609
|
+
isPrimaryKey: false,
|
|
1610
|
+
isEnum: false
|
|
1611
|
+
},
|
|
1612
|
+
{
|
|
1613
|
+
name: "updatedAt",
|
|
1614
|
+
type: "String",
|
|
1615
|
+
nullable: false,
|
|
1616
|
+
isPrimaryKey: false,
|
|
1617
|
+
isEnum: false
|
|
1618
|
+
}
|
|
1619
|
+
);
|
|
1620
|
+
const resourceType = resourceName;
|
|
1621
|
+
const metadata = {
|
|
1622
|
+
name: resourceType,
|
|
1623
|
+
tableName: resourceName,
|
|
1624
|
+
fields,
|
|
1625
|
+
primaryKeys,
|
|
1626
|
+
hasCompositeKeys: primaryKeys.length > 1,
|
|
1627
|
+
hasNoKeys: primaryKeys.length === 0,
|
|
1628
|
+
enumFields,
|
|
1629
|
+
lastUpdated: Date.now(),
|
|
1630
|
+
description: `Auto-discovered resource: ${resourceName}`
|
|
1631
|
+
};
|
|
1632
|
+
resources.push(metadata);
|
|
1633
|
+
} catch (error) {
|
|
1634
|
+
const errorMsg = `Resource ${resourceName} validation failed: ${formatError(error)}`;
|
|
1635
|
+
errors.push(errorMsg);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
getResourceTypes() {
|
|
1641
|
+
return this.resourceTypes;
|
|
1642
|
+
}
|
|
1643
|
+
getResourceMetadata(resourceType) {
|
|
1644
|
+
return this.resourceMetadataMap.get(resourceType) || null;
|
|
1645
|
+
}
|
|
1646
|
+
snakeToCamel(str) {
|
|
1647
|
+
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
1648
|
+
}
|
|
1649
|
+
dubheTypeToGraphQLType(dubheType) {
|
|
1650
|
+
if (dubheType.startsWith("vector<") && dubheType.endsWith(">")) {
|
|
1651
|
+
return "String";
|
|
1652
|
+
}
|
|
1653
|
+
switch (dubheType) {
|
|
1654
|
+
case "u8":
|
|
1655
|
+
case "u16":
|
|
1656
|
+
case "u32":
|
|
1657
|
+
case "u64":
|
|
1658
|
+
case "u128":
|
|
1659
|
+
case "i8":
|
|
1660
|
+
case "i16":
|
|
1661
|
+
case "i32":
|
|
1662
|
+
case "i64":
|
|
1663
|
+
case "i128":
|
|
1664
|
+
return "Int";
|
|
1665
|
+
case "address":
|
|
1666
|
+
case "string":
|
|
1667
|
+
return "String";
|
|
1668
|
+
case "bool":
|
|
1669
|
+
return "Boolean";
|
|
1670
|
+
case "enum":
|
|
1671
|
+
return "String";
|
|
1672
|
+
default:
|
|
1673
|
+
return "String";
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
isEnumType(typeStr) {
|
|
1677
|
+
return this.dubheMetadata.enums.some(
|
|
1678
|
+
(enumDef) => typeof enumDef === "object" && enumDef[typeStr]
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
};
|
|
1682
|
+
var DubheECSWorld = class {
|
|
1683
|
+
constructor(graphqlClient, config) {
|
|
1684
|
+
this.graphqlClient = graphqlClient;
|
|
1685
|
+
this.config = {
|
|
1686
|
+
queryConfig: {
|
|
1687
|
+
defaultCacheTimeout: 5 * 60 * 1e3,
|
|
1688
|
+
maxConcurrentQueries: 10,
|
|
1689
|
+
enableBatchOptimization: true
|
|
1690
|
+
},
|
|
1691
|
+
subscriptionConfig: {
|
|
1692
|
+
defaultDebounceMs: 100,
|
|
1693
|
+
maxSubscriptions: 50,
|
|
1694
|
+
reconnectOnError: true
|
|
1695
|
+
},
|
|
1696
|
+
...config
|
|
1697
|
+
};
|
|
1698
|
+
let dubheMetadata = this.config.dubheMetadata;
|
|
1699
|
+
if (!dubheMetadata) {
|
|
1700
|
+
dubheMetadata = this.graphqlClient.getDubheMetadata();
|
|
1701
|
+
if (!dubheMetadata) {
|
|
1702
|
+
throw new Error(
|
|
1703
|
+
"DubheMetadata is required for ECS World initialization. Please provide it either in ECSWorldConfig or in GraphQL client configuration."
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
this.dubheMetadata = dubheMetadata;
|
|
1708
|
+
this.componentDiscoverer = new ComponentDiscoverer(
|
|
1709
|
+
graphqlClient,
|
|
1710
|
+
this.dubheMetadata
|
|
1711
|
+
);
|
|
1712
|
+
this.resourceDiscoverer = new ResourceDiscoverer(
|
|
1713
|
+
graphqlClient,
|
|
1714
|
+
this.dubheMetadata
|
|
1715
|
+
);
|
|
1716
|
+
this.querySystem = new ECSQuery(graphqlClient, this.componentDiscoverer);
|
|
1717
|
+
this.subscriptionSystem = new ECSSubscription(
|
|
1718
|
+
graphqlClient,
|
|
1719
|
+
this.componentDiscoverer
|
|
1720
|
+
);
|
|
1721
|
+
this.initializeWithConfig();
|
|
1722
|
+
}
|
|
1723
|
+
initializeWithConfig() {
|
|
1724
|
+
try {
|
|
1725
|
+
const ecsComponents = this.componentDiscoverer.discoveryResult.components.filter((comp) => {
|
|
1726
|
+
return comp.primaryKeys.length === 1;
|
|
1727
|
+
});
|
|
1728
|
+
const resources = this.resourceDiscoverer.discoveryResult.resources;
|
|
1729
|
+
this.querySystem.setAvailableComponents(
|
|
1730
|
+
ecsComponents.map((comp) => comp.name)
|
|
1731
|
+
);
|
|
1732
|
+
this.querySystem.initializeComponentMetadata(
|
|
1733
|
+
ecsComponents.map((comp) => ({
|
|
1734
|
+
name: comp.name,
|
|
1735
|
+
primaryKeys: comp.primaryKeys
|
|
1736
|
+
}))
|
|
1737
|
+
);
|
|
1738
|
+
this.subscriptionSystem.setAvailableComponents(
|
|
1739
|
+
ecsComponents.map((comp) => comp.name)
|
|
1740
|
+
);
|
|
1741
|
+
this.subscriptionSystem.initializeComponentMetadata(
|
|
1742
|
+
ecsComponents.map((comp) => ({
|
|
1743
|
+
name: comp.name,
|
|
1744
|
+
primaryKeys: comp.primaryKeys
|
|
1745
|
+
}))
|
|
1746
|
+
);
|
|
1747
|
+
if (this.config.queryConfig) {
|
|
1748
|
+
}
|
|
1749
|
+
if (this.config.subscriptionConfig) {
|
|
1750
|
+
}
|
|
1751
|
+
} catch (error) {
|
|
1752
|
+
throw new Error(`Failed to initialize ECS World: ${formatError(error)}`);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
// ============ Configuration and Initialization ============
|
|
1756
|
+
/**
|
|
1757
|
+
* Configure ECS world
|
|
1758
|
+
*/
|
|
1759
|
+
configure(config) {
|
|
1760
|
+
this.config = { ...this.config, ...config };
|
|
1761
|
+
if (config.dubheMetadata) {
|
|
1762
|
+
this.dubheMetadata = config.dubheMetadata;
|
|
1763
|
+
this.componentDiscoverer = new ComponentDiscoverer(
|
|
1764
|
+
this.graphqlClient,
|
|
1765
|
+
this.dubheMetadata
|
|
1766
|
+
);
|
|
1767
|
+
this.resourceDiscoverer = new ResourceDiscoverer(
|
|
1768
|
+
this.graphqlClient,
|
|
1769
|
+
this.dubheMetadata
|
|
1770
|
+
);
|
|
1771
|
+
this.querySystem.setComponentDiscoverer(this.componentDiscoverer);
|
|
1772
|
+
this.subscriptionSystem.setComponentDiscoverer(this.componentDiscoverer);
|
|
1773
|
+
this.initializeWithConfig();
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
// ============ Component Discovery and Information ============
|
|
1777
|
+
/**
|
|
1778
|
+
* Discover and return all available ECS component types
|
|
1779
|
+
*/
|
|
1780
|
+
discoverComponents() {
|
|
1781
|
+
return this.componentDiscoverer.getComponentTypes();
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Get all available ECS component types (cached)
|
|
1785
|
+
*/
|
|
1786
|
+
getAvailableComponents() {
|
|
1787
|
+
return this.componentDiscoverer.getComponentTypes();
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* Get metadata for a specific ECS component
|
|
1791
|
+
*/
|
|
1792
|
+
getComponentMetadata(componentType) {
|
|
1793
|
+
return this.componentDiscoverer.getComponentMetadata(componentType);
|
|
1794
|
+
}
|
|
1795
|
+
// ============ Resource Discovery and Information ============
|
|
1796
|
+
/**
|
|
1797
|
+
* Get all available resource types
|
|
1798
|
+
*/
|
|
1799
|
+
getAvailableResources() {
|
|
1800
|
+
return this.resourceDiscoverer.getResourceTypes();
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Get metadata for a specific resource
|
|
1804
|
+
*/
|
|
1805
|
+
getResourceMetadata(resourceType) {
|
|
1806
|
+
return this.resourceDiscoverer.getResourceMetadata(resourceType);
|
|
1807
|
+
}
|
|
1808
|
+
// ============ Entity Queries ============
|
|
1809
|
+
/**
|
|
1810
|
+
* Check if entity exists
|
|
1811
|
+
*/
|
|
1812
|
+
async hasEntity(entityId) {
|
|
1813
|
+
return this.querySystem.hasEntity(entityId);
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Get all entity IDs
|
|
1817
|
+
*/
|
|
1818
|
+
async getAllEntities() {
|
|
1819
|
+
return this.querySystem.getAllEntities();
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Get entity count
|
|
1823
|
+
*/
|
|
1824
|
+
async getEntityCount() {
|
|
1825
|
+
return this.querySystem.getEntityCount();
|
|
1826
|
+
}
|
|
1827
|
+
// ============ Standard ECS Interface (camelCase naming) ============
|
|
1828
|
+
/**
|
|
1829
|
+
* Get complete data of a single entity
|
|
1830
|
+
* @param entityId Entity ID
|
|
1831
|
+
* @returns Complete component data of the entity, or null if entity doesn't exist
|
|
1832
|
+
*/
|
|
1833
|
+
async getEntity(entityId) {
|
|
1834
|
+
try {
|
|
1835
|
+
const exists = await this.hasEntity(entityId);
|
|
1836
|
+
if (!exists) {
|
|
1837
|
+
return null;
|
|
1838
|
+
}
|
|
1839
|
+
const componentTypes = await this.getComponents(entityId);
|
|
1840
|
+
if (componentTypes.length === 0) {
|
|
1841
|
+
return null;
|
|
1842
|
+
}
|
|
1843
|
+
const entityData = {
|
|
1844
|
+
entityId,
|
|
1845
|
+
components: {}
|
|
1846
|
+
};
|
|
1847
|
+
for (const componentType of componentTypes) {
|
|
1848
|
+
const componentData = await this.getComponent(entityId, componentType);
|
|
1849
|
+
if (componentData) {
|
|
1850
|
+
entityData.components[componentType] = componentData;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
return entityData;
|
|
1854
|
+
} catch (error) {
|
|
1855
|
+
console.error(`Failed to get entity ${entityId}:`, formatError(error));
|
|
1856
|
+
return null;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* Get all entity ID list
|
|
1861
|
+
* @returns Array of all entity IDs
|
|
1862
|
+
*/
|
|
1863
|
+
async getEntities() {
|
|
1864
|
+
return this.getAllEntities();
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* Get all entities that have a specific component
|
|
1868
|
+
* @param componentType Component type
|
|
1869
|
+
* @returns Array of entity IDs that have this component
|
|
1870
|
+
*/
|
|
1871
|
+
async getEntitiesByComponent(componentType) {
|
|
1872
|
+
return this.queryWith(componentType);
|
|
1873
|
+
}
|
|
1874
|
+
// Note: getComponent, getComponents, hasComponent methods are defined below
|
|
1875
|
+
// ============ Component Queries ============
|
|
1876
|
+
/**
|
|
1877
|
+
* Check if entity has specific component
|
|
1878
|
+
*/
|
|
1879
|
+
async hasComponent(entityId, componentType) {
|
|
1880
|
+
return this.querySystem.hasComponent(entityId, componentType);
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Get specific component data of entity
|
|
1884
|
+
*/
|
|
1885
|
+
async getComponent(entityId, componentType) {
|
|
1886
|
+
return this.querySystem.getComponent(entityId, componentType);
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Get all component types that entity has
|
|
1890
|
+
*/
|
|
1891
|
+
async getComponents(entityId) {
|
|
1892
|
+
return this.querySystem.getComponents(entityId);
|
|
1893
|
+
}
|
|
1894
|
+
// ============ World Queries ============
|
|
1895
|
+
/**
|
|
1896
|
+
* Query all entities that have a specific component
|
|
1897
|
+
*/
|
|
1898
|
+
async queryWith(componentType, options) {
|
|
1899
|
+
return this.querySystem.queryWith(componentType, options);
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Query entities that have all specified components (intersection)
|
|
1903
|
+
*/
|
|
1904
|
+
async queryWithAll(componentTypes, options) {
|
|
1905
|
+
return this.querySystem.queryWithAll(componentTypes, options);
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Query entities that have any of the specified components (union)
|
|
1909
|
+
*/
|
|
1910
|
+
async queryWithAny(componentTypes, options) {
|
|
1911
|
+
return this.querySystem.queryWithAny(componentTypes, options);
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Query entities that have include components but not exclude components
|
|
1915
|
+
*/
|
|
1916
|
+
async queryWithout(includeTypes, excludeTypes, options) {
|
|
1917
|
+
return this.querySystem.queryWithout(includeTypes, excludeTypes, options);
|
|
1918
|
+
}
|
|
1919
|
+
// ============ Conditional Queries ============
|
|
1920
|
+
/**
|
|
1921
|
+
* Query components based on conditions
|
|
1922
|
+
*/
|
|
1923
|
+
async queryWhere(componentType, predicate, options) {
|
|
1924
|
+
return this.querySystem.queryWhere(componentType, predicate, options);
|
|
1925
|
+
}
|
|
1926
|
+
/**
|
|
1927
|
+
* Range query
|
|
1928
|
+
*/
|
|
1929
|
+
async queryRange(componentType, field, min, max, options) {
|
|
1930
|
+
return this.querySystem.queryRange(componentType, field, min, max, options);
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Paginated query
|
|
1934
|
+
*/
|
|
1935
|
+
async queryPaged(componentTypes, page, pageSize) {
|
|
1936
|
+
return this.querySystem.queryPaged(componentTypes, page, pageSize);
|
|
1937
|
+
}
|
|
1938
|
+
// ============ Query Builder ============
|
|
1939
|
+
/**
|
|
1940
|
+
* Create query builder
|
|
1941
|
+
*/
|
|
1942
|
+
query() {
|
|
1943
|
+
return this.querySystem.query();
|
|
1944
|
+
}
|
|
1945
|
+
// ============ Subscription System ============
|
|
1946
|
+
/**
|
|
1947
|
+
* Listen to component added events
|
|
1948
|
+
*/
|
|
1949
|
+
onComponentAdded(componentType, options) {
|
|
1950
|
+
return this.subscriptionSystem.onComponentAdded(componentType, options);
|
|
1951
|
+
}
|
|
1952
|
+
/**
|
|
1953
|
+
* Listen to component removed events
|
|
1954
|
+
*/
|
|
1955
|
+
onComponentRemoved(componentType, options) {
|
|
1956
|
+
return this.subscriptionSystem.onComponentRemoved(
|
|
1957
|
+
componentType,
|
|
1958
|
+
options
|
|
1959
|
+
);
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Listen to component changed events
|
|
1963
|
+
*/
|
|
1964
|
+
onComponentChanged(componentType, options) {
|
|
1965
|
+
return this.subscriptionSystem.onComponentChanged(
|
|
1966
|
+
componentType,
|
|
1967
|
+
options
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* Listen to component changes with specific conditions
|
|
1972
|
+
*/
|
|
1973
|
+
onComponentCondition(componentType, filter, options) {
|
|
1974
|
+
return this.subscriptionSystem.onComponentCondition(
|
|
1975
|
+
componentType,
|
|
1976
|
+
filter,
|
|
1977
|
+
options
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Listen to query result changes
|
|
1982
|
+
*/
|
|
1983
|
+
watchQuery(componentTypes, options) {
|
|
1984
|
+
return this.subscriptionSystem.watchQuery(componentTypes, options);
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* Create real-time data stream
|
|
1988
|
+
*/
|
|
1989
|
+
createRealTimeStream(componentType, initialFilter) {
|
|
1990
|
+
return this.subscriptionSystem.createRealTimeStream(
|
|
1991
|
+
componentType,
|
|
1992
|
+
initialFilter
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
// ============ Convenience Methods ============
|
|
1996
|
+
/**
|
|
1997
|
+
* Query entity data with specific component (includes component data)
|
|
1998
|
+
*/
|
|
1999
|
+
async queryWithComponentData(componentType, options) {
|
|
2000
|
+
try {
|
|
2001
|
+
const entityIds = await this.queryWith(componentType, options);
|
|
2002
|
+
const results = [];
|
|
2003
|
+
for (const entityId of entityIds) {
|
|
2004
|
+
const componentData = await this.getComponent(
|
|
2005
|
+
entityId,
|
|
2006
|
+
componentType
|
|
2007
|
+
);
|
|
2008
|
+
if (componentData) {
|
|
2009
|
+
results.push({ entityId, data: componentData });
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
return results;
|
|
2013
|
+
} catch (error) {
|
|
2014
|
+
return [];
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
/**
|
|
2018
|
+
* Query multi-component entity data
|
|
2019
|
+
*/
|
|
2020
|
+
async queryMultiComponentData(component1Type, component2Type, options) {
|
|
2021
|
+
try {
|
|
2022
|
+
const entityIds = await this.queryWithAll(
|
|
2023
|
+
[component1Type, component2Type],
|
|
2024
|
+
options
|
|
2025
|
+
);
|
|
2026
|
+
const results = [];
|
|
2027
|
+
for (const entityId of entityIds) {
|
|
2028
|
+
const [data1, data2] = await Promise.all([
|
|
2029
|
+
this.getComponent(entityId, component1Type),
|
|
2030
|
+
this.getComponent(entityId, component2Type)
|
|
2031
|
+
]);
|
|
2032
|
+
if (data1 && data2) {
|
|
2033
|
+
results.push({ entityId, data1, data2 });
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
return results;
|
|
2037
|
+
} catch (error) {
|
|
2038
|
+
return [];
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Get complete entity state (all component data)
|
|
2043
|
+
*/
|
|
2044
|
+
async getEntityState(entityId) {
|
|
2045
|
+
try {
|
|
2046
|
+
const componentTypes = await this.getComponents(entityId);
|
|
2047
|
+
if (componentTypes.length === 0)
|
|
2048
|
+
return null;
|
|
2049
|
+
const components = {};
|
|
2050
|
+
for (const componentType of componentTypes) {
|
|
2051
|
+
const componentData = await this.getComponent(entityId, componentType);
|
|
2052
|
+
if (componentData) {
|
|
2053
|
+
components[componentType] = componentData;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
return { entityId, components };
|
|
2057
|
+
} catch (error) {
|
|
2058
|
+
return null;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
// ============ Statistics and Analysis ============
|
|
2062
|
+
/**
|
|
2063
|
+
* Get component statistics
|
|
2064
|
+
*/
|
|
2065
|
+
async getComponentStats() {
|
|
2066
|
+
try {
|
|
2067
|
+
const stats = {};
|
|
2068
|
+
const componentTypes = await this.getAvailableComponents();
|
|
2069
|
+
await Promise.all(
|
|
2070
|
+
componentTypes.map(async (componentType) => {
|
|
2071
|
+
try {
|
|
2072
|
+
const entities = await this.queryWith(componentType);
|
|
2073
|
+
stats[componentType] = entities.length;
|
|
2074
|
+
} catch (error) {
|
|
2075
|
+
stats[componentType] = 0;
|
|
2076
|
+
}
|
|
2077
|
+
})
|
|
2078
|
+
);
|
|
2079
|
+
return stats;
|
|
2080
|
+
} catch (error) {
|
|
2081
|
+
return {};
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Find orphan entities (entities with only one component)
|
|
2086
|
+
*/
|
|
2087
|
+
async findOrphanEntities() {
|
|
2088
|
+
try {
|
|
2089
|
+
const allEntities = await this.getAllEntities();
|
|
2090
|
+
const orphanEntities = [];
|
|
2091
|
+
for (const entityId of allEntities) {
|
|
2092
|
+
const components = await this.getComponents(entityId);
|
|
2093
|
+
if (components.length === 1) {
|
|
2094
|
+
orphanEntities.push(entityId);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
return orphanEntities;
|
|
2098
|
+
} catch (error) {
|
|
2099
|
+
return [];
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
// ============ Resource Management ============
|
|
2103
|
+
/**
|
|
2104
|
+
* Unsubscribe all subscriptions
|
|
2105
|
+
*/
|
|
2106
|
+
unsubscribeAll() {
|
|
2107
|
+
this.subscriptionSystem.unsubscribeAll();
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* Clear all caches
|
|
2111
|
+
*/
|
|
2112
|
+
clearCache() {
|
|
2113
|
+
this.querySystem.dispose();
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* Dispose resources
|
|
2117
|
+
*/
|
|
2118
|
+
dispose() {
|
|
2119
|
+
this.querySystem.dispose();
|
|
2120
|
+
this.subscriptionSystem.dispose();
|
|
2121
|
+
}
|
|
2122
|
+
// ============ Get Underlying Clients ============
|
|
2123
|
+
/**
|
|
2124
|
+
* Get GraphQL client (for advanced operations)
|
|
2125
|
+
*/
|
|
2126
|
+
getGraphQLClient() {
|
|
2127
|
+
return this.graphqlClient;
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
2130
|
+
* Get query system (for advanced query operations)
|
|
2131
|
+
*/
|
|
2132
|
+
getQuerySystem() {
|
|
2133
|
+
return this.querySystem;
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* Get subscription system (for advanced subscription operations)
|
|
2137
|
+
*/
|
|
2138
|
+
getSubscriptionSystem() {
|
|
2139
|
+
return this.subscriptionSystem;
|
|
2140
|
+
}
|
|
2141
|
+
/**
|
|
2142
|
+
* Get ECS world configuration
|
|
2143
|
+
*/
|
|
2144
|
+
getConfig() {
|
|
2145
|
+
return { ...this.config };
|
|
2146
|
+
}
|
|
2147
|
+
/**
|
|
2148
|
+
* Get dubhe metadata info (JSON format)
|
|
2149
|
+
*/
|
|
2150
|
+
getDubheMetadata() {
|
|
2151
|
+
return this.dubheMetadata;
|
|
2152
|
+
}
|
|
2153
|
+
// ============ Resource Queries ============
|
|
2154
|
+
/**
|
|
2155
|
+
* Query resource by primary keys
|
|
2156
|
+
*/
|
|
2157
|
+
async getResource(resourceType, keyValues, options) {
|
|
2158
|
+
try {
|
|
2159
|
+
const resourceMetadata = this.resourceDiscoverer.getResourceMetadata(resourceType);
|
|
2160
|
+
if (!resourceMetadata) {
|
|
2161
|
+
return null;
|
|
2162
|
+
}
|
|
2163
|
+
keyValues = keyValues || {};
|
|
2164
|
+
const whereConditions = {};
|
|
2165
|
+
for (const [key, value] of Object.entries(keyValues)) {
|
|
2166
|
+
whereConditions[key] = { equalTo: value };
|
|
2167
|
+
}
|
|
2168
|
+
const result = await this.graphqlClient.getAllTables(resourceType, {
|
|
2169
|
+
first: 1,
|
|
2170
|
+
filter: whereConditions,
|
|
2171
|
+
fields: options?.fields || resourceMetadata.fields.map((f) => f.name),
|
|
2172
|
+
...options
|
|
2173
|
+
});
|
|
2174
|
+
const record = result.edges[0]?.node;
|
|
2175
|
+
return record ? record : null;
|
|
2176
|
+
} catch (error) {
|
|
2177
|
+
return null;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Query multiple resources
|
|
2182
|
+
*/
|
|
2183
|
+
async getResources(resourceType, filters, options) {
|
|
2184
|
+
try {
|
|
2185
|
+
const resourceMetadata = this.resourceDiscoverer.getResourceMetadata(resourceType);
|
|
2186
|
+
if (!resourceMetadata) {
|
|
2187
|
+
return [];
|
|
2188
|
+
}
|
|
2189
|
+
const whereConditions = {};
|
|
2190
|
+
if (filters) {
|
|
2191
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
2192
|
+
if (typeof value === "object" && value !== null) {
|
|
2193
|
+
whereConditions[key] = value;
|
|
2194
|
+
} else {
|
|
2195
|
+
whereConditions[key] = { equalTo: value };
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
const result = await this.graphqlClient.getAllTables(resourceType, {
|
|
2200
|
+
filter: Object.keys(whereConditions).length > 0 ? whereConditions : void 0,
|
|
2201
|
+
fields: options?.fields || resourceMetadata.fields.map((f) => f.name),
|
|
2202
|
+
...options
|
|
2203
|
+
});
|
|
2204
|
+
const records = result.edges.map((edge) => edge.node);
|
|
2205
|
+
return records;
|
|
2206
|
+
} catch (error) {
|
|
2207
|
+
return [];
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* Get all resources of a specific type
|
|
2212
|
+
*/
|
|
2213
|
+
async getAllResources(resourceType, options) {
|
|
2214
|
+
return this.getResources(resourceType, void 0, options);
|
|
2215
|
+
}
|
|
2216
|
+
/**
|
|
2217
|
+
* Check if a resource exists
|
|
2218
|
+
*/
|
|
2219
|
+
async hasResource(resourceType, keyValues) {
|
|
2220
|
+
const resource = await this.getResource(resourceType, keyValues);
|
|
2221
|
+
return resource !== null;
|
|
2222
|
+
}
|
|
2223
|
+
/**
|
|
2224
|
+
* Get resource count
|
|
2225
|
+
*/
|
|
2226
|
+
async getResourceCount(resourceType) {
|
|
2227
|
+
try {
|
|
2228
|
+
const result = await this.graphqlClient.getAllTables(resourceType, {
|
|
2229
|
+
first: 1
|
|
2230
|
+
// Only need count, not actual data
|
|
2231
|
+
});
|
|
2232
|
+
return result.totalCount || 0;
|
|
2233
|
+
} catch (error) {
|
|
2234
|
+
return 0;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Subscribe to resource changes
|
|
2239
|
+
*/
|
|
2240
|
+
subscribeToResourceChanges(resourceType, options) {
|
|
2241
|
+
const resourceMetadata = this.resourceDiscoverer.getResourceMetadata(resourceType);
|
|
2242
|
+
if (!resourceMetadata) {
|
|
2243
|
+
throw new Error(
|
|
2244
|
+
`Unknown resource type: ${resourceType}. Available resources: [${this.getAvailableResources().join(", ")}]`
|
|
2245
|
+
);
|
|
2246
|
+
}
|
|
2247
|
+
const subscriptionFields = options?.fields || resourceMetadata.fields.map((f) => f.name);
|
|
2248
|
+
return this.graphqlClient.subscribeToFilteredTableChanges(
|
|
2249
|
+
resourceType,
|
|
2250
|
+
options?.filter,
|
|
2251
|
+
{
|
|
2252
|
+
...options,
|
|
2253
|
+
fields: subscriptionFields
|
|
2254
|
+
}
|
|
2255
|
+
);
|
|
2256
|
+
}
|
|
2257
|
+
};
|
|
2258
|
+
function createECSWorld(graphqlClient, config) {
|
|
2259
|
+
return new DubheECSWorld(graphqlClient, config);
|
|
2260
|
+
}
|
|
2261
|
+
export {
|
|
2262
|
+
ComponentDiscoverer,
|
|
2263
|
+
DubheECSWorld,
|
|
2264
|
+
ECSQuery,
|
|
2265
|
+
ECSSubscription,
|
|
2266
|
+
ResourceDiscoverer,
|
|
2267
|
+
calculateDelta,
|
|
2268
|
+
createCacheKey,
|
|
2269
|
+
createECSWorld,
|
|
2270
|
+
createTimestamp,
|
|
2271
|
+
debounce,
|
|
2272
|
+
deepEqual,
|
|
2273
|
+
DubheECSWorld as default,
|
|
2274
|
+
extractEntityIds,
|
|
2275
|
+
extractIntersectionFromBatchResult,
|
|
2276
|
+
extractUnionFromBatchResult,
|
|
2277
|
+
findEntityIntersection,
|
|
2278
|
+
findEntityUnion,
|
|
2279
|
+
formatError,
|
|
2280
|
+
isValidComponentType,
|
|
2281
|
+
isValidEntityId,
|
|
2282
|
+
limitArray,
|
|
2283
|
+
normalizeComponentType,
|
|
2284
|
+
paginateArray,
|
|
2285
|
+
safeJsonParse
|
|
2286
|
+
};
|
|
2287
|
+
//# sourceMappingURL=index.mjs.map
|