@nocobase/flow-engine 2.2.0-beta.10 → 2.2.0-beta.11
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.
|
@@ -31,8 +31,10 @@ __export(dirtyAwareApiClient_exports, {
|
|
|
31
31
|
getDirtyAwareApiClient: () => getDirtyAwareApiClient
|
|
32
32
|
});
|
|
33
33
|
module.exports = __toCommonJS(dirtyAwareApiClient_exports);
|
|
34
|
+
var import_sdk = require("@nocobase/sdk");
|
|
34
35
|
var import_dataSourceDirty = require("./dataSourceDirty");
|
|
35
36
|
const SKIP_DATA_SOURCE_DIRTY = "__nocobaseSkipDataSourceDirty";
|
|
37
|
+
const DIRTY_DISPATCH_TOKEN = Symbol("nocobaseDirtyDispatchToken");
|
|
36
38
|
const dirtyAwareApiClientCache = /* @__PURE__ */ new WeakMap();
|
|
37
39
|
const dirtyAwareApiClientProxies = /* @__PURE__ */ new WeakSet();
|
|
38
40
|
const MUTATING_RESOURCE_ACTIONS = [
|
|
@@ -288,6 +290,43 @@ function resolveDirtyResourceAction(options, context) {
|
|
|
288
290
|
return parseDirtyResourceActionFromUrl(options == null ? void 0 : options.url, context);
|
|
289
291
|
}
|
|
290
292
|
__name(resolveDirtyResourceAction, "resolveDirtyResourceAction");
|
|
293
|
+
function getDirtyResourceActionDispatchKey(dirtyResourceAction, headers) {
|
|
294
|
+
if (!dirtyResourceAction) {
|
|
295
|
+
return void 0;
|
|
296
|
+
}
|
|
297
|
+
return JSON.stringify([
|
|
298
|
+
dirtyResourceAction.dataSourceKey || (0, import_dataSourceDirty.getDataSourceKeyFromHeaders)(headers),
|
|
299
|
+
dirtyResourceAction.resourceName,
|
|
300
|
+
dirtyResourceAction.actionName
|
|
301
|
+
]);
|
|
302
|
+
}
|
|
303
|
+
__name(getDirtyResourceActionDispatchKey, "getDirtyResourceActionDispatchKey");
|
|
304
|
+
function getRequestDispatchKey(options, context) {
|
|
305
|
+
return getDirtyResourceActionDispatchKey(resolveDirtyResourceAction(options, context), options.headers);
|
|
306
|
+
}
|
|
307
|
+
__name(getRequestDispatchKey, "getRequestDispatchKey");
|
|
308
|
+
function getResourceDispatchKey(name, of, headers) {
|
|
309
|
+
const resourceName = String(name ?? "").trim();
|
|
310
|
+
if (!resourceName) {
|
|
311
|
+
return void 0;
|
|
312
|
+
}
|
|
313
|
+
return JSON.stringify([resourceName, String(of ?? ""), (0, import_dataSourceDirty.getDataSourceKeyFromHeaders)(headers)]);
|
|
314
|
+
}
|
|
315
|
+
__name(getResourceDispatchKey, "getResourceDispatchKey");
|
|
316
|
+
function getMatchingRequestToken(token, requestKey) {
|
|
317
|
+
if (!token || !requestKey || token.requestKey && token.requestKey !== requestKey) {
|
|
318
|
+
return void 0;
|
|
319
|
+
}
|
|
320
|
+
return token;
|
|
321
|
+
}
|
|
322
|
+
__name(getMatchingRequestToken, "getMatchingRequestToken");
|
|
323
|
+
function getMatchingResourceToken(token, resourceKey) {
|
|
324
|
+
if (!token || !resourceKey || token.resourceKey && token.resourceKey !== resourceKey) {
|
|
325
|
+
return void 0;
|
|
326
|
+
}
|
|
327
|
+
return token;
|
|
328
|
+
}
|
|
329
|
+
__name(getMatchingResourceToken, "getMatchingResourceToken");
|
|
291
330
|
function markResourceActionDataSourceDirty(context, dirtyResourceAction, headers) {
|
|
292
331
|
(0, import_dataSourceDirty.markDataSourceDirty)({
|
|
293
332
|
engine: context.engine,
|
|
@@ -297,7 +336,19 @@ function markResourceActionDataSourceDirty(context, dirtyResourceAction, headers
|
|
|
297
336
|
});
|
|
298
337
|
}
|
|
299
338
|
__name(markResourceActionDataSourceDirty, "markResourceActionDataSourceDirty");
|
|
300
|
-
function
|
|
339
|
+
function markResourceActionDataSourceDirtyOnce(token, context, dirtyResourceAction, headers) {
|
|
340
|
+
if (token.skip || token.marked || !dirtyResourceAction || !isMutatingResourceAction(dirtyResourceAction.actionName)) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
token.marked = true;
|
|
344
|
+
markResourceActionDataSourceDirty(context, dirtyResourceAction, headers);
|
|
345
|
+
}
|
|
346
|
+
__name(markResourceActionDataSourceDirtyOnce, "markResourceActionDataSourceDirtyOnce");
|
|
347
|
+
function isObjectRecord(value) {
|
|
348
|
+
return !!value && typeof value === "object";
|
|
349
|
+
}
|
|
350
|
+
__name(isObjectRecord, "isObjectRecord");
|
|
351
|
+
function createDirtyAwareResource(context, resource, resourceName, resourceOf, headers, requestTokenStack, parentToken) {
|
|
301
352
|
return new Proxy(resource, {
|
|
302
353
|
get(target, prop, receiver) {
|
|
303
354
|
const original = Reflect.get(target, prop, receiver);
|
|
@@ -306,10 +357,38 @@ function createDirtyAwareResource(context, resource, resourceName, resourceOf, h
|
|
|
306
357
|
}
|
|
307
358
|
const action = original;
|
|
308
359
|
return async (...args) => {
|
|
309
|
-
const
|
|
360
|
+
const actionOptions = isObjectRecord(args[1]) ? args[1] : void 0;
|
|
310
361
|
const dirtyResourceAction = resolveDirtyResourceActionFromResource(resourceName, resourceOf, prop, context);
|
|
311
|
-
|
|
312
|
-
|
|
362
|
+
const requestKey = getDirtyResourceActionDispatchKey(dirtyResourceAction, headers);
|
|
363
|
+
const resourceKey = getResourceDispatchKey(resourceName, resourceOf, headers);
|
|
364
|
+
const inheritedToken = getMatchingRequestToken(
|
|
365
|
+
actionOptions == null ? void 0 : actionOptions[DIRTY_DISPATCH_TOKEN],
|
|
366
|
+
requestKey
|
|
367
|
+
) || getMatchingRequestToken(parentToken, requestKey);
|
|
368
|
+
const token = inheritedToken || { marked: false, skip: false };
|
|
369
|
+
const ownsToken = !inheritedToken;
|
|
370
|
+
token.requestKey ||= requestKey;
|
|
371
|
+
token.resourceKey ||= resourceKey;
|
|
372
|
+
if (actionOptions == null ? void 0 : actionOptions[SKIP_DATA_SOURCE_DIRTY]) {
|
|
373
|
+
token.skip = true;
|
|
374
|
+
}
|
|
375
|
+
const forwardedArgs = actionOptions || args[1] == null ? [
|
|
376
|
+
args[0],
|
|
377
|
+
{
|
|
378
|
+
...actionOptions,
|
|
379
|
+
[DIRTY_DISPATCH_TOKEN]: token
|
|
380
|
+
}
|
|
381
|
+
] : args;
|
|
382
|
+
let actionResult;
|
|
383
|
+
requestTokenStack.push({ key: requestKey, token });
|
|
384
|
+
try {
|
|
385
|
+
actionResult = Reflect.apply(action, receiver, forwardedArgs);
|
|
386
|
+
} finally {
|
|
387
|
+
requestTokenStack.pop();
|
|
388
|
+
}
|
|
389
|
+
const result = await actionResult;
|
|
390
|
+
if (ownsToken) {
|
|
391
|
+
markResourceActionDataSourceDirtyOnce(token, context, dirtyResourceAction, headers);
|
|
313
392
|
}
|
|
314
393
|
return result;
|
|
315
394
|
};
|
|
@@ -318,31 +397,206 @@ function createDirtyAwareResource(context, resource, resourceName, resourceOf, h
|
|
|
318
397
|
}
|
|
319
398
|
__name(createDirtyAwareResource, "createDirtyAwareResource");
|
|
320
399
|
function createDirtyAwareApiClient(api, context) {
|
|
400
|
+
const baseResource = api.resource;
|
|
401
|
+
const baseRequest = api.request;
|
|
402
|
+
const shouldUseResourceDispatchReceiver = baseResource === import_sdk.APIClient.prototype.resource;
|
|
403
|
+
const shouldUseRequestDispatchReceiver = baseRequest === import_sdk.APIClient.prototype.request;
|
|
404
|
+
const resourceTokenStack = [];
|
|
405
|
+
const requestTokenStack = [];
|
|
406
|
+
let hasResourceOverride = false;
|
|
407
|
+
let resourceOverride;
|
|
408
|
+
let hasRequestOverride = false;
|
|
409
|
+
let requestOverride;
|
|
410
|
+
const getCurrentResource = /* @__PURE__ */ __name(() => hasResourceOverride ? resourceOverride : resource, "getCurrentResource");
|
|
411
|
+
const getCurrentRequest = /* @__PURE__ */ __name(() => hasRequestOverride ? requestOverride : request, "getCurrentRequest");
|
|
412
|
+
const dispatchResource = /* @__PURE__ */ __name((token, args) => {
|
|
413
|
+
const resourceKey = getResourceDispatchKey(args[0], args[1], args[2]);
|
|
414
|
+
const activeToken = getMatchingResourceToken(token, resourceKey);
|
|
415
|
+
const shouldWrapActions = !!activeToken && hasResourceOverride;
|
|
416
|
+
if (activeToken) {
|
|
417
|
+
activeToken.resourceKey ||= resourceKey;
|
|
418
|
+
resourceTokenStack.push({ key: resourceKey, token: activeToken });
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
const resourceInstance = Reflect.apply(getCurrentResource(), proxy, args);
|
|
422
|
+
if (!shouldWrapActions || !activeToken) {
|
|
423
|
+
return resourceInstance;
|
|
424
|
+
}
|
|
425
|
+
return new Proxy(resourceInstance, {
|
|
426
|
+
get(target, prop, receiver) {
|
|
427
|
+
const original = Reflect.get(target, prop, receiver);
|
|
428
|
+
if (typeof prop !== "string" || typeof original !== "function" || !isMutatingResourceAction(prop)) {
|
|
429
|
+
return original;
|
|
430
|
+
}
|
|
431
|
+
const action = original;
|
|
432
|
+
return (...actionArgs) => {
|
|
433
|
+
const actionOptions = isObjectRecord(actionArgs[1]) ? actionArgs[1] : void 0;
|
|
434
|
+
const forwardedArgs = actionOptions || actionArgs[1] == null ? [
|
|
435
|
+
actionArgs[0],
|
|
436
|
+
{
|
|
437
|
+
...actionOptions,
|
|
438
|
+
[DIRTY_DISPATCH_TOKEN]: activeToken
|
|
439
|
+
}
|
|
440
|
+
] : actionArgs;
|
|
441
|
+
resourceTokenStack.push({ key: resourceKey, token: activeToken });
|
|
442
|
+
requestTokenStack.push({ key: activeToken.requestKey, token: activeToken });
|
|
443
|
+
try {
|
|
444
|
+
return Reflect.apply(action, receiver, forwardedArgs);
|
|
445
|
+
} finally {
|
|
446
|
+
requestTokenStack.pop();
|
|
447
|
+
resourceTokenStack.pop();
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
} finally {
|
|
453
|
+
if (activeToken) {
|
|
454
|
+
resourceTokenStack.pop();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}, "dispatchResource");
|
|
458
|
+
const createDispatchReceiver = /* @__PURE__ */ __name((token) => {
|
|
459
|
+
const receiver = Object.create(api);
|
|
460
|
+
Object.defineProperties(receiver, {
|
|
461
|
+
request: {
|
|
462
|
+
configurable: true,
|
|
463
|
+
value: /* @__PURE__ */ __name((config) => {
|
|
464
|
+
const options = config;
|
|
465
|
+
const requestKey = getRequestDispatchKey(options, context);
|
|
466
|
+
const stackFrame = requestTokenStack.at(-1);
|
|
467
|
+
const activeToken = getMatchingRequestToken(options == null ? void 0 : options[DIRTY_DISPATCH_TOKEN], requestKey) || (requestKey && (stackFrame == null ? void 0 : stackFrame.key) === requestKey ? stackFrame.token : void 0) || getMatchingRequestToken(token, requestKey);
|
|
468
|
+
const { [DIRTY_DISPATCH_TOKEN]: _dirtyDispatchToken, ...cleanOptions } = options;
|
|
469
|
+
const configWithToken = activeToken ? { ...cleanOptions, [DIRTY_DISPATCH_TOKEN]: activeToken } : cleanOptions;
|
|
470
|
+
if (activeToken) {
|
|
471
|
+
activeToken.requestKey ||= requestKey;
|
|
472
|
+
requestTokenStack.push({ key: requestKey, token: activeToken });
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
return Reflect.apply(getCurrentRequest(), proxy, [configWithToken]);
|
|
476
|
+
} finally {
|
|
477
|
+
if (activeToken) {
|
|
478
|
+
requestTokenStack.pop();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}, "value")
|
|
482
|
+
},
|
|
483
|
+
resource: {
|
|
484
|
+
configurable: true,
|
|
485
|
+
value: /* @__PURE__ */ __name((...args) => {
|
|
486
|
+
const resourceKey = getResourceDispatchKey(args[0], args[1], args[2]);
|
|
487
|
+
const stackFrame = resourceTokenStack.at(-1);
|
|
488
|
+
const activeToken = getMatchingResourceToken(token, resourceKey) || (resourceKey && (stackFrame == null ? void 0 : stackFrame.key) === resourceKey ? stackFrame.token : void 0);
|
|
489
|
+
return dispatchResource(activeToken, args);
|
|
490
|
+
}, "value")
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
return receiver;
|
|
494
|
+
}, "createDispatchReceiver");
|
|
321
495
|
const resource = /* @__PURE__ */ __name((name, of, headers, cancel) => {
|
|
322
|
-
const
|
|
323
|
-
|
|
496
|
+
const resourceKey = getResourceDispatchKey(name, of, headers);
|
|
497
|
+
const stackFrame = resourceTokenStack.at(-1);
|
|
498
|
+
const parentToken = resourceKey && (stackFrame == null ? void 0 : stackFrame.key) === resourceKey ? stackFrame.token : void 0;
|
|
499
|
+
const receiver = createDispatchReceiver(parentToken);
|
|
500
|
+
const resourceInstance = Reflect.apply(baseResource, shouldUseResourceDispatchReceiver ? receiver : api, [
|
|
501
|
+
name,
|
|
502
|
+
of,
|
|
503
|
+
headers,
|
|
504
|
+
cancel
|
|
505
|
+
]);
|
|
506
|
+
return createDirtyAwareResource(context, resourceInstance, name, of, headers, requestTokenStack, parentToken);
|
|
324
507
|
}, "resource");
|
|
325
508
|
const request = /* @__PURE__ */ __name((config) => {
|
|
326
509
|
const options = config;
|
|
510
|
+
const requestKey = getRequestDispatchKey(options, context);
|
|
511
|
+
const stackFrame = requestTokenStack.at(-1);
|
|
512
|
+
const inheritedToken = getMatchingRequestToken(options == null ? void 0 : options[DIRTY_DISPATCH_TOKEN], requestKey) || (requestKey && (stackFrame == null ? void 0 : stackFrame.key) === requestKey ? stackFrame.token : void 0);
|
|
513
|
+
const token = inheritedToken || { marked: false, skip: false };
|
|
514
|
+
const ownsToken = !inheritedToken;
|
|
515
|
+
token.requestKey ||= requestKey;
|
|
516
|
+
if (typeof options.resource === "string") {
|
|
517
|
+
token.resourceKey ||= getResourceDispatchKey(options.resource, options.resourceOf, options.headers);
|
|
518
|
+
}
|
|
327
519
|
const skipDataSourceDirty = options == null ? void 0 : options[SKIP_DATA_SOURCE_DIRTY];
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
520
|
+
if (skipDataSourceDirty) {
|
|
521
|
+
token.skip = true;
|
|
522
|
+
}
|
|
523
|
+
const dirtyResourceAction = resolveDirtyResourceAction(options, context);
|
|
524
|
+
const {
|
|
525
|
+
[DIRTY_DISPATCH_TOKEN]: _dirtyDispatchToken,
|
|
526
|
+
[SKIP_DATA_SOURCE_DIRTY]: _skipDataSourceDirty,
|
|
527
|
+
...cleanConfig
|
|
528
|
+
} = options;
|
|
529
|
+
const receiver = createDispatchReceiver(token);
|
|
530
|
+
return Reflect.apply(baseRequest, shouldUseRequestDispatchReceiver ? receiver : api, [cleanConfig]).then((result) => {
|
|
531
|
+
if (ownsToken) {
|
|
532
|
+
markResourceActionDataSourceDirtyOnce(token, context, dirtyResourceAction, options.headers);
|
|
333
533
|
}
|
|
334
534
|
return result;
|
|
335
535
|
});
|
|
336
536
|
}, "request");
|
|
537
|
+
const isLockedOwnProperty = /* @__PURE__ */ __name((target, prop) => {
|
|
538
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
539
|
+
return !!descriptor && !descriptor.configurable;
|
|
540
|
+
}, "isLockedOwnProperty");
|
|
337
541
|
const proxy = new Proxy(api, {
|
|
338
542
|
get(target, prop, receiver) {
|
|
339
543
|
if (prop === "resource") {
|
|
340
|
-
|
|
544
|
+
if (isLockedOwnProperty(target, prop)) {
|
|
545
|
+
return Reflect.get(target, prop, receiver);
|
|
546
|
+
}
|
|
547
|
+
return hasResourceOverride ? resourceOverride : resource;
|
|
341
548
|
}
|
|
342
549
|
if (prop === "request") {
|
|
343
|
-
|
|
550
|
+
if (isLockedOwnProperty(target, prop)) {
|
|
551
|
+
return Reflect.get(target, prop, receiver);
|
|
552
|
+
}
|
|
553
|
+
return hasRequestOverride ? requestOverride : request;
|
|
344
554
|
}
|
|
345
555
|
return Reflect.get(target, prop, receiver);
|
|
556
|
+
},
|
|
557
|
+
set(target, prop, value, receiver) {
|
|
558
|
+
if (prop === "resource") {
|
|
559
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
560
|
+
if (descriptor && (!descriptor.configurable || !Reflect.isExtensible(target))) {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
hasResourceOverride = true;
|
|
564
|
+
resourceOverride = value;
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
if (prop === "request") {
|
|
568
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
569
|
+
if (descriptor && (!descriptor.configurable || !Reflect.isExtensible(target))) {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
hasRequestOverride = true;
|
|
573
|
+
requestOverride = value;
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
return Reflect.set(target, prop, value, receiver);
|
|
577
|
+
},
|
|
578
|
+
deleteProperty(target, prop) {
|
|
579
|
+
if (prop !== "resource" && prop !== "request") {
|
|
580
|
+
return Reflect.deleteProperty(target, prop);
|
|
581
|
+
}
|
|
582
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
583
|
+
if (descriptor && (!descriptor.configurable || !Reflect.isExtensible(target))) {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
if (prop === "resource") {
|
|
587
|
+
hasResourceOverride = false;
|
|
588
|
+
resourceOverride = void 0;
|
|
589
|
+
} else {
|
|
590
|
+
hasRequestOverride = false;
|
|
591
|
+
requestOverride = void 0;
|
|
592
|
+
}
|
|
593
|
+
return true;
|
|
594
|
+
},
|
|
595
|
+
defineProperty(target, prop, descriptor) {
|
|
596
|
+
if (prop === "resource" || prop === "request") {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
return Reflect.defineProperty(target, prop, descriptor);
|
|
346
600
|
}
|
|
347
601
|
});
|
|
348
602
|
dirtyAwareApiClientProxies.add(proxy);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.2.0-beta.
|
|
3
|
+
"version": "2.2.0-beta.11",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@formily/antd-v5": "1.x",
|
|
10
10
|
"@formily/reactive": "2.x",
|
|
11
|
-
"@nocobase/sdk": "2.2.0-beta.
|
|
12
|
-
"@nocobase/shared": "2.2.0-beta.
|
|
11
|
+
"@nocobase/sdk": "2.2.0-beta.11",
|
|
12
|
+
"@nocobase/shared": "2.2.0-beta.11",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
14
|
"axios": "^1.7.0",
|
|
15
15
|
"dayjs": "^1.11.9",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
],
|
|
38
38
|
"author": "NocoBase Team",
|
|
39
39
|
"license": "Apache-2.0",
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "8dab25af2bb83a81da68158d0dcf836f96103d61"
|
|
41
41
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { APIClient, type IResource, type RequestOptions } from '@nocobase/sdk';
|
|
11
12
|
import { FlowContext } from '../../flowContext';
|
|
12
13
|
import { FlowEngine } from '../../flowEngine';
|
|
13
14
|
import { createViewScopedEngine } from '../../ViewScopedFlowEngine';
|
|
@@ -80,6 +81,326 @@ describe('dirtyAwareApiClient', () => {
|
|
|
80
81
|
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
81
82
|
});
|
|
82
83
|
|
|
84
|
+
it('should keep request overrides local to the dirty-aware proxy', async () => {
|
|
85
|
+
const engine = new FlowEngine();
|
|
86
|
+
const request = vi.fn(async () => ({ data: { ok: true } }));
|
|
87
|
+
const api: TestApi = {
|
|
88
|
+
auth: { locale: 'zh-CN' },
|
|
89
|
+
request,
|
|
90
|
+
resource: vi.fn(),
|
|
91
|
+
};
|
|
92
|
+
const originalRequest = api.request;
|
|
93
|
+
const wrappedApi = getWrappedApi(engine, api);
|
|
94
|
+
const delegatedRequest = wrappedApi.request.bind(wrappedApi);
|
|
95
|
+
const requestOverride = vi.fn((config: TestRequestOptions) => delegatedRequest(config));
|
|
96
|
+
|
|
97
|
+
wrappedApi.request = requestOverride;
|
|
98
|
+
|
|
99
|
+
await wrappedApi.request({ url: 'posts:list' });
|
|
100
|
+
|
|
101
|
+
expect(requestOverride).toHaveBeenCalledTimes(1);
|
|
102
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
103
|
+
expect(api.request).toBe(originalRequest);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should keep resource overrides local to the dirty-aware proxy', async () => {
|
|
107
|
+
const engine = new FlowEngine();
|
|
108
|
+
const update = vi.fn(async () => ({ data: { ok: true } }));
|
|
109
|
+
const api: TestApi = {
|
|
110
|
+
auth: { locale: 'zh-CN' },
|
|
111
|
+
request: vi.fn(async () => ({ data: { ok: true } })),
|
|
112
|
+
resource: vi.fn(() => ({ update })),
|
|
113
|
+
};
|
|
114
|
+
const originalResource = api.resource;
|
|
115
|
+
const wrappedApi = getWrappedApi(engine, api);
|
|
116
|
+
const delegatedResource = wrappedApi.resource.bind(wrappedApi);
|
|
117
|
+
const resourceOverride = vi.fn((...args: Parameters<TestApi['resource']>) => delegatedResource(...args));
|
|
118
|
+
|
|
119
|
+
wrappedApi.resource = resourceOverride;
|
|
120
|
+
|
|
121
|
+
await wrappedApi.resource('posts').update({ filterByTk: 1 });
|
|
122
|
+
|
|
123
|
+
expect(resourceOverride).toHaveBeenCalledTimes(1);
|
|
124
|
+
expect(update).toHaveBeenCalledTimes(1);
|
|
125
|
+
expect(api.resource).toBe(originalResource);
|
|
126
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should route resource actions through the local request override without double-marking', async () => {
|
|
130
|
+
const engine = new FlowEngine();
|
|
131
|
+
const api = new APIClient();
|
|
132
|
+
const transport = vi.spyOn(api.axios, 'request').mockResolvedValue({ data: { ok: true } });
|
|
133
|
+
const wrappedApi = getDirtyAwareApiClient(api, engine.context) as APIClient;
|
|
134
|
+
const delegatedRequest = wrappedApi.request.bind(wrappedApi);
|
|
135
|
+
const requestOverride = vi.fn((config: Parameters<APIClient['request']>[0]) => delegatedRequest(config));
|
|
136
|
+
|
|
137
|
+
wrappedApi.request = requestOverride as APIClient['request'];
|
|
138
|
+
|
|
139
|
+
await wrappedApi.resource('posts').update({ filterByTk: 1, values: { title: 'updated' } });
|
|
140
|
+
|
|
141
|
+
expect(requestOverride).toHaveBeenCalledTimes(1);
|
|
142
|
+
expect(transport).toHaveBeenCalledTimes(1);
|
|
143
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should invoke custom request methods on the original api instance', async () => {
|
|
147
|
+
class StatefulRequestAPIClient extends APIClient {
|
|
148
|
+
#privateRequestCalls = 0;
|
|
149
|
+
publicRequestCalls = 0;
|
|
150
|
+
|
|
151
|
+
get privateRequestCalls() {
|
|
152
|
+
return this.#privateRequestCalls;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
override request<T, R, D>(config: Parameters<APIClient['request']>[0]): Promise<R> {
|
|
156
|
+
this.#privateRequestCalls += 1;
|
|
157
|
+
this.publicRequestCalls += 1;
|
|
158
|
+
return super.request<T, R, D>(config);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const engine = new FlowEngine();
|
|
163
|
+
const api = new StatefulRequestAPIClient();
|
|
164
|
+
const transport = vi.spyOn(api.axios, 'request').mockResolvedValue({ data: { ok: true } });
|
|
165
|
+
const wrappedApi = getDirtyAwareApiClient(api, engine.context) as APIClient;
|
|
166
|
+
|
|
167
|
+
await wrappedApi.resource('posts').update({ filterByTk: 1, values: { title: 'updated' } });
|
|
168
|
+
|
|
169
|
+
expect(transport).toHaveBeenCalledTimes(1);
|
|
170
|
+
expect(api.privateRequestCalls).toBe(1);
|
|
171
|
+
expect(api.publicRequestCalls).toBe(1);
|
|
172
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should invoke custom resource methods on the original api instance', async () => {
|
|
176
|
+
class StatefulResourceAPIClient extends APIClient {
|
|
177
|
+
#privateResourceCalls = 0;
|
|
178
|
+
publicResourceCalls = 0;
|
|
179
|
+
|
|
180
|
+
get privateResourceCalls() {
|
|
181
|
+
return this.#privateResourceCalls;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
override resource(...args: Parameters<APIClient['resource']>): IResource {
|
|
185
|
+
this.#privateResourceCalls += 1;
|
|
186
|
+
this.publicResourceCalls += 1;
|
|
187
|
+
return super.resource(...args);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const engine = new FlowEngine();
|
|
192
|
+
const api = new StatefulResourceAPIClient();
|
|
193
|
+
const transport = vi.spyOn(api.axios, 'request').mockResolvedValue({ data: { ok: true } });
|
|
194
|
+
const wrappedApi = getDirtyAwareApiClient(api, engine.context) as APIClient;
|
|
195
|
+
|
|
196
|
+
await wrappedApi.request({
|
|
197
|
+
resource: 'posts',
|
|
198
|
+
action: 'update',
|
|
199
|
+
params: { filterByTk: 1, values: { title: 'updated' } },
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(transport).toHaveBeenCalledTimes(1);
|
|
203
|
+
expect(api.privateResourceCalls).toBe(1);
|
|
204
|
+
expect(api.publicResourceCalls).toBe(1);
|
|
205
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should keep one dirty mark when the request override rebuilds the config', async () => {
|
|
209
|
+
const engine = new FlowEngine();
|
|
210
|
+
const api = new APIClient();
|
|
211
|
+
const transport = vi.spyOn(api.axios, 'request').mockResolvedValue({ data: { ok: true } });
|
|
212
|
+
const wrappedApi = getDirtyAwareApiClient(api, engine.context) as APIClient;
|
|
213
|
+
const delegatedRequest = wrappedApi.request.bind(wrappedApi);
|
|
214
|
+
const requestOverride = vi.fn((config: Parameters<APIClient['request']>[0]) => {
|
|
215
|
+
const { url, method, headers, params, data } = config as RequestOptions;
|
|
216
|
+
return delegatedRequest({ url, method, headers, params, data });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
wrappedApi.request = requestOverride as APIClient['request'];
|
|
220
|
+
|
|
221
|
+
await wrappedApi.resource('posts').update({ filterByTk: 1, values: { title: 'updated' } });
|
|
222
|
+
|
|
223
|
+
expect(requestOverride).toHaveBeenCalledTimes(1);
|
|
224
|
+
expect(transport).toHaveBeenCalledTimes(1);
|
|
225
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should keep nested mutations from a request override independent', async () => {
|
|
229
|
+
const engine = new FlowEngine();
|
|
230
|
+
const api = new APIClient();
|
|
231
|
+
const transport = vi.spyOn(api.axios, 'request').mockResolvedValue({ data: { ok: true } });
|
|
232
|
+
const wrappedApi = getDirtyAwareApiClient(api, engine.context) as APIClient;
|
|
233
|
+
const delegatedRequest = wrappedApi.request.bind(wrappedApi);
|
|
234
|
+
const requestOverride = vi.fn(async (config: Parameters<APIClient['request']>[0]) => {
|
|
235
|
+
await delegatedRequest({ url: 'comments:create', method: 'post' });
|
|
236
|
+
return delegatedRequest(config);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
wrappedApi.request = requestOverride as APIClient['request'];
|
|
240
|
+
|
|
241
|
+
await wrappedApi.resource('posts').update({ filterByTk: 1, values: { title: 'updated' } });
|
|
242
|
+
|
|
243
|
+
expect(requestOverride).toHaveBeenCalledTimes(1);
|
|
244
|
+
expect(transport).toHaveBeenCalledTimes(2);
|
|
245
|
+
expect(engine.getDataSourceDirtyVersion('main', 'comments')).toBe(1);
|
|
246
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should route structured requests through the local resource override without double-marking', async () => {
|
|
250
|
+
const engine = new FlowEngine();
|
|
251
|
+
const api = new APIClient();
|
|
252
|
+
const transport = vi.spyOn(api.axios, 'request').mockResolvedValue({ data: { ok: true } });
|
|
253
|
+
const wrappedApi = getDirtyAwareApiClient(api, engine.context) as APIClient;
|
|
254
|
+
const delegatedResource = wrappedApi.resource.bind(wrappedApi);
|
|
255
|
+
const resourceOverride = vi.fn((...args: Parameters<APIClient['resource']>) => delegatedResource(...args));
|
|
256
|
+
|
|
257
|
+
wrappedApi.resource = resourceOverride;
|
|
258
|
+
|
|
259
|
+
await wrappedApi.request({
|
|
260
|
+
resource: 'posts',
|
|
261
|
+
action: 'update',
|
|
262
|
+
params: { filterByTk: 1, values: { title: 'updated' } },
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(resourceOverride).toHaveBeenCalledTimes(1);
|
|
266
|
+
expect(transport).toHaveBeenCalledTimes(1);
|
|
267
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should keep one dirty mark when the resource override delegates lazily', async () => {
|
|
271
|
+
const engine = new FlowEngine();
|
|
272
|
+
const api = new APIClient();
|
|
273
|
+
const transport = vi.spyOn(api.axios, 'request').mockResolvedValue({ data: { ok: true } });
|
|
274
|
+
const wrappedApi = getDirtyAwareApiClient(api, engine.context) as APIClient;
|
|
275
|
+
const delegatedResource = wrappedApi.resource.bind(wrappedApi);
|
|
276
|
+
const resourceOverride = vi.fn(
|
|
277
|
+
(...args: Parameters<APIClient['resource']>): IResource => ({
|
|
278
|
+
update: async (...actionArgs: Parameters<IResource['update']>) => {
|
|
279
|
+
await Promise.resolve();
|
|
280
|
+
return delegatedResource(...args).update(...actionArgs);
|
|
281
|
+
},
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
wrappedApi.resource = resourceOverride;
|
|
286
|
+
|
|
287
|
+
await wrappedApi.request({
|
|
288
|
+
resource: 'posts',
|
|
289
|
+
action: 'update',
|
|
290
|
+
params: { filterByTk: 1, values: { title: 'updated' } },
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
expect(resourceOverride).toHaveBeenCalledTimes(1);
|
|
294
|
+
expect(transport).toHaveBeenCalledTimes(1);
|
|
295
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should keep nested mutations from a resource override independent', async () => {
|
|
299
|
+
const engine = new FlowEngine();
|
|
300
|
+
const api = new APIClient();
|
|
301
|
+
const transport = vi.spyOn(api.axios, 'request').mockResolvedValue({ data: { ok: true } });
|
|
302
|
+
const wrappedApi = getDirtyAwareApiClient(api, engine.context) as APIClient;
|
|
303
|
+
const delegatedResource = wrappedApi.resource.bind(wrappedApi);
|
|
304
|
+
let nestedMutation: Promise<unknown> | undefined;
|
|
305
|
+
const resourceOverride = vi.fn((...args: Parameters<APIClient['resource']>) => {
|
|
306
|
+
nestedMutation = delegatedResource('comments').create({ values: { body: 'nested' } });
|
|
307
|
+
return delegatedResource(...args);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
wrappedApi.resource = resourceOverride;
|
|
311
|
+
|
|
312
|
+
await wrappedApi.request({
|
|
313
|
+
resource: 'posts',
|
|
314
|
+
action: 'update',
|
|
315
|
+
params: { filterByTk: 1, values: { title: 'updated' } },
|
|
316
|
+
});
|
|
317
|
+
await nestedMutation;
|
|
318
|
+
|
|
319
|
+
expect(resourceOverride).toHaveBeenCalledTimes(1);
|
|
320
|
+
expect(transport).toHaveBeenCalledTimes(2);
|
|
321
|
+
expect(engine.getDataSourceDirtyVersion('main', 'comments')).toBe(1);
|
|
322
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should clear local request and resource overrides when deleted', () => {
|
|
326
|
+
const engine = new FlowEngine();
|
|
327
|
+
const api: TestApi = {
|
|
328
|
+
auth: { locale: 'zh-CN' },
|
|
329
|
+
request: vi.fn(async () => ({ data: { ok: true } })),
|
|
330
|
+
resource: vi.fn(() => ({})),
|
|
331
|
+
};
|
|
332
|
+
const wrappedApi = getWrappedApi(engine, api);
|
|
333
|
+
const defaultRequest = wrappedApi.request;
|
|
334
|
+
const defaultResource = wrappedApi.resource;
|
|
335
|
+
|
|
336
|
+
wrappedApi.request = vi.fn();
|
|
337
|
+
wrappedApi.resource = vi.fn();
|
|
338
|
+
|
|
339
|
+
expect(Reflect.deleteProperty(wrappedApi, 'request')).toBe(true);
|
|
340
|
+
expect(Reflect.deleteProperty(wrappedApi, 'resource')).toBe(true);
|
|
341
|
+
expect(wrappedApi.request).toBe(defaultRequest);
|
|
342
|
+
expect(wrappedApi.resource).toBe(defaultResource);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should reject request and resource descriptor overrides without mutating the raw api', () => {
|
|
346
|
+
const engine = new FlowEngine();
|
|
347
|
+
const api: TestApi = {
|
|
348
|
+
auth: { locale: 'zh-CN' },
|
|
349
|
+
request: vi.fn(async () => ({ data: { ok: true } })),
|
|
350
|
+
resource: vi.fn(() => ({})),
|
|
351
|
+
};
|
|
352
|
+
const wrappedApi = getWrappedApi(engine, api);
|
|
353
|
+
const originalRequest = api.request;
|
|
354
|
+
const originalResource = api.resource;
|
|
355
|
+
|
|
356
|
+
expect(Reflect.defineProperty(wrappedApi, 'request', { value: vi.fn() })).toBe(false);
|
|
357
|
+
expect(Reflect.defineProperty(wrappedApi, 'resource', { value: vi.fn() })).toBe(false);
|
|
358
|
+
expect(() => Object.defineProperty(wrappedApi, 'request', { value: vi.fn() })).toThrow(TypeError);
|
|
359
|
+
expect(() => Object.defineProperty(wrappedApi, 'resource', { value: vi.fn() })).toThrow(TypeError);
|
|
360
|
+
expect(api.request).toBe(originalRequest);
|
|
361
|
+
expect(api.resource).toBe(originalResource);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should reject local overrides for own methods on a non-extensible raw api', () => {
|
|
365
|
+
const engine = new FlowEngine();
|
|
366
|
+
const api: TestApi = {
|
|
367
|
+
auth: { locale: 'zh-CN' },
|
|
368
|
+
request: vi.fn(async () => ({ data: { ok: true } })),
|
|
369
|
+
resource: vi.fn(() => ({})),
|
|
370
|
+
};
|
|
371
|
+
const wrappedApi = getWrappedApi(engine, api);
|
|
372
|
+
const defaultMethods = {
|
|
373
|
+
request: wrappedApi.request,
|
|
374
|
+
resource: wrappedApi.resource,
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
Object.preventExtensions(api);
|
|
378
|
+
|
|
379
|
+
for (const methodName of ['request', 'resource'] as const) {
|
|
380
|
+
expect(Reflect.set(wrappedApi, methodName, vi.fn())).toBe(false);
|
|
381
|
+
expect(wrappedApi[methodName]).toBe(defaultMethods[methodName]);
|
|
382
|
+
expect(Reflect.deleteProperty(wrappedApi, methodName)).toBe(false);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('should keep overrides isolated between flow contexts', () => {
|
|
387
|
+
const api: TestApi = {
|
|
388
|
+
auth: { locale: 'zh-CN' },
|
|
389
|
+
request: vi.fn(async () => ({ data: { ok: true } })),
|
|
390
|
+
resource: vi.fn(() => ({})),
|
|
391
|
+
};
|
|
392
|
+
const firstContextApi = getDirtyAwareApiClient(api, new FlowContext()) as TestApi;
|
|
393
|
+
const secondContextApi = getDirtyAwareApiClient(api, new FlowContext()) as TestApi;
|
|
394
|
+
const secondRequest = secondContextApi.request;
|
|
395
|
+
const requestOverride = vi.fn();
|
|
396
|
+
|
|
397
|
+
firstContextApi.request = requestOverride;
|
|
398
|
+
|
|
399
|
+
expect(firstContextApi.request).toBe(requestOverride);
|
|
400
|
+
expect(secondContextApi.request).toBe(secondRequest);
|
|
401
|
+
expect(api.request).not.toBe(requestOverride);
|
|
402
|
+
});
|
|
403
|
+
|
|
83
404
|
it('should not mark dirty for read actions', async () => {
|
|
84
405
|
const nonMutatingActions = [
|
|
85
406
|
'get',
|
|
@@ -7,13 +7,28 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import { APIClient } from '@nocobase/sdk';
|
|
11
|
+
import type { ActionParams, IResource, RequestOptions } from '@nocobase/sdk';
|
|
11
12
|
import type { FlowContext } from '../flowContext';
|
|
12
13
|
import { getDataSourceKeyFromHeaders, markDataSourceDirty } from './dataSourceDirty';
|
|
13
14
|
|
|
14
15
|
type ResourceActionFn = (params?: ActionParams, opts?: unknown) => Promise<unknown>;
|
|
15
16
|
|
|
16
17
|
export const SKIP_DATA_SOURCE_DIRTY = '__nocobaseSkipDataSourceDirty';
|
|
18
|
+
// Carries one logical mutation through APIClient's request/resource cross-dispatch.
|
|
19
|
+
const DIRTY_DISPATCH_TOKEN = Symbol('nocobaseDirtyDispatchToken');
|
|
20
|
+
|
|
21
|
+
type DirtyDispatchToken = {
|
|
22
|
+
marked: boolean;
|
|
23
|
+
requestKey?: string;
|
|
24
|
+
resourceKey?: string;
|
|
25
|
+
skip: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type DirtyDispatchFrame = {
|
|
29
|
+
key?: string;
|
|
30
|
+
token: DirtyDispatchToken;
|
|
31
|
+
};
|
|
17
32
|
|
|
18
33
|
type ResourceRequestOptions = RequestOptions & {
|
|
19
34
|
resource?: unknown;
|
|
@@ -21,6 +36,7 @@ type ResourceRequestOptions = RequestOptions & {
|
|
|
21
36
|
action?: unknown;
|
|
22
37
|
headers?: unknown;
|
|
23
38
|
[SKIP_DATA_SOURCE_DIRTY]?: boolean;
|
|
39
|
+
[DIRTY_DISPATCH_TOKEN]?: DirtyDispatchToken;
|
|
24
40
|
};
|
|
25
41
|
|
|
26
42
|
type DirtyResourceAction = {
|
|
@@ -329,6 +345,53 @@ function resolveDirtyResourceAction(
|
|
|
329
345
|
return parseDirtyResourceActionFromUrl(options?.url, context);
|
|
330
346
|
}
|
|
331
347
|
|
|
348
|
+
function getDirtyResourceActionDispatchKey(
|
|
349
|
+
dirtyResourceAction: DirtyResourceAction | undefined,
|
|
350
|
+
headers: unknown,
|
|
351
|
+
): string | undefined {
|
|
352
|
+
if (!dirtyResourceAction) {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
return JSON.stringify([
|
|
356
|
+
dirtyResourceAction.dataSourceKey || getDataSourceKeyFromHeaders(headers),
|
|
357
|
+
dirtyResourceAction.resourceName,
|
|
358
|
+
dirtyResourceAction.actionName,
|
|
359
|
+
]);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function getRequestDispatchKey(options: ResourceRequestOptions, context: FlowContext): string | undefined {
|
|
363
|
+
return getDirtyResourceActionDispatchKey(resolveDirtyResourceAction(options, context), options.headers);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function getResourceDispatchKey(name: unknown, of: unknown, headers: unknown): string | undefined {
|
|
367
|
+
const resourceName = String(name ?? '').trim();
|
|
368
|
+
if (!resourceName) {
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
return JSON.stringify([resourceName, String(of ?? ''), getDataSourceKeyFromHeaders(headers)]);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function getMatchingRequestToken(
|
|
375
|
+
token: DirtyDispatchToken | undefined,
|
|
376
|
+
requestKey: string | undefined,
|
|
377
|
+
): DirtyDispatchToken | undefined {
|
|
378
|
+
// A matching key prevents helper requests inside an override from sharing the outer mutation token.
|
|
379
|
+
if (!token || !requestKey || (token.requestKey && token.requestKey !== requestKey)) {
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
return token;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function getMatchingResourceToken(
|
|
386
|
+
token: DirtyDispatchToken | undefined,
|
|
387
|
+
resourceKey: string | undefined,
|
|
388
|
+
): DirtyDispatchToken | undefined {
|
|
389
|
+
if (!token || !resourceKey || (token.resourceKey && token.resourceKey !== resourceKey)) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
return token;
|
|
393
|
+
}
|
|
394
|
+
|
|
332
395
|
function markResourceActionDataSourceDirty(
|
|
333
396
|
context: FlowContext,
|
|
334
397
|
dirtyResourceAction: DirtyResourceAction,
|
|
@@ -342,12 +405,31 @@ function markResourceActionDataSourceDirty(
|
|
|
342
405
|
});
|
|
343
406
|
}
|
|
344
407
|
|
|
408
|
+
function markResourceActionDataSourceDirtyOnce(
|
|
409
|
+
token: DirtyDispatchToken,
|
|
410
|
+
context: FlowContext,
|
|
411
|
+
dirtyResourceAction: DirtyResourceAction | undefined,
|
|
412
|
+
headers: unknown,
|
|
413
|
+
) {
|
|
414
|
+
if (token.skip || token.marked || !dirtyResourceAction || !isMutatingResourceAction(dirtyResourceAction.actionName)) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
token.marked = true;
|
|
418
|
+
markResourceActionDataSourceDirty(context, dirtyResourceAction, headers);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function isObjectRecord(value: unknown): value is Record<PropertyKey, unknown> {
|
|
422
|
+
return !!value && typeof value === 'object';
|
|
423
|
+
}
|
|
424
|
+
|
|
345
425
|
function createDirtyAwareResource(
|
|
346
426
|
context: FlowContext,
|
|
347
427
|
resource: IResource,
|
|
348
428
|
resourceName: string,
|
|
349
429
|
resourceOf: unknown,
|
|
350
430
|
headers: unknown,
|
|
431
|
+
requestTokenStack: DirtyDispatchFrame[],
|
|
432
|
+
parentToken?: DirtyDispatchToken,
|
|
351
433
|
): IResource {
|
|
352
434
|
return new Proxy(resource, {
|
|
353
435
|
get(target, prop, receiver) {
|
|
@@ -358,10 +440,42 @@ function createDirtyAwareResource(
|
|
|
358
440
|
|
|
359
441
|
const action = original as ResourceActionFn;
|
|
360
442
|
return async (...args: Parameters<ResourceActionFn>) => {
|
|
361
|
-
const
|
|
443
|
+
const actionOptions = isObjectRecord(args[1]) ? args[1] : undefined;
|
|
362
444
|
const dirtyResourceAction = resolveDirtyResourceActionFromResource(resourceName, resourceOf, prop, context);
|
|
363
|
-
|
|
364
|
-
|
|
445
|
+
const requestKey = getDirtyResourceActionDispatchKey(dirtyResourceAction, headers);
|
|
446
|
+
const resourceKey = getResourceDispatchKey(resourceName, resourceOf, headers);
|
|
447
|
+
const inheritedToken =
|
|
448
|
+
getMatchingRequestToken(
|
|
449
|
+
actionOptions?.[DIRTY_DISPATCH_TOKEN] as DirtyDispatchToken | undefined,
|
|
450
|
+
requestKey,
|
|
451
|
+
) || getMatchingRequestToken(parentToken, requestKey);
|
|
452
|
+
const token = inheritedToken || { marked: false, skip: false };
|
|
453
|
+
const ownsToken = !inheritedToken;
|
|
454
|
+
token.requestKey ||= requestKey;
|
|
455
|
+
token.resourceKey ||= resourceKey;
|
|
456
|
+
if (actionOptions?.[SKIP_DATA_SOURCE_DIRTY]) {
|
|
457
|
+
token.skip = true;
|
|
458
|
+
}
|
|
459
|
+
const forwardedArgs: Parameters<ResourceActionFn> =
|
|
460
|
+
actionOptions || args[1] == null
|
|
461
|
+
? [
|
|
462
|
+
args[0],
|
|
463
|
+
{
|
|
464
|
+
...actionOptions,
|
|
465
|
+
[DIRTY_DISPATCH_TOKEN]: token,
|
|
466
|
+
},
|
|
467
|
+
]
|
|
468
|
+
: args;
|
|
469
|
+
let actionResult: Promise<unknown>;
|
|
470
|
+
requestTokenStack.push({ key: requestKey, token });
|
|
471
|
+
try {
|
|
472
|
+
actionResult = Reflect.apply(action, receiver, forwardedArgs);
|
|
473
|
+
} finally {
|
|
474
|
+
requestTokenStack.pop();
|
|
475
|
+
}
|
|
476
|
+
const result = await actionResult;
|
|
477
|
+
if (ownsToken) {
|
|
478
|
+
markResourceActionDataSourceDirtyOnce(token, context, dirtyResourceAction, headers);
|
|
365
479
|
}
|
|
366
480
|
return result;
|
|
367
481
|
};
|
|
@@ -370,34 +484,232 @@ function createDirtyAwareResource(
|
|
|
370
484
|
}
|
|
371
485
|
|
|
372
486
|
function createDirtyAwareApiClient(api: DirtyAwareAPIClient, context: FlowContext): APIClient {
|
|
487
|
+
const baseResource = api.resource;
|
|
488
|
+
const baseRequest = api.request;
|
|
489
|
+
// SDK methods use the dispatch receiver; custom methods keep the original instance for private fields and state.
|
|
490
|
+
const shouldUseResourceDispatchReceiver = baseResource === APIClient.prototype.resource;
|
|
491
|
+
const shouldUseRequestDispatchReceiver = baseRequest === APIClient.prototype.request;
|
|
492
|
+
// Stacks cover synchronous delegation; the symbol token survives async overrides that forward arguments.
|
|
493
|
+
const resourceTokenStack: DirtyDispatchFrame[] = [];
|
|
494
|
+
const requestTokenStack: DirtyDispatchFrame[] = [];
|
|
495
|
+
let hasResourceOverride = false;
|
|
496
|
+
let resourceOverride: unknown;
|
|
497
|
+
let hasRequestOverride = false;
|
|
498
|
+
let requestOverride: unknown;
|
|
499
|
+
|
|
500
|
+
const getCurrentResource = () => (hasResourceOverride ? resourceOverride : resource) as APIClient['resource'];
|
|
501
|
+
const getCurrentRequest = () => (hasRequestOverride ? requestOverride : request) as APIClient['request'];
|
|
502
|
+
|
|
503
|
+
const dispatchResource = (
|
|
504
|
+
token: DirtyDispatchToken | undefined,
|
|
505
|
+
args: Parameters<APIClient['resource']>,
|
|
506
|
+
): IResource => {
|
|
507
|
+
const resourceKey = getResourceDispatchKey(args[0], args[1], args[2]);
|
|
508
|
+
const activeToken = getMatchingResourceToken(token, resourceKey);
|
|
509
|
+
const shouldWrapActions = !!activeToken && hasResourceOverride;
|
|
510
|
+
if (activeToken) {
|
|
511
|
+
activeToken.resourceKey ||= resourceKey;
|
|
512
|
+
resourceTokenStack.push({ key: resourceKey, token: activeToken });
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
const resourceInstance = Reflect.apply(getCurrentResource(), proxy, args);
|
|
516
|
+
if (!shouldWrapActions || !activeToken) {
|
|
517
|
+
return resourceInstance;
|
|
518
|
+
}
|
|
519
|
+
return new Proxy(resourceInstance, {
|
|
520
|
+
get(target, prop, receiver) {
|
|
521
|
+
const original = Reflect.get(target, prop, receiver);
|
|
522
|
+
if (typeof prop !== 'string' || typeof original !== 'function' || !isMutatingResourceAction(prop)) {
|
|
523
|
+
return original;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const action = original as ResourceActionFn;
|
|
527
|
+
return (...actionArgs: Parameters<ResourceActionFn>) => {
|
|
528
|
+
const actionOptions = isObjectRecord(actionArgs[1]) ? actionArgs[1] : undefined;
|
|
529
|
+
const forwardedArgs: Parameters<ResourceActionFn> =
|
|
530
|
+
actionOptions || actionArgs[1] == null
|
|
531
|
+
? [
|
|
532
|
+
actionArgs[0],
|
|
533
|
+
{
|
|
534
|
+
...actionOptions,
|
|
535
|
+
[DIRTY_DISPATCH_TOKEN]: activeToken,
|
|
536
|
+
},
|
|
537
|
+
]
|
|
538
|
+
: actionArgs;
|
|
539
|
+
resourceTokenStack.push({ key: resourceKey, token: activeToken });
|
|
540
|
+
requestTokenStack.push({ key: activeToken.requestKey, token: activeToken });
|
|
541
|
+
try {
|
|
542
|
+
return Reflect.apply(action, receiver, forwardedArgs);
|
|
543
|
+
} finally {
|
|
544
|
+
requestTokenStack.pop();
|
|
545
|
+
resourceTokenStack.pop();
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
} finally {
|
|
551
|
+
if (activeToken) {
|
|
552
|
+
resourceTokenStack.pop();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const createDispatchReceiver = (token?: DirtyDispatchToken): DirtyAwareAPIClient => {
|
|
558
|
+
const receiver = Object.create(api) as DirtyAwareAPIClient;
|
|
559
|
+
Object.defineProperties(receiver, {
|
|
560
|
+
request: {
|
|
561
|
+
configurable: true,
|
|
562
|
+
value: (<T, R, D>(config: APIClientRequestConfig): Promise<R> => {
|
|
563
|
+
const options = config as ResourceRequestOptions;
|
|
564
|
+
const requestKey = getRequestDispatchKey(options, context);
|
|
565
|
+
const stackFrame = requestTokenStack.at(-1);
|
|
566
|
+
const activeToken =
|
|
567
|
+
getMatchingRequestToken(options?.[DIRTY_DISPATCH_TOKEN], requestKey) ||
|
|
568
|
+
(requestKey && stackFrame?.key === requestKey ? stackFrame.token : undefined) ||
|
|
569
|
+
getMatchingRequestToken(token, requestKey);
|
|
570
|
+
const { [DIRTY_DISPATCH_TOKEN]: _dirtyDispatchToken, ...cleanOptions } = options;
|
|
571
|
+
const configWithToken = activeToken ? { ...cleanOptions, [DIRTY_DISPATCH_TOKEN]: activeToken } : cleanOptions;
|
|
572
|
+
if (activeToken) {
|
|
573
|
+
activeToken.requestKey ||= requestKey;
|
|
574
|
+
requestTokenStack.push({ key: requestKey, token: activeToken });
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
return Reflect.apply(getCurrentRequest(), proxy, [configWithToken]) as Promise<R>;
|
|
578
|
+
} finally {
|
|
579
|
+
if (activeToken) {
|
|
580
|
+
requestTokenStack.pop();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}) as APIClient['request'],
|
|
584
|
+
},
|
|
585
|
+
resource: {
|
|
586
|
+
configurable: true,
|
|
587
|
+
value: ((...args: Parameters<APIClient['resource']>) => {
|
|
588
|
+
const resourceKey = getResourceDispatchKey(args[0], args[1], args[2]);
|
|
589
|
+
const stackFrame = resourceTokenStack.at(-1);
|
|
590
|
+
const activeToken =
|
|
591
|
+
getMatchingResourceToken(token, resourceKey) ||
|
|
592
|
+
(resourceKey && stackFrame?.key === resourceKey ? stackFrame.token : undefined);
|
|
593
|
+
return dispatchResource(activeToken, args);
|
|
594
|
+
}) as APIClient['resource'],
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
return receiver;
|
|
598
|
+
};
|
|
599
|
+
|
|
373
600
|
const resource: APIClient['resource'] = (name, of, headers, cancel) => {
|
|
374
|
-
const
|
|
375
|
-
|
|
601
|
+
const resourceKey = getResourceDispatchKey(name, of, headers);
|
|
602
|
+
const stackFrame = resourceTokenStack.at(-1);
|
|
603
|
+
const parentToken = resourceKey && stackFrame?.key === resourceKey ? stackFrame.token : undefined;
|
|
604
|
+
const receiver = createDispatchReceiver(parentToken);
|
|
605
|
+
const resourceInstance = Reflect.apply(baseResource, shouldUseResourceDispatchReceiver ? receiver : api, [
|
|
606
|
+
name,
|
|
607
|
+
of,
|
|
608
|
+
headers,
|
|
609
|
+
cancel,
|
|
610
|
+
]);
|
|
611
|
+
return createDirtyAwareResource(context, resourceInstance, name, of, headers, requestTokenStack, parentToken);
|
|
376
612
|
};
|
|
377
613
|
|
|
378
614
|
const request = (<T, R, D>(config: APIClientRequestConfig): Promise<R> => {
|
|
379
615
|
const options = config as ResourceRequestOptions;
|
|
616
|
+
const requestKey = getRequestDispatchKey(options, context);
|
|
617
|
+
const stackFrame = requestTokenStack.at(-1);
|
|
618
|
+
const inheritedToken =
|
|
619
|
+
getMatchingRequestToken(options?.[DIRTY_DISPATCH_TOKEN], requestKey) ||
|
|
620
|
+
(requestKey && stackFrame?.key === requestKey ? stackFrame.token : undefined);
|
|
621
|
+
const token = inheritedToken || { marked: false, skip: false };
|
|
622
|
+
const ownsToken = !inheritedToken;
|
|
623
|
+
token.requestKey ||= requestKey;
|
|
624
|
+
if (typeof options.resource === 'string') {
|
|
625
|
+
token.resourceKey ||= getResourceDispatchKey(options.resource, options.resourceOf, options.headers);
|
|
626
|
+
}
|
|
380
627
|
const skipDataSourceDirty = options?.[SKIP_DATA_SOURCE_DIRTY];
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
628
|
+
if (skipDataSourceDirty) {
|
|
629
|
+
token.skip = true;
|
|
630
|
+
}
|
|
631
|
+
const dirtyResourceAction = resolveDirtyResourceAction(options, context);
|
|
632
|
+
const {
|
|
633
|
+
[DIRTY_DISPATCH_TOKEN]: _dirtyDispatchToken,
|
|
634
|
+
[SKIP_DATA_SOURCE_DIRTY]: _skipDataSourceDirty,
|
|
635
|
+
...cleanConfig
|
|
636
|
+
} = options;
|
|
637
|
+
const receiver = createDispatchReceiver(token);
|
|
638
|
+
return (
|
|
639
|
+
Reflect.apply(baseRequest, shouldUseRequestDispatchReceiver ? receiver : api, [cleanConfig]) as Promise<R>
|
|
640
|
+
).then((result) => {
|
|
641
|
+
if (ownsToken) {
|
|
642
|
+
markResourceActionDataSourceDirtyOnce(token, context, dirtyResourceAction, options.headers);
|
|
386
643
|
}
|
|
387
644
|
return result;
|
|
388
645
|
});
|
|
389
646
|
}) as APIClient['request'];
|
|
390
647
|
|
|
648
|
+
const isLockedOwnProperty = (target: DirtyAwareAPIClient, prop: PropertyKey) => {
|
|
649
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
650
|
+
return !!descriptor && !descriptor.configurable;
|
|
651
|
+
};
|
|
652
|
+
|
|
391
653
|
const proxy = new Proxy(api, {
|
|
392
654
|
get(target, prop, receiver) {
|
|
393
655
|
if (prop === 'resource') {
|
|
394
|
-
|
|
656
|
+
if (isLockedOwnProperty(target, prop)) {
|
|
657
|
+
return Reflect.get(target, prop, receiver);
|
|
658
|
+
}
|
|
659
|
+
return hasResourceOverride ? resourceOverride : resource;
|
|
395
660
|
}
|
|
396
661
|
if (prop === 'request') {
|
|
397
|
-
|
|
662
|
+
if (isLockedOwnProperty(target, prop)) {
|
|
663
|
+
return Reflect.get(target, prop, receiver);
|
|
664
|
+
}
|
|
665
|
+
return hasRequestOverride ? requestOverride : request;
|
|
398
666
|
}
|
|
399
667
|
return Reflect.get(target, prop, receiver);
|
|
400
668
|
},
|
|
669
|
+
set(target, prop, value, receiver) {
|
|
670
|
+
if (prop === 'resource') {
|
|
671
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
672
|
+
if (descriptor && (!descriptor.configurable || !Reflect.isExtensible(target))) {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
hasResourceOverride = true;
|
|
676
|
+
resourceOverride = value;
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
if (prop === 'request') {
|
|
680
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
681
|
+
if (descriptor && (!descriptor.configurable || !Reflect.isExtensible(target))) {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
hasRequestOverride = true;
|
|
685
|
+
requestOverride = value;
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
return Reflect.set(target, prop, value, receiver);
|
|
689
|
+
},
|
|
690
|
+
deleteProperty(target, prop) {
|
|
691
|
+
if (prop !== 'resource' && prop !== 'request') {
|
|
692
|
+
return Reflect.deleteProperty(target, prop);
|
|
693
|
+
}
|
|
694
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
695
|
+
if (descriptor && (!descriptor.configurable || !Reflect.isExtensible(target))) {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
if (prop === 'resource') {
|
|
699
|
+
hasResourceOverride = false;
|
|
700
|
+
resourceOverride = undefined;
|
|
701
|
+
} else {
|
|
702
|
+
hasRequestOverride = false;
|
|
703
|
+
requestOverride = undefined;
|
|
704
|
+
}
|
|
705
|
+
return true;
|
|
706
|
+
},
|
|
707
|
+
defineProperty(target, prop, descriptor) {
|
|
708
|
+
if (prop === 'resource' || prop === 'request') {
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
return Reflect.defineProperty(target, prop, descriptor);
|
|
712
|
+
},
|
|
401
713
|
}) as APIClient;
|
|
402
714
|
dirtyAwareApiClientProxies.add(proxy);
|
|
403
715
|
return proxy;
|