@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/LICENSE +92 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +2413 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2353 -0
- package/dist/index.mjs.map +1 -0
- package/dist/query.d.ts +164 -0
- package/dist/subscription.d.ts +123 -0
- package/dist/types.d.ts +142 -0
- package/dist/utils.d.ts +103 -0
- package/dist/world.d.ts +306 -0
- package/package.json +99 -0
- package/src/index.ts +60 -0
- package/src/query.ts +839 -0
- package/src/subscription.ts +848 -0
- package/src/types.ts +209 -0
- package/src/utils.ts +309 -0
- package/src/world.ts +1282 -0
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
|
+
}
|