ti2 1.0.99 → 1.0.102
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/.windsurfrules +9 -0
- package/CLAUDE.md +159 -0
- package/__tests__/cache.js +76 -0
- package/cache.js +21 -0
- package/controllers/__tests__/bookings-searchProducts.js +2 -2
- package/controllers/__tests__/bookings.js +82 -0
- package/controllers/bookings.js +39 -40
- package/index.js +1 -0
- package/package.json +1 -1
package/.windsurfrules
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
This is a Node.js project using Sequelize for database operations and Express.js for the web server.
|
|
2
|
+
The API is defined in api.yml using OpenAPI 3.0.2
|
|
3
|
+
The database is defined in models/index.js
|
|
4
|
+
The models are defined in models/
|
|
5
|
+
to run tests you have to run them on the ti2 named docker container, and consider this codebase is part of node_modules/ti2 path for example :
|
|
6
|
+
`` $ docker exec ti2 bash -c "cd /ti2 && npx jest lib/__tests__/callback.js --forceExit"``
|
|
7
|
+
When writing / editing tests, make use of test/utils.js and review their implementation to understand how to mock the environment.
|
|
8
|
+
Do not make any changes until you 95% confident of the changes you want to make, ask follow up questions until you have that confidence.
|
|
9
|
+
when sending jobs to the background worker using the queue, asssume a worker is always runing to execute it but is on a different instance thread, so to review / wait for jobs we have to use the wokrer/queue.js listJobs fn to check for it
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
Ti2 (Tourism Information Interchange) is an open-source integration framework for the tourism industry. It provides standardized functions for bookings, content, and rates management through a plugin-based architecture.
|
|
8
|
+
|
|
9
|
+
## Critical Development Rules
|
|
10
|
+
|
|
11
|
+
1. **Docker Container Execution**: ALL commands must be run inside the `ti2` Docker container
|
|
12
|
+
- The codebase is located at `/ti2` path inside the container
|
|
13
|
+
- This project is part of `node_modules/ti2` in the container environment
|
|
14
|
+
- **NEVER run npm, node, or npx commands on the host machine**
|
|
15
|
+
- Always use: `docker exec ti2 bash -c "cd /ti2 && <command>"`
|
|
16
|
+
- Example for npm install: `docker exec ti2 bash -c "cd /ti2 && npm install <package>"`
|
|
17
|
+
|
|
18
|
+
2. **Testing Rules**:
|
|
19
|
+
- Tests MUST be run inside the Docker container
|
|
20
|
+
- Example: `docker exec ti2 bash -c "cd /ti2 && npx jest lib/__tests__/callback.js --forceExit"`
|
|
21
|
+
- Use `test/utils.js` for mocking the environment
|
|
22
|
+
- Review test utility implementations before writing/editing tests
|
|
23
|
+
|
|
24
|
+
3. **Background Jobs**:
|
|
25
|
+
- Jobs sent to the queue run on a separate worker thread/instance
|
|
26
|
+
- Use `worker/queue.js` `listJobs` function to check job status
|
|
27
|
+
- Workers are always assumed to be running
|
|
28
|
+
|
|
29
|
+
4. **Code Confidence**: Do not make changes until 95% confident. Ask follow-up questions if uncertain.
|
|
30
|
+
|
|
31
|
+
## Common Development Commands
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Run tests (inside Docker container)
|
|
35
|
+
docker exec ti2 bash -c "cd /ti2 && npm test"
|
|
36
|
+
docker exec ti2 bash -c "cd /ti2 && npx jest [test-file] --forceExit"
|
|
37
|
+
|
|
38
|
+
# Run specific test file
|
|
39
|
+
docker exec ti2 bash -c "cd /ti2 && npx jest controllers/__tests__/bookings.js --forceExit"
|
|
40
|
+
|
|
41
|
+
# Database migrations
|
|
42
|
+
docker exec ti2 bash -c "cd /ti2 && npm run sequelize db:migrate"
|
|
43
|
+
|
|
44
|
+
# Generate documentation
|
|
45
|
+
docker exec ti2 bash -c "cd /ti2 && npm run doc"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Architecture
|
|
49
|
+
|
|
50
|
+
### Core Components
|
|
51
|
+
|
|
52
|
+
1. **Main Entry Point** (`index.js`):
|
|
53
|
+
- Initializes Express server on port 10010 (default)
|
|
54
|
+
- Sets up Swagger documentation at `/api-docs`
|
|
55
|
+
- Handles plugin instantiation and middleware
|
|
56
|
+
- Implements caching mechanism
|
|
57
|
+
- Manages background job processing via `worker/queue.js`
|
|
58
|
+
|
|
59
|
+
2. **API Definition** (`api.yml`):
|
|
60
|
+
- OpenAPI 3.0.0 specification
|
|
61
|
+
- Defines all endpoints and schemas
|
|
62
|
+
- JWT Bearer authentication
|
|
63
|
+
|
|
64
|
+
3. **Plugin System**:
|
|
65
|
+
- Two types: Integration Plugins (connect systems) and App Plugins (value-added tools)
|
|
66
|
+
- Plugins are instantiated with cache, axios, events, and configuration
|
|
67
|
+
- Plugin schemas are merged with main API schema
|
|
68
|
+
- Each plugin gets its own routes under `/apps/{pluginName}`
|
|
69
|
+
|
|
70
|
+
4. **Controllers** (`controllers/`):
|
|
71
|
+
- `app.js`: Core application controller
|
|
72
|
+
- `bookings.js`: Booking operations (searchProducts, createBooking, etc.)
|
|
73
|
+
- `admin.js`: Administrative functions
|
|
74
|
+
- `user.js`: User management
|
|
75
|
+
- `ping.js`: Health check endpoints
|
|
76
|
+
- `allotment.js`: Allotment management
|
|
77
|
+
|
|
78
|
+
5. **Database Models** (`models/`):
|
|
79
|
+
- Sequelize ORM with MySQL
|
|
80
|
+
- Key models: Integration, User, UserAppKey, CronJobs, ApiCronJobs
|
|
81
|
+
- Migrations in `migrations/` directory
|
|
82
|
+
|
|
83
|
+
6. **Worker System** (`worker/`):
|
|
84
|
+
- Bull queue for background job processing
|
|
85
|
+
- Redis-backed job queue
|
|
86
|
+
- Supports plugin jobs, API jobs, and callback jobs
|
|
87
|
+
- Multi-worker support with throng
|
|
88
|
+
|
|
89
|
+
7. **Authentication** (`auth/authHandler.js`):
|
|
90
|
+
- JWT-based authentication
|
|
91
|
+
- Bearer token scheme
|
|
92
|
+
|
|
93
|
+
### Key Features
|
|
94
|
+
|
|
95
|
+
- **Caching**: Redis-based caching with configurable TTL
|
|
96
|
+
- **Event System**: EventEmitter2 for plugin communication
|
|
97
|
+
- **Background Jobs**: Async job processing with Bull/Redis
|
|
98
|
+
- **Cron Jobs**: Scheduled task execution
|
|
99
|
+
- **Multi-tenancy**: Support for multiple integrations/plugins
|
|
100
|
+
|
|
101
|
+
### Plugin Development
|
|
102
|
+
|
|
103
|
+
Plugins must implement standard methods:
|
|
104
|
+
- `validateToken`: Token validation
|
|
105
|
+
- `tokenTemplate`: Token configuration template
|
|
106
|
+
- Booking methods: `searchProducts`, `searchAvailability`, `createBooking`, `cancelBooking`
|
|
107
|
+
- Content methods: `getProducts`, `getProduct`, `createProduct`, `updateProduct`
|
|
108
|
+
|
|
109
|
+
### Testing Strategy
|
|
110
|
+
|
|
111
|
+
- Jest testing framework
|
|
112
|
+
- Test fixtures in `__fixtures__/`
|
|
113
|
+
- Mock plugins in `__mocks__/`
|
|
114
|
+
- Use `test/utils.js` for creating test environments
|
|
115
|
+
- Tests organized by controller in `controllers/__tests__/`
|
|
116
|
+
|
|
117
|
+
## Database
|
|
118
|
+
|
|
119
|
+
- Sequelize ORM with MySQL/MariaDB
|
|
120
|
+
- Connection managed through `models/db.js`
|
|
121
|
+
- Migrations handled via sequelize-cli
|
|
122
|
+
- Models auto-loaded from `models/` directory
|
|
123
|
+
|
|
124
|
+
## Environment Variables
|
|
125
|
+
|
|
126
|
+
Key variables:
|
|
127
|
+
- `PORT`: Server port (default: 10010)
|
|
128
|
+
- `REDIS_URL`: Redis connection (default: redis://redis:6379)
|
|
129
|
+
- `jwtSecret`: JWT signing secret
|
|
130
|
+
- `adminKey`: Admin authentication key
|
|
131
|
+
- `WEB_CONCURRENCY`: Number of workers
|
|
132
|
+
- `MAX_JOBS_PER_WORKER`: Job concurrency per worker
|
|
133
|
+
- `SSL_INSECURE_ALLOWED_DOMAINS`: Pipe-separated list of domains where SSL certificate verification is skipped (e.g., `localhost|staging.example.com|192.168.1.100`)
|
|
134
|
+
|
|
135
|
+
Plugin-specific variables follow pattern: `ti2_{pluginName}_{setting}`
|
|
136
|
+
|
|
137
|
+
### SSL Configuration
|
|
138
|
+
|
|
139
|
+
The `SSL_INSECURE_ALLOWED_DOMAINS` environment variable allows you to specify domains where SSL certificate verification should be skipped. This is useful for:
|
|
140
|
+
- Development environments with self-signed certificates
|
|
141
|
+
- Testing against staging servers with invalid certificates
|
|
142
|
+
- Local development with HTTPS
|
|
143
|
+
|
|
144
|
+
Example usage:
|
|
145
|
+
```bash
|
|
146
|
+
# Skip SSL verification for specific domains
|
|
147
|
+
export SSL_INSECURE_ALLOWED_DOMAINS="localhost|staging.api.com|192.168.1.100"
|
|
148
|
+
|
|
149
|
+
# Multiple domains separated by pipe
|
|
150
|
+
export SSL_INSECURE_ALLOWED_DOMAINS="dev.example.com|test.example.com"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Important Implementation Notes**:
|
|
154
|
+
- SSL configuration is ONLY applied to plugin axios instances (for external API calls) and worker axios instances
|
|
155
|
+
- Internal communication (callbacks, per-request axios) do NOT use SSL configuration
|
|
156
|
+
- SSL configuration uses a per-request interceptor approach for better isolation
|
|
157
|
+
- The configuration checks domains dynamically on each request
|
|
158
|
+
|
|
159
|
+
**Warning**: Only use this in development/testing environments. Never skip SSL verification in production for security reasons.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/* globals describe it expect beforeAll afterAll afterEach */
|
|
2
|
+
|
|
3
|
+
const cache = require('../cache');
|
|
4
|
+
|
|
5
|
+
describe('cache', () => {
|
|
6
|
+
const testPluginName = 'testPlugin';
|
|
7
|
+
const testKeys = [];
|
|
8
|
+
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
// Clean up test keys
|
|
11
|
+
for (const key of testKeys) {
|
|
12
|
+
await cache.drop({ pluginName: testPluginName, key });
|
|
13
|
+
}
|
|
14
|
+
testKeys.length = 0;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
describe('scan', () => {
|
|
19
|
+
it('should return keys matching the pattern without plugin prefix', async () => {
|
|
20
|
+
// Save some test keys
|
|
21
|
+
const keys = ['product:123', 'product:456', 'booking:789'];
|
|
22
|
+
for (const key of keys) {
|
|
23
|
+
testKeys.push(key);
|
|
24
|
+
await cache.save({ pluginName: testPluginName, key, value: { test: true } });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Scan for product keys
|
|
28
|
+
const result = await cache.scan({ pluginName: testPluginName, pattern: 'product:*' });
|
|
29
|
+
|
|
30
|
+
expect(result).toHaveLength(2);
|
|
31
|
+
expect(result).toContain('product:123');
|
|
32
|
+
expect(result).toContain('product:456');
|
|
33
|
+
expect(result).not.toContain('booking:789');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should return empty array when no keys match', async () => {
|
|
37
|
+
const result = await cache.scan({ pluginName: testPluginName, pattern: 'nonexistent:*' });
|
|
38
|
+
|
|
39
|
+
expect(result).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should not include keys from other plugins', async () => {
|
|
43
|
+
const otherPluginName = 'otherPlugin';
|
|
44
|
+
|
|
45
|
+
// Save keys for both plugins
|
|
46
|
+
await cache.save({ pluginName: testPluginName, key: 'shared:key1', value: { test: true } });
|
|
47
|
+
await cache.save({ pluginName: otherPluginName, key: 'shared:key2', value: { test: true } });
|
|
48
|
+
testKeys.push('shared:key1');
|
|
49
|
+
|
|
50
|
+
// Scan should only return keys for the specified plugin
|
|
51
|
+
const result = await cache.scan({ pluginName: testPluginName, pattern: 'shared:*' });
|
|
52
|
+
|
|
53
|
+
expect(result).toHaveLength(1);
|
|
54
|
+
expect(result).toContain('shared:key1');
|
|
55
|
+
expect(result).not.toContain('shared:key2');
|
|
56
|
+
|
|
57
|
+
// Cleanup other plugin key
|
|
58
|
+
await cache.drop({ pluginName: otherPluginName, key: 'shared:key2' });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should handle wildcard patterns correctly', async () => {
|
|
62
|
+
const keys = ['cache:v1:product:1', 'cache:v1:product:2', 'cache:v2:product:1'];
|
|
63
|
+
for (const key of keys) {
|
|
64
|
+
testKeys.push(key);
|
|
65
|
+
await cache.save({ pluginName: testPluginName, key, value: { test: true } });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Scan with nested wildcard
|
|
69
|
+
const result = await cache.scan({ pluginName: testPluginName, pattern: 'cache:v1:*' });
|
|
70
|
+
|
|
71
|
+
expect(result).toHaveLength(2);
|
|
72
|
+
expect(result).toContain('cache:v1:product:1');
|
|
73
|
+
expect(result).toContain('cache:v1:product:2');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
package/cache.js
CHANGED
|
@@ -93,6 +93,26 @@ const drop = async ({
|
|
|
93
93
|
|
|
94
94
|
const keys = async () => cache.keys('*');
|
|
95
95
|
|
|
96
|
+
const scan = ({
|
|
97
|
+
pluginName,
|
|
98
|
+
pattern,
|
|
99
|
+
count = 1000,
|
|
100
|
+
}) => new Promise((resolve, reject) => {
|
|
101
|
+
const fullPattern = `${pluginName}:${pattern}`;
|
|
102
|
+
const foundKeys = [];
|
|
103
|
+
const prefixLength = pluginName.length + 1; // +1 for the colon
|
|
104
|
+
|
|
105
|
+
const stream = cache.scanStream({ match: fullPattern, count });
|
|
106
|
+
stream.on('data', keys => {
|
|
107
|
+
keys.forEach(key => {
|
|
108
|
+
// Return keys without the plugin prefix
|
|
109
|
+
foundKeys.push(key.slice(prefixLength));
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
stream.on('end', () => resolve(foundKeys));
|
|
113
|
+
stream.on('error', reject);
|
|
114
|
+
});
|
|
115
|
+
|
|
96
116
|
module.exports = {
|
|
97
117
|
cache,
|
|
98
118
|
save,
|
|
@@ -100,4 +120,5 @@ module.exports = {
|
|
|
100
120
|
drop,
|
|
101
121
|
get,
|
|
102
122
|
keys,
|
|
123
|
+
scan,
|
|
103
124
|
};
|
|
@@ -285,7 +285,7 @@ describe('user: bookings controller - searchProducts', () => {
|
|
|
285
285
|
expect(jobData.method).toBe(expectedPluginMethod);
|
|
286
286
|
expect(jobData.token).toBeDefined(); // Token passed to the job
|
|
287
287
|
expect(jobData.payload.userId).toBe(testUserId);
|
|
288
|
-
expect(jobData.payload.payload).toEqual({}); // Original payload for the plugin method
|
|
288
|
+
expect(jobData.payload.payload).toEqual({ forceRefresh: false }); // Original payload for the plugin method
|
|
289
289
|
expect(jobData.postProcess.controller).toBe('bookings');
|
|
290
290
|
expect(jobData.postProcess.action).toBe('$updateProductSearchCache');
|
|
291
291
|
expect(jobData.postProcess.args.appKey).toBe(testAppName);
|
|
@@ -512,7 +512,7 @@ describe('Bookings Product Search Lock Mechanism (Job Queuing on Stale Cache)',
|
|
|
512
512
|
method: expectedPluginMethodName,
|
|
513
513
|
token: expect.objectContaining({ ttlForProducts: shortTTRTokenConfig.ttlForProducts }),
|
|
514
514
|
payload: expect.objectContaining({
|
|
515
|
-
payload: {}, // originalRequestBody was empty for the product search
|
|
515
|
+
payload: { forceRefresh: false }, // originalRequestBody was empty for the product search
|
|
516
516
|
userId: testUserId,
|
|
517
517
|
}),
|
|
518
518
|
postProcess: expect.objectContaining({
|
|
@@ -168,4 +168,86 @@ describe('user: bookings controller', () => {
|
|
|
168
168
|
expect(plugins[0].createBooking.mock.calls[0][0].payload).toEqual(payload);
|
|
169
169
|
expect(plugins[0].createBooking.mock.calls[0][0].token).toEqual(token);
|
|
170
170
|
});
|
|
171
|
+
|
|
172
|
+
describe('product search with empty cache', () => {
|
|
173
|
+
const productSearchCacheKey = hash({
|
|
174
|
+
userId,
|
|
175
|
+
hint: 'testingToken',
|
|
176
|
+
operationId: 'bookingsProductSearch',
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
beforeEach(async () => {
|
|
180
|
+
// Clear cache before each test
|
|
181
|
+
await cache.drop({ pluginName: appKey, key: productSearchCacheKey });
|
|
182
|
+
await cache.drop({ pluginName: appKey, key: `${productSearchCacheKey}:lock` });
|
|
183
|
+
await cache.drop({ pluginName: appKey, key: `${productSearchCacheKey}:lastUpdated` });
|
|
184
|
+
await cache.drop({ pluginName: appKey, key: `${productSearchCacheKey}:jobLock` });
|
|
185
|
+
jest.clearAllMocks();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should fetch from plugin when cache is empty and no search filter', async () => {
|
|
189
|
+
// First call: set up empty cache
|
|
190
|
+
plugins[0].searchProducts.mockImplementationOnce(() => ({ products: [] }));
|
|
191
|
+
await doApiPost({
|
|
192
|
+
url: `/products/${appKey}/${userId}/testingToken/search`,
|
|
193
|
+
token: userToken,
|
|
194
|
+
payload: {},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Second call: cache is empty, no filter - should call plugin again
|
|
198
|
+
plugins[0].searchProducts.mockImplementationOnce(() => ({
|
|
199
|
+
products: [{ productId: '999', productName: 'Fresh Product' }],
|
|
200
|
+
}));
|
|
201
|
+
const { products } = await doApiPost({
|
|
202
|
+
url: `/products/${appKey}/${userId}/testingToken/search`,
|
|
203
|
+
token: userToken,
|
|
204
|
+
payload: {},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(plugins[0].searchProducts).toHaveBeenCalledTimes(2);
|
|
208
|
+
expect(products).toHaveLength(1);
|
|
209
|
+
expect(products[0].productName).toBe('Fresh Product');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should return empty when searching with filter on empty cache', async () => {
|
|
213
|
+
// Set up empty cache first
|
|
214
|
+
plugins[0].searchProducts.mockImplementationOnce(() => ({ products: [] }));
|
|
215
|
+
await doApiPost({
|
|
216
|
+
url: `/products/${appKey}/${userId}/testingToken/search`,
|
|
217
|
+
token: userToken,
|
|
218
|
+
payload: {},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Search with filter on empty cache - should return empty (filter finds nothing)
|
|
222
|
+
const { products } = await doApiPost({
|
|
223
|
+
url: `/products/${appKey}/${userId}/testingToken/search`,
|
|
224
|
+
token: userToken,
|
|
225
|
+
payload: { searchInput: 'nonexistent' },
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(products).toHaveLength(0);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should return cached products when cache has data', async () => {
|
|
232
|
+
// Set up cache with products
|
|
233
|
+
plugins[0].searchProducts.mockImplementationOnce(() => ({
|
|
234
|
+
products: [{ productId: '123', productName: 'Cached Product' }],
|
|
235
|
+
}));
|
|
236
|
+
await doApiPost({
|
|
237
|
+
url: `/products/${appKey}/${userId}/testingToken/search`,
|
|
238
|
+
token: userToken,
|
|
239
|
+
payload: {},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Second call - should return cached data
|
|
243
|
+
const { products } = await doApiPost({
|
|
244
|
+
url: `/products/${appKey}/${userId}/testingToken/search`,
|
|
245
|
+
token: userToken,
|
|
246
|
+
payload: {},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
expect(products).toHaveLength(1);
|
|
250
|
+
expect(products[0].productName).toBe('Cached Product');
|
|
251
|
+
});
|
|
252
|
+
});
|
|
171
253
|
});
|
package/controllers/bookings.js
CHANGED
|
@@ -138,8 +138,8 @@ const $bookingsProductSearch = plugins => async ({
|
|
|
138
138
|
forceRefresh = false,
|
|
139
139
|
} = originalRequestBody;
|
|
140
140
|
|
|
141
|
-
// Payload for the plugin function (func) -
|
|
142
|
-
const payloadForPlugin = R.omit(['forceRefresh'], originalRequestBody);
|
|
141
|
+
// Payload for the plugin function (func) - pass forceRefresh so plugins can trigger background cache rebuilds
|
|
142
|
+
const payloadForPlugin = { ...R.omit(['forceRefresh'], originalRequestBody), forceRefresh };
|
|
143
143
|
|
|
144
144
|
const app = plugins.find(({ name }) => name === appKey);
|
|
145
145
|
assert(userId, 'userId is required');
|
|
@@ -212,48 +212,47 @@ const $bookingsProductSearch = plugins => async ({
|
|
|
212
212
|
|
|
213
213
|
// 3. Cache exists (initialActualCacheContent) and not forceRefresh:
|
|
214
214
|
if (initialActualCacheContent && initialActualCacheContent.products) {
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
// Cache is effectively stale AND no plugin execution is currently in progress.
|
|
224
|
-
// Try to queue a background job.
|
|
225
|
-
const hasJobQueueLock = await app.cache.get({ key: jobQueueLockKey });
|
|
226
|
-
|
|
227
|
-
if (hasJobQueueLock) {
|
|
228
|
-
// A background job has recently been queued by another request. Serve stale data.
|
|
215
|
+
const cacheIsEmpty = !initialActualCacheContent.products.length;
|
|
216
|
+
const trimmedSearch = (searchInput || '').trim();
|
|
217
|
+
const searchFilterIsEmpty = (!trimmedSearch || trimmedSearch === '*') && !(optionId && optionId.length);
|
|
218
|
+
|
|
219
|
+
// If cache is empty and no search filter, skip to case 4 to fetch fresh data
|
|
220
|
+
const shouldSkipEmptyCache = cacheIsEmpty && searchFilterIsEmpty;
|
|
221
|
+
if (!shouldSkipEmptyCache) {
|
|
222
|
+
const returnCachedResults = () => {
|
|
229
223
|
const searchResults = $searchProductList(initialActualCacheContent.products, searchInput, optionId);
|
|
230
224
|
return { ...initialActualCacheContent, products: searchResults, ...(token.configuration || {}) };
|
|
231
|
-
}
|
|
232
|
-
// No job queue lock. Set one, then queue the job, then serve stale data.
|
|
233
|
-
await app.cache.save({ key: jobQueueLockKey, value: true, ttl: 60 }); // Lock for 60 seconds
|
|
225
|
+
};
|
|
234
226
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
payload: pluginMethodPayload,
|
|
247
|
-
postProcess: {
|
|
248
|
-
controller: 'bookings',
|
|
249
|
-
action: '$updateProductSearchCache',
|
|
250
|
-
args: { appKey, userId, hint },
|
|
251
|
-
},
|
|
252
|
-
};
|
|
253
|
-
await addJob(jobData, { removeOnComplete: true });
|
|
254
|
-
|
|
255
|
-
return { ...initialActualCacheContent, products: searchResults, ...(token.configuration || {}) };
|
|
227
|
+
const isEffectivelyStale = isStaleByTTR && !doNotCallPluginForProducts;
|
|
228
|
+
|
|
229
|
+
// Cache is fresh or plugin execution in progress: serve from cache
|
|
230
|
+
if (!isEffectivelyStale || hasPluginExecutionLock) {
|
|
231
|
+
return returnCachedResults();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Cache is stale - check if background job already queued
|
|
235
|
+
const hasJobQueueLock = await app.cache.get({ key: jobQueueLockKey });
|
|
236
|
+
if (hasJobQueueLock) {
|
|
237
|
+
return returnCachedResults();
|
|
256
238
|
}
|
|
239
|
+
|
|
240
|
+
// Queue background refresh job and serve stale data
|
|
241
|
+
await app.cache.save({ key: jobQueueLockKey, value: true, ttl: 60 });
|
|
242
|
+
await addJob({
|
|
243
|
+
type: 'plugin',
|
|
244
|
+
pluginName: appKey,
|
|
245
|
+
method: app.searchProducts ? 'searchProducts' : 'searchProductsForItinerary',
|
|
246
|
+
token,
|
|
247
|
+
payload: { payload: payloadForPlugin, userId },
|
|
248
|
+
postProcess: {
|
|
249
|
+
controller: 'bookings',
|
|
250
|
+
action: '$updateProductSearchCache',
|
|
251
|
+
args: { appKey, userId, hint },
|
|
252
|
+
},
|
|
253
|
+
}, { removeOnComplete: true });
|
|
254
|
+
|
|
255
|
+
return returnCachedResults();
|
|
257
256
|
}
|
|
258
257
|
}
|
|
259
258
|
|
package/index.js
CHANGED
|
@@ -89,6 +89,7 @@ module.exports = async ({
|
|
|
89
89
|
get: args => cache.get({ ...args, pluginName }),
|
|
90
90
|
getOrExec: args => cache.getOrExec({ ...args, pluginName }),
|
|
91
91
|
save: args => cache.save({ ...args, pluginName }),
|
|
92
|
+
scan: args => cache.scan({ ...args, pluginName }),
|
|
92
93
|
},
|
|
93
94
|
axios: pluginAxios,
|
|
94
95
|
events: ti2Events,
|