@0xobelisk/ecs 1.2.0-pre.100

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/src/types.ts ADDED
@@ -0,0 +1,209 @@
1
+ export type DubheMetadata = {
2
+ components: Array<
3
+ Record<
4
+ string,
5
+ {
6
+ fields: Array<Record<string, any>>;
7
+ keys: string[];
8
+ offchain?: boolean;
9
+ }
10
+ >
11
+ >;
12
+ resources: Array<
13
+ Record<
14
+ string,
15
+ {
16
+ fields: Array<Record<string, any>>;
17
+ keys: string[];
18
+ offchain?: boolean;
19
+ }
20
+ >
21
+ >;
22
+ enums: any[];
23
+ };
24
+
25
+ // ECS type definitions
26
+
27
+ export type EntityId = string;
28
+ export type ComponentType = string;
29
+
30
+ // Unsubscribe function
31
+ export type Unsubscribe = () => void;
32
+
33
+ // Component callback functions
34
+ export type ComponentCallback<T> = (entityId: EntityId, component: T) => void;
35
+ export type ComponentChangeCallback<T> = (
36
+ entityId: EntityId,
37
+ oldComponent: T,
38
+ newComponent: T
39
+ ) => void;
40
+ export type EntityCallback = (entityId: EntityId) => void;
41
+
42
+ // Query change results
43
+ export interface QueryChange {
44
+ added: EntityId[]; // Newly matched entities
45
+ removed: EntityId[]; // Entities that no longer match
46
+ current: EntityId[]; // All currently matched entities
47
+ }
48
+
49
+ export type QueryChangeCallback = (changes: QueryChange) => void;
50
+
51
+ // Query watcher
52
+ export interface QueryWatcher {
53
+ unsubscribe: Unsubscribe;
54
+ getCurrentResults: () => EntityId[];
55
+ }
56
+
57
+ // Paginated query results (legacy)
58
+ export interface PagedResult<T = EntityId> {
59
+ items: T[];
60
+ totalCount: number;
61
+ hasMore: boolean;
62
+ page: number;
63
+ pageSize: number;
64
+ }
65
+
66
+ // Complete paginated query results with GraphQL connection info
67
+ export interface PagedQueryResult<T = any> {
68
+ // Entity IDs for ECS queries
69
+ entityIds: EntityId[];
70
+ // Actual data items (can be component data, resource data, etc.)
71
+ items: T[];
72
+ // GraphQL pagination info
73
+ pageInfo: {
74
+ hasNextPage: boolean;
75
+ hasPreviousPage: boolean;
76
+ startCursor?: string;
77
+ endCursor?: string;
78
+ };
79
+ // Total count of items
80
+ totalCount: number;
81
+ }
82
+
83
+ // Batch query results
84
+ export interface BatchQueryResult {
85
+ [componentType: string]: EntityId[];
86
+ }
87
+
88
+ // Component change events
89
+ export interface ComponentChangeEvent<T = any> {
90
+ entityId: EntityId;
91
+ componentType: ComponentType;
92
+ changeType: 'ADDED' | 'REMOVED' | 'MODIFIED';
93
+ oldValue?: T;
94
+ newValue?: T;
95
+ timestamp: number;
96
+ }
97
+
98
+ // Entity change events
99
+ export interface EntityChangeEvent {
100
+ entityId: EntityId;
101
+ changeType: 'CREATED' | 'DESTROYED';
102
+ componentTypes: ComponentType[];
103
+ timestamp: number;
104
+ }
105
+
106
+ // Query options
107
+ export interface QueryOptions {
108
+ fields?: string[]; // Field names to query
109
+ idFields?: string[]; // Field names to use as entity ID, defaults to ['nodeId', 'entityId']
110
+ compositeId?: boolean; // Whether to compose multiple fields as ID, defaults to false
111
+
112
+ // GraphQL pagination parameters (aligned with GraphQL client)
113
+ first?: number; // Get first N records (replaces limit)
114
+ last?: number; // Get last N records
115
+ after?: string; // Cursor-based pagination start position
116
+ before?: string; // Cursor-based pagination end position
117
+
118
+ // Legacy pagination parameters (for backward compatibility, will be converted)
119
+ limit?: number; // Will be mapped to first
120
+ offset?: number; // Will be ignored with warning (use cursor-based pagination instead)
121
+
122
+ filters?: Record<string, any>;
123
+
124
+ orderBy?: Array<{
125
+ field: string;
126
+ direction: 'ASC' | 'DESC';
127
+ }>;
128
+ cache?: boolean;
129
+ }
130
+
131
+ // Subscription options
132
+ export interface SubscriptionOptions {
133
+ initialEvent?: boolean;
134
+ debounceMs?: number;
135
+ filter?: Record<string, any>;
136
+ }
137
+
138
+ // Component metadata
139
+ export interface ComponentMetadata {
140
+ name: ComponentType;
141
+ tableName: string; // Corresponding database table name
142
+ description?: string; // Component description
143
+ fields: ComponentField[]; // Field information
144
+ primaryKeys: string[]; // Primary key field list
145
+ hasDefaultId: boolean; // Whether has default ID field
146
+ enumFields: string[]; // Enum field list
147
+ lastUpdated: number; // Last updated timestamp
148
+ }
149
+
150
+ // Component field information
151
+ export interface ComponentField {
152
+ name: string;
153
+ type: string; // GraphQL type
154
+ nullable: boolean;
155
+ description?: string;
156
+ isEnum?: boolean; // Whether is enum field
157
+ isPrimaryKey?: boolean; // Whether is primary key field
158
+ }
159
+
160
+ // Component discovery results
161
+ export interface ComponentDiscoveryResult {
162
+ components: ComponentMetadata[];
163
+ discoveredAt: number;
164
+ errors?: string[];
165
+ totalDiscovered?: number; // Total number of discovered components
166
+ fromDubheMetadata?: boolean; // Whether from dubhe metadata
167
+ }
168
+
169
+ // Resource metadata
170
+ export interface ResourceMetadata {
171
+ name: string;
172
+ tableName: string; // Corresponding database table name
173
+ description?: string; // Resource description
174
+ fields: ComponentField[]; // Field information
175
+ primaryKeys: string[]; // Primary key field list
176
+ hasCompositeKeys: boolean; // Whether has composite primary keys
177
+ hasNoKeys: boolean; // Whether has no primary keys
178
+ enumFields: string[]; // Enum field list
179
+ lastUpdated: number; // Last updated timestamp
180
+ }
181
+
182
+ // Resource discovery results
183
+ export interface ResourceDiscoveryResult {
184
+ resources: ResourceMetadata[];
185
+ discoveredAt: number;
186
+ errors?: string[];
187
+ totalDiscovered?: number; // Total number of discovered resources
188
+ fromDubheMetadata?: boolean; // Whether from dubhe metadata
189
+ }
190
+
191
+ // ECS world configuration
192
+ export interface ECSWorldConfig {
193
+ // Dubhe Metadata (JSON format, optional - if not provided, gets from GraphQL client)
194
+ dubheMetadata?: DubheMetadata;
195
+
196
+ // Query configuration
197
+ queryConfig?: {
198
+ defaultCacheTimeout?: number; // Default cache timeout
199
+ maxConcurrentQueries?: number; // Maximum concurrent queries
200
+ enableBatchOptimization?: boolean; // Enable batch query optimization
201
+ };
202
+
203
+ // Subscription configuration
204
+ subscriptionConfig?: {
205
+ defaultDebounceMs?: number; // Default debounce time
206
+ maxSubscriptions?: number; // Maximum subscriptions
207
+ reconnectOnError?: boolean; // Auto reconnect on error
208
+ };
209
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,309 @@
1
+ // ECS utility functions
2
+
3
+ import { EntityId, ComponentType, QueryChange, PagedQueryResult } from './types';
4
+ import { Connection, StoreTableRow } from '@0xobelisk/graphql-client';
5
+
6
+ /**
7
+ * Extract entity IDs from GraphQL query results
8
+ * @param connection GraphQL query result
9
+ * @param options Extraction options
10
+ * @param options.idFields Field names to use as entity ID, defaults to ['nodeId', 'entityId']
11
+ * @param options.composite Whether to compose multiple fields as ID, defaults to false
12
+ */
13
+ export function extractEntityIds<T extends StoreTableRow>(
14
+ connection: Connection<T>,
15
+ options?: {
16
+ idFields?: string[];
17
+ composite?: boolean;
18
+ }
19
+ ): EntityId[] {
20
+ const { idFields = ['nodeId', 'entityId'], composite = false } = options || {};
21
+
22
+ return connection.edges
23
+ .map((edge) => {
24
+ const node = edge.node as any;
25
+
26
+ if (composite) {
27
+ // Compose multiple fields as ID
28
+ const idParts = idFields.map((field) => node[field] || '').filter(Boolean);
29
+ return idParts.join('|'); // Use | separator to compose
30
+ } else {
31
+ // Try to find the first existing field as ID
32
+ for (const field of idFields) {
33
+ if (node[field] !== undefined && node[field] !== null) {
34
+ return node[field] as EntityId;
35
+ }
36
+ }
37
+
38
+ // If none found, return the first available value or empty string
39
+ return (Object.values(node)[0] as EntityId) || '';
40
+ }
41
+ })
42
+ .filter(Boolean); // Filter out empty values
43
+ }
44
+
45
+ /**
46
+ * Extract complete paginated query result from GraphQL connection
47
+ * @param connection GraphQL query result
48
+ * @param options Extraction options
49
+ * @param options.idFields Field names to use as entity ID, defaults to ['nodeId', 'entityId']
50
+ * @param options.composite Whether to compose multiple fields as ID, defaults to false
51
+ */
52
+ export function extractPagedQueryResult<T extends StoreTableRow>(
53
+ connection: Connection<T>,
54
+ options?: {
55
+ idFields?: string[];
56
+ composite?: boolean;
57
+ }
58
+ ): PagedQueryResult<T> {
59
+ const entityIds = extractEntityIds(connection, options);
60
+ const items = connection.edges.map((edge) => edge.node);
61
+
62
+ return {
63
+ entityIds,
64
+ items,
65
+ pageInfo: {
66
+ hasNextPage: connection.pageInfo.hasNextPage,
67
+ hasPreviousPage: connection.pageInfo.hasPreviousPage,
68
+ startCursor: connection.pageInfo.startCursor,
69
+ endCursor: connection.pageInfo.endCursor
70
+ },
71
+ totalCount: connection.totalCount || 0
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Calculate differences between two entity ID arrays
77
+ */
78
+ export function calculateDelta(oldResults: EntityId[], newResults: EntityId[]): QueryChange {
79
+ const oldSet = new Set(oldResults);
80
+ const newSet = new Set(newResults);
81
+
82
+ const added = newResults.filter((id) => !oldSet.has(id));
83
+ const removed = oldResults.filter((id) => !newSet.has(id));
84
+
85
+ return {
86
+ added,
87
+ removed,
88
+ current: newResults
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Find intersection of multiple entity ID arrays
94
+ */
95
+ export function findEntityIntersection(entitySets: EntityId[][]): EntityId[] {
96
+ if (entitySets.length === 0) return [];
97
+ if (entitySets.length === 1) return entitySets[0];
98
+
99
+ return entitySets.reduce((intersection, currentSet) => {
100
+ const currentSetLookup = new Set(currentSet);
101
+ return intersection.filter((id) => currentSetLookup.has(id));
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Find union of multiple entity ID arrays
107
+ */
108
+ export function findEntityUnion(entitySets: EntityId[][]): EntityId[] {
109
+ const unionSet = new Set<EntityId>();
110
+
111
+ entitySets.forEach((set) => {
112
+ set.forEach((id) => unionSet.add(id));
113
+ });
114
+
115
+ return Array.from(unionSet);
116
+ }
117
+
118
+ /**
119
+ * Extract entity intersection from batch query results
120
+ */
121
+ export function extractIntersectionFromBatchResult(
122
+ batchResult: Record<string, Connection<StoreTableRow>>,
123
+ componentTypes: ComponentType[],
124
+ options?: {
125
+ idFields?: string[];
126
+ composite?: boolean;
127
+ }
128
+ ): EntityId[] {
129
+ const entitySets = componentTypes.map((type) => {
130
+ const connection = batchResult[type];
131
+ return connection ? extractEntityIds(connection, options) : [];
132
+ });
133
+
134
+ return findEntityIntersection(entitySets);
135
+ }
136
+
137
+ /**
138
+ * Extract entity union from batch query results
139
+ */
140
+ export function extractUnionFromBatchResult(
141
+ batchResult: Record<string, Connection<StoreTableRow>>,
142
+ componentTypes: ComponentType[],
143
+ options?: {
144
+ idFields?: string[];
145
+ composite?: boolean;
146
+ }
147
+ ): EntityId[] {
148
+ const entitySets = componentTypes.map((type) => {
149
+ const connection = batchResult[type];
150
+ return connection ? extractEntityIds(connection, options) : [];
151
+ });
152
+
153
+ return findEntityUnion(entitySets);
154
+ }
155
+
156
+ /**
157
+ * Debounce function
158
+ */
159
+ export function debounce<T extends (...args: any[]) => any>(
160
+ func: T,
161
+ waitMs: number
162
+ ): (...args: Parameters<T>) => void {
163
+ let timeoutId: NodeJS.Timeout | null = null;
164
+
165
+ return (...args: Parameters<T>) => {
166
+ if (timeoutId) {
167
+ clearTimeout(timeoutId);
168
+ }
169
+
170
+ timeoutId = setTimeout(() => {
171
+ func(...args);
172
+ timeoutId = null;
173
+ }, waitMs);
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Normalize component type name (handle singular/plural)
179
+ */
180
+ export function normalizeComponentType(componentType: ComponentType): {
181
+ singular: string;
182
+ plural: string;
183
+ } {
184
+ // Simple singular/plural conversion logic
185
+ const singular = componentType.endsWith('s') ? componentType.slice(0, -1) : componentType;
186
+
187
+ const plural = componentType.endsWith('s') ? componentType : componentType + 's';
188
+
189
+ return { singular, plural };
190
+ }
191
+
192
+ /**
193
+ * Create cache key
194
+ */
195
+ export function createCacheKey(
196
+ operation: string,
197
+ componentTypes: ComponentType[],
198
+ options?: Record<string, any>
199
+ ): string {
200
+ const sortedTypes = [...componentTypes].sort();
201
+ const optionsStr = options ? JSON.stringify(options) : '';
202
+ return `${operation}:${sortedTypes.join(',')}:${optionsStr}`;
203
+ }
204
+
205
+ /**
206
+ * Validate entity ID format
207
+ */
208
+ export function isValidEntityId(entityId: any): entityId is EntityId {
209
+ return typeof entityId === 'string' && entityId.length > 0;
210
+ }
211
+
212
+ /**
213
+ * Validate component type format
214
+ */
215
+ export function isValidComponentType(componentType: any): componentType is ComponentType {
216
+ return typeof componentType === 'string' && componentType.length > 0;
217
+ }
218
+
219
+ /**
220
+ * Deep compare two objects for equality
221
+ */
222
+ export function deepEqual(obj1: any, obj2: any): boolean {
223
+ if (obj1 === obj2) return true;
224
+
225
+ if (obj1 == null || obj2 == null) return false;
226
+
227
+ if (typeof obj1 !== typeof obj2) return false;
228
+
229
+ if (typeof obj1 !== 'object') return false;
230
+
231
+ const keys1 = Object.keys(obj1);
232
+ const keys2 = Object.keys(obj2);
233
+
234
+ if (keys1.length !== keys2.length) return false;
235
+
236
+ for (const key of keys1) {
237
+ if (!keys2.includes(key)) return false;
238
+ if (!deepEqual(obj1[key], obj2[key])) return false;
239
+ }
240
+
241
+ return true;
242
+ }
243
+
244
+ /**
245
+ * Safely parse JSON
246
+ */
247
+ export function safeJsonParse<T = any>(json: string, defaultValue: T): T {
248
+ try {
249
+ return JSON.parse(json);
250
+ } catch {
251
+ return defaultValue;
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Format error message
257
+ */
258
+ export function formatError(error: any): string {
259
+ if (error instanceof Error) {
260
+ return error.message;
261
+ }
262
+
263
+ if (typeof error === 'string') {
264
+ return error;
265
+ }
266
+
267
+ return JSON.stringify(error);
268
+ }
269
+
270
+ /**
271
+ * Create timestamp
272
+ */
273
+ export function createTimestamp(): number {
274
+ return Date.now();
275
+ }
276
+
277
+ /**
278
+ * Limit array size
279
+ */
280
+ export function limitArray<T>(array: T[], limit: number): T[] {
281
+ return limit > 0 ? array.slice(0, limit) : array;
282
+ }
283
+
284
+ /**
285
+ * Paginate array
286
+ */
287
+ export function paginateArray<T>(
288
+ array: T[],
289
+ page: number,
290
+ pageSize: number
291
+ ): {
292
+ items: T[];
293
+ totalCount: number;
294
+ hasMore: boolean;
295
+ page: number;
296
+ pageSize: number;
297
+ } {
298
+ const startIndex = (page - 1) * pageSize;
299
+ const endIndex = startIndex + pageSize;
300
+ const items = array.slice(startIndex, endIndex);
301
+
302
+ return {
303
+ items,
304
+ totalCount: array.length,
305
+ hasMore: endIndex < array.length,
306
+ page,
307
+ pageSize
308
+ };
309
+ }