@milaboratories/pl-client 2.4.20 → 2.5.0
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/dist/core/cache.d.ts +9 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/client.d.ts +5 -0
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/config.d.ts +5 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/final.d.ts +15 -0
- package/dist/core/final.d.ts.map +1 -0
- package/dist/core/ll_client.d.ts.map +1 -1
- package/dist/core/transaction.d.ts +17 -1
- package/dist/core/transaction.d.ts.map +1 -1
- package/dist/core/types.d.ts +14 -13
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1434 -1306
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/core/cache.ts +9 -0
- package/src/core/client.ts +27 -1
- package/src/core/config.ts +16 -2
- package/src/core/final.ts +84 -0
- package/src/core/ll_client.ts +8 -1
- package/src/core/transaction.ts +84 -5
- package/src/core/types.ts +39 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milaboratories/pl-client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "New TS/JS client for Platform API",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"@protobuf-ts/runtime-rpc": "^2.9.4",
|
|
24
24
|
"canonicalize": "^2.0.0",
|
|
25
25
|
"denque": "^2.1.0",
|
|
26
|
+
"lru-cache": "^11.0.1",
|
|
26
27
|
"https-proxy-agent": "^7.0.5",
|
|
27
28
|
"cacheable-lookup": "^6.1.0",
|
|
28
29
|
"long": "^5.2.3",
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
"jest": "^29.7.0",
|
|
42
43
|
"@jest/globals": "^29.7.0",
|
|
43
44
|
"ts-jest": "^29.2.5",
|
|
44
|
-
"@milaboratories/platforma-build-configs": "1.0.
|
|
45
|
+
"@milaboratories/platforma-build-configs": "1.0.2"
|
|
45
46
|
},
|
|
46
47
|
"scripts": {
|
|
47
48
|
"type-check": "tsc --noEmit --composite false",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BasicResourceData, ResourceData } from './types';
|
|
2
|
+
|
|
3
|
+
export type ResourceDataCacheRecord = {
|
|
4
|
+
/** There is a slight chance of inconsistent data retrieval from tx if we allow later transactions to leak resource data into earlier transactions.
|
|
5
|
+
* This field allows to prevent this. */
|
|
6
|
+
cacheTxOpenTimestamp: number;
|
|
7
|
+
data: ResourceData | undefined;
|
|
8
|
+
readonly basicData: BasicResourceData;
|
|
9
|
+
};
|
package/src/core/client.ts
CHANGED
|
@@ -20,6 +20,9 @@ import { PlDriver, PlDriverDefinition } from './driver';
|
|
|
20
20
|
import { MaintenanceAPI_Ping_Response } from '../proto/github.com/milaboratory/pl/plapi/plapiproto/api';
|
|
21
21
|
import * as tp from 'node:timers/promises';
|
|
22
22
|
import { Dispatcher } from 'undici';
|
|
23
|
+
import { LRUCache } from 'lru-cache';
|
|
24
|
+
import { ResourceDataCacheRecord } from './cache';
|
|
25
|
+
import { DefaultFinalResourceDataPredicate, FinalResourceDataPredicate } from './final';
|
|
23
26
|
|
|
24
27
|
export type TxOps = PlCallOps & {
|
|
25
28
|
sync?: boolean;
|
|
@@ -56,17 +59,33 @@ export class PlClient {
|
|
|
56
59
|
|
|
57
60
|
private _serverInfo: MaintenanceAPI_Ping_Response | undefined = undefined;
|
|
58
61
|
|
|
62
|
+
//
|
|
63
|
+
// Caching
|
|
64
|
+
//
|
|
65
|
+
|
|
66
|
+
/** This function determines whether resource data can be cached */
|
|
67
|
+
public readonly finalPredicate: FinalResourceDataPredicate;
|
|
68
|
+
|
|
69
|
+
/** Resource data cache, to minimize redundant data rereading from remote db */
|
|
70
|
+
private readonly resourceDataCache: LRUCache<ResourceId, ResourceDataCacheRecord>;
|
|
71
|
+
|
|
59
72
|
private constructor(
|
|
60
73
|
configOrAddress: PlClientConfig | string,
|
|
61
74
|
auth: AuthOps,
|
|
62
75
|
ops: {
|
|
63
76
|
statusListener?: PlConnectionStatusListener;
|
|
77
|
+
finalPredicate?: FinalResourceDataPredicate;
|
|
64
78
|
} = {}
|
|
65
79
|
) {
|
|
66
80
|
this.ll = new LLPlClient(configOrAddress, { auth, ...ops });
|
|
67
81
|
const conf = this.ll.conf;
|
|
68
82
|
this.txDelay = conf.txDelay;
|
|
69
83
|
this.forceSync = conf.forceSync;
|
|
84
|
+
this.finalPredicate = ops.finalPredicate ?? DefaultFinalResourceDataPredicate;
|
|
85
|
+
this.resourceDataCache = new LRUCache({
|
|
86
|
+
maxSize: conf.maxCacheBytes,
|
|
87
|
+
sizeCalculation: (v) => (v.basicData.data?.length ?? 0) + 64
|
|
88
|
+
});
|
|
70
89
|
switch (conf.retryBackoffAlgorithm) {
|
|
71
90
|
case 'exponential':
|
|
72
91
|
this.defaultRetryOptions = {
|
|
@@ -199,7 +218,14 @@ export class PlClient {
|
|
|
199
218
|
// opening low-level tx
|
|
200
219
|
const llTx = this.ll.createTx(ops);
|
|
201
220
|
// wrapping it into high-level tx (this also asynchronously sends initialization message)
|
|
202
|
-
const tx = new PlTransaction(
|
|
221
|
+
const tx = new PlTransaction(
|
|
222
|
+
llTx,
|
|
223
|
+
name,
|
|
224
|
+
writable,
|
|
225
|
+
clientRoot,
|
|
226
|
+
this.finalPredicate,
|
|
227
|
+
this.resourceDataCache
|
|
228
|
+
);
|
|
203
229
|
|
|
204
230
|
let ok = false;
|
|
205
231
|
let result: T | undefined = undefined;
|
package/src/core/config.ts
CHANGED
|
@@ -45,6 +45,9 @@ export interface PlClientConfig {
|
|
|
45
45
|
/** Last resort measure to solve complicated race conditions in pl. */
|
|
46
46
|
forceSync: boolean;
|
|
47
47
|
|
|
48
|
+
/** Maximal number of bytes of resource state to cache */
|
|
49
|
+
maxCacheBytes: number;
|
|
50
|
+
|
|
48
51
|
//
|
|
49
52
|
// Retry
|
|
50
53
|
//
|
|
@@ -54,23 +57,30 @@ export interface PlClientConfig {
|
|
|
54
57
|
* (pl uses optimistic transaction model with regular retries in write transactions)
|
|
55
58
|
* */
|
|
56
59
|
retryBackoffAlgorithm: 'exponential' | 'linear';
|
|
60
|
+
|
|
57
61
|
/** Maximal number of attempts in */
|
|
58
62
|
retryMaxAttempts: number;
|
|
63
|
+
|
|
59
64
|
/** Delay after first failed attempt, in ms. */
|
|
60
65
|
retryInitialDelay: number;
|
|
66
|
+
|
|
61
67
|
/** Each time delay will be multiplied by this number (1.5 means plus on 50% each attempt) */
|
|
62
68
|
retryExponentialBackoffMultiplier: number;
|
|
69
|
+
|
|
63
70
|
/** [used only for ] This value will be added to the delay from the previous step, in ms */
|
|
64
71
|
retryLinearBackoffStep: number;
|
|
72
|
+
|
|
65
73
|
/** Value from 0 to 1, determine level of randomness to introduce to the backoff delays sequence. (0 meaning no randomness) */
|
|
66
74
|
retryJitter: number;
|
|
67
75
|
}
|
|
68
76
|
|
|
69
|
-
export const DEFAULT_REQUEST_TIMEOUT =
|
|
70
|
-
export const DEFAULT_TX_TIMEOUT =
|
|
77
|
+
export const DEFAULT_REQUEST_TIMEOUT = 2000;
|
|
78
|
+
export const DEFAULT_TX_TIMEOUT = 30_000;
|
|
71
79
|
export const DEFAULT_TOKEN_TTL_SECONDS = 31 * 24 * 60 * 60;
|
|
72
80
|
export const DEFAULT_AUTH_MAX_REFRESH = 12 * 24 * 60 * 60;
|
|
73
81
|
|
|
82
|
+
export const DEFAULT_MAX_CACHE_BYTES = 35_000_000; // 35 Mb
|
|
83
|
+
|
|
74
84
|
export const DEFAULT_RETRY_BACKOFF_ALGORITHM = 'exponential';
|
|
75
85
|
export const DEFAULT_RETRY_MAX_ATTEMPTS = 10;
|
|
76
86
|
export const DEFAULT_RETRY_INITIAL_DELAY = 4; // 4 ms
|
|
@@ -110,6 +120,8 @@ export function plAddressToConfig(
|
|
|
110
120
|
txDelay: 0,
|
|
111
121
|
forceSync: false,
|
|
112
122
|
|
|
123
|
+
maxCacheBytes: DEFAULT_MAX_CACHE_BYTES,
|
|
124
|
+
|
|
113
125
|
retryBackoffAlgorithm: DEFAULT_RETRY_BACKOFF_ALGORITHM,
|
|
114
126
|
retryMaxAttempts: DEFAULT_RETRY_MAX_ATTEMPTS,
|
|
115
127
|
retryInitialDelay: DEFAULT_RETRY_INITIAL_DELAY,
|
|
@@ -149,6 +161,8 @@ export function plAddressToConfig(
|
|
|
149
161
|
txDelay: parseInt(url.searchParams.get('tx-delay')) ?? 0,
|
|
150
162
|
forceSync: Boolean(url.searchParams.get('force-sync')) ?? false,
|
|
151
163
|
|
|
164
|
+
maxCacheBytes: parseInt(url.searchParams.get('max-cache-bytes')) ?? DEFAULT_MAX_CACHE_BYTES,
|
|
165
|
+
|
|
152
166
|
retryBackoffAlgorithm: (url.searchParams.get('retry-backoff-algorithm') ??
|
|
153
167
|
DEFAULT_RETRY_BACKOFF_ALGORITHM) as any,
|
|
154
168
|
retryMaxAttempts:
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Optional } from 'utility-types';
|
|
2
|
+
import { BasicResourceData, isNotNullResourceId, isNullResourceId, ResourceData } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Function is used to guide multiple layers of caching in pl-client and derived pl-tree.
|
|
6
|
+
*
|
|
7
|
+
* This function defines expected resource-specific state mutation behaviour,
|
|
8
|
+
* if it returns true, system will expect that this data will never change as long as resource exist.
|
|
9
|
+
*
|
|
10
|
+
* If resource data contain information about fields, if should be taken into account, fields are undefined,
|
|
11
|
+
* "final" state should be calculated for "basic" part of resource data only.
|
|
12
|
+
*/
|
|
13
|
+
export type FinalResourceDataPredicate = (
|
|
14
|
+
resourceData: Optional<ResourceData, 'fields'>
|
|
15
|
+
) => boolean;
|
|
16
|
+
|
|
17
|
+
function readyOrDuplicateOrError(r: ResourceData | BasicResourceData): boolean {
|
|
18
|
+
return (
|
|
19
|
+
r.resourceReady || isNotNullResourceId(r.originalResourceId) || isNotNullResourceId(r.error)
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readyAndHasAllOutputsFilled(r: Optional<ResourceData, 'fields'>): boolean {
|
|
24
|
+
if (!readyOrDuplicateOrError(r)) return false;
|
|
25
|
+
if (!r.outputsLocked) return false;
|
|
26
|
+
if (r.fields === undefined) return true; // if fields are not provided basic resource state is not expected to change in the future
|
|
27
|
+
for (const f of r.fields)
|
|
28
|
+
if (isNullResourceId(f.error) && (isNullResourceId(f.value) || !f.valueIsFinal)) return false;
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// solaly for logging
|
|
33
|
+
const unknownResourceTypeNames = new Set<string>();
|
|
34
|
+
|
|
35
|
+
/** Default implementation, defining behaviour for built-in resource types. */
|
|
36
|
+
export const DefaultFinalResourceDataPredicate: FinalResourceDataPredicate = (r): boolean => {
|
|
37
|
+
switch (r.type.name) {
|
|
38
|
+
case 'StdMap':
|
|
39
|
+
case 'std/map':
|
|
40
|
+
case 'EphStdMap':
|
|
41
|
+
case 'PFrame':
|
|
42
|
+
case 'BContext':
|
|
43
|
+
case 'BlockPackCustom':
|
|
44
|
+
case 'BinaryMap':
|
|
45
|
+
case 'BinaryValue':
|
|
46
|
+
case 'BlobMap':
|
|
47
|
+
case 'BResolveSingle':
|
|
48
|
+
case 'BResolveSingleNoResult':
|
|
49
|
+
case 'BQueryResult':
|
|
50
|
+
case 'TengoTemplate':
|
|
51
|
+
case 'TengoLib':
|
|
52
|
+
case 'SoftwareInfo':
|
|
53
|
+
case 'Dummy':
|
|
54
|
+
return readyOrDuplicateOrError(r);
|
|
55
|
+
case 'json/resourceError':
|
|
56
|
+
return r.type.version === '1';
|
|
57
|
+
case 'json/object':
|
|
58
|
+
case 'json/string':
|
|
59
|
+
case 'json/array':
|
|
60
|
+
case 'json/number':
|
|
61
|
+
case 'BContextEnd':
|
|
62
|
+
case 'Frontend/FromUrl':
|
|
63
|
+
case 'Frontend/FromFolder':
|
|
64
|
+
case 'BObjectSpec':
|
|
65
|
+
return true;
|
|
66
|
+
case 'UserProject':
|
|
67
|
+
return false;
|
|
68
|
+
default:
|
|
69
|
+
if (r.type.name.startsWith('Blob/')) return true;
|
|
70
|
+
else if (r.type.name.startsWith('BlobUpload/')) {
|
|
71
|
+
return readyAndHasAllOutputsFilled(r);
|
|
72
|
+
} else if (r.type.name.startsWith('PColumnData/')) {
|
|
73
|
+
return readyOrDuplicateOrError(r);
|
|
74
|
+
} else {
|
|
75
|
+
// Unknonw resource type detected
|
|
76
|
+
// Set used to log this message only once
|
|
77
|
+
if (!unknownResourceTypeNames.has(r.type.name)) {
|
|
78
|
+
console.log('UNKNOWN RESOURCE TYPE: ' + r.type.name);
|
|
79
|
+
unknownResourceTypeNames.add(r.type.name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
};
|
package/src/core/ll_client.ts
CHANGED
|
@@ -77,6 +77,13 @@ export class LLPlClient {
|
|
|
77
77
|
|
|
78
78
|
grpcInterceptors.push(this.createErrorInterceptor());
|
|
79
79
|
|
|
80
|
+
//
|
|
81
|
+
// Leaving it here for now
|
|
82
|
+
// https://github.com/grpc/grpc-node/issues/2788
|
|
83
|
+
//
|
|
84
|
+
// We should implement message pooling algorithm to overcome hardcoded NO_DELAY behaviour
|
|
85
|
+
// of HTTP/2 and allow our small messages to batch together.
|
|
86
|
+
//
|
|
80
87
|
const grpcOptions: GrpcOptions = {
|
|
81
88
|
host: this.conf.hostAndPort,
|
|
82
89
|
timeout: this.conf.defaultRequestTimeout,
|
|
@@ -84,7 +91,7 @@ export class LLPlClient {
|
|
|
84
91
|
? ChannelCredentials.createSsl()
|
|
85
92
|
: ChannelCredentials.createInsecure(),
|
|
86
93
|
clientOptions: {
|
|
87
|
-
'grpc.
|
|
94
|
+
'grpc.keepalive_time_ms': 30_000, // 30 seconds
|
|
88
95
|
interceptors: grpcInterceptors
|
|
89
96
|
}
|
|
90
97
|
};
|
package/src/core/transaction.ts
CHANGED
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
ResourceData,
|
|
12
12
|
ResourceId,
|
|
13
13
|
ResourceType,
|
|
14
|
-
FutureFieldType
|
|
14
|
+
FutureFieldType,
|
|
15
|
+
isLocalResourceId,
|
|
16
|
+
extractBasicResourceData
|
|
15
17
|
} from './types';
|
|
16
18
|
import {
|
|
17
19
|
ClientMessageRequest,
|
|
@@ -25,6 +27,9 @@ import { toBytes } from '../util/util';
|
|
|
25
27
|
import { fieldTypeToProto, protoToField, protoToResource } from './type_conversion';
|
|
26
28
|
import { notEmpty } from '@milaboratories/ts-helpers';
|
|
27
29
|
import { isNotFoundError } from './errors';
|
|
30
|
+
import { FinalResourceDataPredicate } from './final';
|
|
31
|
+
import { LRUCache } from 'lru-cache';
|
|
32
|
+
import { ResourceDataCacheRecord } from './cache';
|
|
28
33
|
|
|
29
34
|
/** Reference to resource, used only within transaction */
|
|
30
35
|
export interface ResourceRef {
|
|
@@ -132,6 +137,9 @@ export class PlTransaction {
|
|
|
132
137
|
private readonly globalTxId: Promise<bigint>;
|
|
133
138
|
private readonly localTxId: number = PlTransaction.nextLocalTxId();
|
|
134
139
|
|
|
140
|
+
/** Used in caching */
|
|
141
|
+
private readonly txOpenTimestamp = Date.now();
|
|
142
|
+
|
|
135
143
|
private localResourceIdCounter = 0;
|
|
136
144
|
|
|
137
145
|
/** Store logical tx open / closed state to prevent invalid sequence of requests.
|
|
@@ -148,7 +156,9 @@ export class PlTransaction {
|
|
|
148
156
|
private readonly ll: LLPlTransaction,
|
|
149
157
|
public readonly name: string,
|
|
150
158
|
public readonly writable: boolean,
|
|
151
|
-
private readonly _clientRoot: OptionalResourceId
|
|
159
|
+
private readonly _clientRoot: OptionalResourceId,
|
|
160
|
+
private readonly finalPredicate: FinalResourceDataPredicate,
|
|
161
|
+
private readonly sharedResourceDataCache: LRUCache<ResourceId, ResourceDataCacheRecord>
|
|
152
162
|
) {
|
|
153
163
|
// initiating transaction
|
|
154
164
|
this.globalTxId = this.sendSingleAndParse(
|
|
@@ -436,23 +446,82 @@ export class PlTransaction {
|
|
|
436
446
|
);
|
|
437
447
|
}
|
|
438
448
|
|
|
449
|
+
/** This method may return stale resource state from cache if resource was removed */
|
|
439
450
|
public async getResourceData(rId: AnyResourceRef, loadFields: true): Promise<ResourceData>;
|
|
451
|
+
/** This method may return stale resource state from cache if resource was removed */
|
|
440
452
|
public async getResourceData(rId: AnyResourceRef, loadFields: false): Promise<BasicResourceData>;
|
|
453
|
+
/** This method may return stale resource state from cache if resource was removed */
|
|
441
454
|
public async getResourceData(
|
|
442
455
|
rId: AnyResourceRef,
|
|
443
456
|
loadFields: boolean
|
|
444
457
|
): Promise<BasicResourceData | ResourceData>;
|
|
458
|
+
/** This method may return stale resource state from cache if ignoreCache == false if resource was removed */
|
|
445
459
|
public async getResourceData(
|
|
446
460
|
rId: AnyResourceRef,
|
|
447
|
-
loadFields:
|
|
461
|
+
loadFields: true,
|
|
462
|
+
ignoreCache: boolean
|
|
463
|
+
): Promise<ResourceData>;
|
|
464
|
+
/** This method may return stale resource state from cache if ignoreCache == false if resource was removed */
|
|
465
|
+
public async getResourceData(
|
|
466
|
+
rId: AnyResourceRef,
|
|
467
|
+
loadFields: false,
|
|
468
|
+
ignoreCache: boolean
|
|
469
|
+
): Promise<BasicResourceData>;
|
|
470
|
+
/** This method may return stale resource state from cache if ignoreCache == false if resource was removed */
|
|
471
|
+
public async getResourceData(
|
|
472
|
+
rId: AnyResourceRef,
|
|
473
|
+
loadFields: boolean,
|
|
474
|
+
ignoreCache: boolean
|
|
475
|
+
): Promise<BasicResourceData | ResourceData>;
|
|
476
|
+
public async getResourceData(
|
|
477
|
+
rId: AnyResourceRef,
|
|
478
|
+
loadFields: boolean = true,
|
|
479
|
+
ignoreCache: boolean = false
|
|
448
480
|
): Promise<BasicResourceData | ResourceData> {
|
|
449
|
-
|
|
481
|
+
if (!ignoreCache && !isResourceRef(rId) && !isLocalResourceId(rId)) {
|
|
482
|
+
// checking if we can return result from cache
|
|
483
|
+
const fromCache = this.sharedResourceDataCache.get(rId);
|
|
484
|
+
if (fromCache && fromCache.cacheTxOpenTimestamp < this.txOpenTimestamp) {
|
|
485
|
+
if (!loadFields) return fromCache.basicData;
|
|
486
|
+
else if (fromCache.data) return fromCache.data;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const result = await this.sendSingleAndParse(
|
|
450
491
|
{
|
|
451
492
|
oneofKind: 'resourceGet',
|
|
452
493
|
resourceGet: { resourceId: toResourceId(rId), loadFields: loadFields }
|
|
453
494
|
},
|
|
454
495
|
(r) => protoToResource(notEmpty(r.resourceGet.resource))
|
|
455
496
|
);
|
|
497
|
+
|
|
498
|
+
// we will cache only final resource data states
|
|
499
|
+
// caching result even if we were ignore the cache
|
|
500
|
+
if (!isResourceRef(rId) && !isLocalResourceId(rId) && this.finalPredicate(result)) {
|
|
501
|
+
const fromCache = this.sharedResourceDataCache.get(rId);
|
|
502
|
+
if (fromCache) {
|
|
503
|
+
if (loadFields && !fromCache.data) {
|
|
504
|
+
fromCache.data = result;
|
|
505
|
+
// updating timestamp becuse we updated the record
|
|
506
|
+
fromCache.cacheTxOpenTimestamp = this.txOpenTimestamp;
|
|
507
|
+
}
|
|
508
|
+
} else {
|
|
509
|
+
if (loadFields)
|
|
510
|
+
this.sharedResourceDataCache.set(rId, {
|
|
511
|
+
basicData: extractBasicResourceData(result),
|
|
512
|
+
data: result,
|
|
513
|
+
cacheTxOpenTimestamp: this.txOpenTimestamp
|
|
514
|
+
});
|
|
515
|
+
else
|
|
516
|
+
this.sharedResourceDataCache.set(rId, {
|
|
517
|
+
basicData: extractBasicResourceData(result),
|
|
518
|
+
data: undefined,
|
|
519
|
+
cacheTxOpenTimestamp: this.txOpenTimestamp
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return result;
|
|
456
525
|
}
|
|
457
526
|
|
|
458
527
|
public async getResourceDataIfExists(
|
|
@@ -471,7 +540,17 @@ export class PlTransaction {
|
|
|
471
540
|
rId: AnyResourceRef,
|
|
472
541
|
loadFields: boolean = true
|
|
473
542
|
): Promise<BasicResourceData | ResourceData | undefined> {
|
|
474
|
-
|
|
543
|
+
// calling this mehtod will ignore cache, because user intention is to detect resource absence
|
|
544
|
+
// which cache will prevent
|
|
545
|
+
const result = await notFoundToUndefined(
|
|
546
|
+
async () => await this.getResourceData(rId, loadFields, true)
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
// cleaning cache record if resorce was removed from the db
|
|
550
|
+
if (result === undefined && !isResourceRef(rId) && !isLocalResourceId(rId))
|
|
551
|
+
this.sharedResourceDataCache.delete(rId);
|
|
552
|
+
|
|
553
|
+
return result;
|
|
475
554
|
}
|
|
476
555
|
|
|
477
556
|
/**
|
package/src/core/types.ts
CHANGED
|
@@ -70,7 +70,7 @@ export function resourceTypesEqual(type1: ResourceType, type2: ResourceType): bo
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/** Readonly fields here marks properties of resource that can't change according to pl's state machine. */
|
|
73
|
-
export
|
|
73
|
+
export type BasicResourceData = {
|
|
74
74
|
readonly id: ResourceId;
|
|
75
75
|
readonly originalResourceId: OptionalResourceId;
|
|
76
76
|
|
|
@@ -88,30 +88,57 @@ export interface BasicResourceData {
|
|
|
88
88
|
/** This value is derived from resource state by the server and can be used as
|
|
89
89
|
* a robust criteria to determine resource is in final state. */
|
|
90
90
|
readonly final: boolean;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export function extractBasicResourceData(rd: ResourceData): BasicResourceData {
|
|
94
|
+
const {
|
|
95
|
+
id,
|
|
96
|
+
originalResourceId,
|
|
97
|
+
kind,
|
|
98
|
+
type,
|
|
99
|
+
data,
|
|
100
|
+
error,
|
|
101
|
+
inputsLocked,
|
|
102
|
+
outputsLocked,
|
|
103
|
+
resourceReady,
|
|
104
|
+
final
|
|
105
|
+
} = rd;
|
|
106
|
+
return {
|
|
107
|
+
id,
|
|
108
|
+
originalResourceId,
|
|
109
|
+
kind,
|
|
110
|
+
type,
|
|
111
|
+
data,
|
|
112
|
+
error,
|
|
113
|
+
inputsLocked,
|
|
114
|
+
outputsLocked,
|
|
115
|
+
resourceReady,
|
|
116
|
+
final
|
|
117
|
+
};
|
|
91
118
|
}
|
|
92
119
|
|
|
93
120
|
export const jsonToData = (data: unknown) => Buffer.from(JSON.stringify(data));
|
|
94
121
|
|
|
95
122
|
export const resDataToJson = (res: ResourceData) => JSON.parse(notEmpty(res.data).toString());
|
|
96
123
|
|
|
97
|
-
export
|
|
98
|
-
fields: FieldData[];
|
|
99
|
-
}
|
|
124
|
+
export type ResourceData = BasicResourceData & {
|
|
125
|
+
readonly fields: FieldData[];
|
|
126
|
+
};
|
|
100
127
|
|
|
101
128
|
export function getField(r: ResourceData, name: string): FieldData {
|
|
102
129
|
return notEmpty(r.fields.find((f) => f.name === name));
|
|
103
130
|
}
|
|
104
131
|
|
|
105
|
-
export
|
|
106
|
-
name: string;
|
|
107
|
-
type: FieldType;
|
|
108
|
-
status: FieldStatus;
|
|
109
|
-
value: OptionalResourceId;
|
|
110
|
-
error: OptionalResourceId;
|
|
132
|
+
export type FieldData = {
|
|
133
|
+
readonly name: string;
|
|
134
|
+
readonly type: FieldType;
|
|
135
|
+
readonly status: FieldStatus;
|
|
136
|
+
readonly value: OptionalResourceId;
|
|
137
|
+
readonly error: OptionalResourceId;
|
|
111
138
|
|
|
112
139
|
/** True if value the fields points to is in final state. */
|
|
113
|
-
valueIsFinal: boolean;
|
|
114
|
-
}
|
|
140
|
+
readonly valueIsFinal: boolean;
|
|
141
|
+
};
|
|
115
142
|
|
|
116
143
|
//
|
|
117
144
|
// Local / Global ResourceId arithmetics
|