@wp-typia/project-tools 0.13.4 → 0.15.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.
Files changed (31) hide show
  1. package/dist/runtime/scaffold-onboarding.js +8 -4
  2. package/dist/runtime/scaffold.d.ts +1 -0
  3. package/dist/runtime/scaffold.js +3 -0
  4. package/dist/runtime/schema-core.d.ts +1 -0
  5. package/dist/runtime/schema-core.js +188 -8
  6. package/package.json +1 -1
  7. package/templates/_shared/compound/persistence/scripts/block-config.ts.mustache +16 -0
  8. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-types.ts.mustache +10 -0
  9. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api.ts.mustache +51 -38
  10. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/data.ts.mustache +76 -33
  11. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/interactivity.ts.mustache +206 -41
  12. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/render.php.mustache +37 -43
  13. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/transport.ts.mustache +254 -0
  14. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/types.ts.mustache +13 -8
  15. package/templates/_shared/compound/persistence-auth/{{slugKebabCase}}.php.mustache +139 -0
  16. package/templates/_shared/compound/persistence-public/{{slugKebabCase}}.php.mustache +159 -0
  17. package/templates/_shared/persistence/auth/{{slugKebabCase}}.php.mustache +139 -0
  18. package/templates/_shared/persistence/core/scripts/sync-rest-contracts.ts.mustache +16 -0
  19. package/templates/_shared/persistence/core/src/api-types.ts.mustache +10 -0
  20. package/templates/_shared/persistence/core/src/api.ts.mustache +51 -38
  21. package/templates/_shared/persistence/core/src/data.ts.mustache +76 -33
  22. package/templates/_shared/persistence/core/src/interactivity.ts.mustache +206 -43
  23. package/templates/_shared/persistence/core/src/transport.ts.mustache +254 -0
  24. package/templates/_shared/persistence/public/{{slugKebabCase}}.php.mustache +159 -0
  25. package/templates/_shared/rest-helpers/public/inc/rest-public.php.mustache +1 -1
  26. package/templates/_shared/workspace/persistence-auth/server.php.mustache +139 -0
  27. package/templates/_shared/workspace/persistence-public/inc/rest-public.php.mustache +1 -1
  28. package/templates/_shared/workspace/persistence-public/server.php.mustache +159 -0
  29. package/templates/persistence/src/edit.tsx.mustache +1 -1
  30. package/templates/persistence/src/render.php.mustache +37 -43
  31. package/templates/persistence/src/types.ts.mustache +13 -9
@@ -1,17 +1,20 @@
1
1
  import { getContext, store } from '@wordpress/interactivity';
2
2
  import { generatePublicWriteRequestId } from '@wp-typia/block-runtime/identifiers';
3
3
 
4
- import { fetchState, writeState } from './api';
5
- import type { {{pascalCase}}Context, {{pascalCase}}State } from './types';
4
+ import { fetchBootstrap, fetchState, writeState } from './api';
5
+ import type {
6
+ {{pascalCase}}ClientState,
7
+ {{pascalCase}}Context,
8
+ {{pascalCase}}State,
9
+ } from './types';
6
10
 
7
11
  function hasExpiredPublicWriteToken(
8
- context: {{pascalCase}}Context
12
+ expiresAt?: number
9
13
  ): boolean {
10
14
  return (
11
- context.persistencePolicy === 'public' &&
12
- typeof context.publicWriteExpiresAt === 'number' &&
13
- context.publicWriteExpiresAt > 0 &&
14
- Date.now() >= context.publicWriteExpiresAt * 1000
15
+ typeof expiresAt === 'number' &&
16
+ expiresAt > 0 &&
17
+ Date.now() >= expiresAt * 1000
15
18
  );
16
19
  }
17
20
 
@@ -20,67 +23,212 @@ function getWriteBlockedMessage(
20
23
  ): string {
21
24
  return context.persistencePolicy === 'authenticated'
22
25
  ? 'Sign in to persist this counter.'
23
- : 'Reload the page to refresh this write token.';
26
+ : 'Public writes are temporarily unavailable.';
27
+ }
28
+
29
+ const BOOTSTRAP_MAX_ATTEMPTS = 3;
30
+ const BOOTSTRAP_RETRY_DELAYS_MS = [ 250, 500 ];
31
+
32
+ async function waitForBootstrapRetry( delayMs: number ): Promise< void > {
33
+ await new Promise( ( resolve ) => {
34
+ setTimeout( resolve, delayMs );
35
+ } );
36
+ }
37
+
38
+ function getClientState(
39
+ context: {{pascalCase}}Context
40
+ ): {{pascalCase}}ClientState {
41
+ if ( context.client ) {
42
+ return context.client;
43
+ }
44
+
45
+ context.client = {
46
+ bootstrapError: '',
47
+ writeExpiry: 0,
48
+ writeNonce: '',
49
+ writeToken: '',
50
+ };
51
+
52
+ return context.client;
53
+ }
54
+
55
+ function clearBootstrapError(
56
+ context: {{pascalCase}}Context,
57
+ clientState: {{pascalCase}}ClientState
58
+ ): void {
59
+ if ( context.error === clientState.bootstrapError ) {
60
+ context.error = '';
61
+ }
62
+ clientState.bootstrapError = '';
63
+ }
64
+
65
+ function setBootstrapError(
66
+ context: {{pascalCase}}Context,
67
+ clientState: {{pascalCase}}ClientState,
68
+ message: string
69
+ ): void {
70
+ clientState.bootstrapError = message;
71
+ context.error = message;
24
72
  }
25
73
 
26
74
  const { actions, state } = store( '{{slugKebabCase}}', {
27
75
  state: {
28
- canWrite: false,
29
- count: 0,
30
- error: undefined,
31
76
  isHydrated: false,
32
- isLoading: false,
33
- isSaving: false,
34
- isVisible: true,
35
77
  } as {{pascalCase}}State,
36
78
 
37
79
  actions: {
38
- async loadCounter() {
80
+ async loadState() {
39
81
  const context = getContext< {{pascalCase}}Context >();
40
82
  if ( context.postId <= 0 || ! context.resourceKey ) {
41
83
  return;
42
84
  }
43
85
 
44
- state.isLoading = true;
45
- state.error = undefined;
86
+ context.isLoading = true;
87
+ context.error = '';
46
88
 
47
89
  try {
48
90
  const result = await fetchState( {
49
91
  postId: context.postId,
50
92
  resourceKey: context.resourceKey,
93
+ }, {
94
+ transportTarget: 'frontend',
51
95
  } );
52
96
  if ( ! result.isValid || ! result.data ) {
53
- state.error = result.errors[ 0 ]?.expected ?? 'Unable to load counter';
97
+ context.error = result.errors[ 0 ]?.expected ?? 'Unable to load counter';
54
98
  return;
55
99
  }
56
100
  context.count = result.data.count;
57
- context.storage = result.data.storage;
58
- state.count = result.data.count;
59
101
  } catch ( error ) {
60
- state.error =
102
+ context.error =
61
103
  error instanceof Error ? error.message : 'Unknown loading error';
62
104
  } finally {
63
- state.isLoading = false;
105
+ context.isLoading = false;
64
106
  }
65
107
  },
108
+ async loadBootstrap() {
109
+ const context = getContext< {{pascalCase}}Context >();
110
+ const clientState = getClientState( context );
111
+ if ( context.postId <= 0 || ! context.resourceKey ) {
112
+ context.bootstrapReady = true;
113
+ context.canWrite = false;
114
+ clientState.bootstrapError = '';
115
+ clientState.writeExpiry = 0;
116
+ clientState.writeNonce = '';
117
+ clientState.writeToken = '';
118
+ return;
119
+ }
120
+
121
+ context.isBootstrapping = true;
122
+
123
+ let bootstrapSucceeded = false;
124
+ let lastBootstrapError =
125
+ 'Unable to initialize write access';
126
+
127
+ for ( let attempt = 1; attempt <= BOOTSTRAP_MAX_ATTEMPTS; attempt += 1 ) {
128
+ try {
129
+ const result = await fetchBootstrap( {
130
+ postId: context.postId,
131
+ resourceKey: context.resourceKey,
132
+ }, {
133
+ transportTarget: 'frontend',
134
+ } );
135
+ if ( ! result.isValid || ! result.data ) {
136
+ lastBootstrapError =
137
+ result.errors[ 0 ]?.expected ??
138
+ 'Unable to initialize write access';
139
+ if ( attempt < BOOTSTRAP_MAX_ATTEMPTS ) {
140
+ await waitForBootstrapRetry(
141
+ BOOTSTRAP_RETRY_DELAYS_MS[ attempt - 1 ] ?? 750
142
+ );
143
+ continue;
144
+ }
145
+ break;
146
+ }
147
+
148
+ clientState.writeExpiry =
149
+ typeof result.data.publicWriteExpiresAt === 'number' &&
150
+ result.data.publicWriteExpiresAt > 0
151
+ ? result.data.publicWriteExpiresAt
152
+ : 0;
153
+ clientState.writeToken =
154
+ typeof result.data.publicWriteToken === 'string' &&
155
+ result.data.publicWriteToken.length > 0
156
+ ? result.data.publicWriteToken
157
+ : '';
158
+ clientState.writeNonce =
159
+ typeof result.data.restNonce === 'string' &&
160
+ result.data.restNonce.length > 0
161
+ ? result.data.restNonce
162
+ : '';
163
+ context.bootstrapReady = true;
164
+ context.canWrite =
165
+ result.data.canWrite === true &&
166
+ (
167
+ context.persistencePolicy === 'authenticated'
168
+ ? clientState.writeNonce.length > 0
169
+ : clientState.writeToken.length > 0 &&
170
+ ! hasExpiredPublicWriteToken( clientState.writeExpiry )
171
+ );
172
+ clearBootstrapError( context, clientState );
173
+ bootstrapSucceeded = true;
174
+ break;
175
+ } catch ( error ) {
176
+ lastBootstrapError =
177
+ error instanceof Error ? error.message : 'Unknown bootstrap error';
178
+ if ( attempt < BOOTSTRAP_MAX_ATTEMPTS ) {
179
+ await waitForBootstrapRetry(
180
+ BOOTSTRAP_RETRY_DELAYS_MS[ attempt - 1 ] ?? 750
181
+ );
182
+ continue;
183
+ }
184
+ break;
185
+ }
186
+ }
187
+
188
+ if ( ! bootstrapSucceeded ) {
189
+ context.bootstrapReady = false;
190
+ context.canWrite = false;
191
+ clientState.writeExpiry = 0;
192
+ clientState.writeNonce = '';
193
+ clientState.writeToken = '';
194
+ setBootstrapError( context, clientState, lastBootstrapError );
195
+ }
196
+ context.isBootstrapping = false;
197
+ },
66
198
  async increment() {
67
199
  const context = getContext< {{pascalCase}}Context >();
200
+ const clientState = getClientState( context );
68
201
  if ( context.postId <= 0 || ! context.resourceKey ) {
69
202
  return;
70
203
  }
71
- if ( hasExpiredPublicWriteToken( context ) ) {
204
+ if ( ! context.bootstrapReady ) {
205
+ await actions.loadBootstrap();
206
+ }
207
+ if ( ! context.bootstrapReady ) {
208
+ context.error = 'Write access is still initializing.';
209
+ return;
210
+ }
211
+ if (
212
+ context.persistencePolicy === 'public' &&
213
+ hasExpiredPublicWriteToken( clientState.writeExpiry )
214
+ ) {
215
+ await actions.loadBootstrap();
216
+ }
217
+ if (
218
+ context.persistencePolicy === 'public' &&
219
+ hasExpiredPublicWriteToken( clientState.writeExpiry )
220
+ ) {
72
221
  context.canWrite = false;
73
- state.canWrite = false;
74
- state.error = getWriteBlockedMessage( context );
222
+ context.error = getWriteBlockedMessage( context );
75
223
  return;
76
224
  }
77
- if ( ! context.canWrite || ! state.canWrite ) {
78
- state.error = getWriteBlockedMessage( context );
225
+ if ( ! context.canWrite ) {
226
+ context.error = getWriteBlockedMessage( context );
79
227
  return;
80
228
  }
81
229
 
82
- state.isSaving = true;
83
- state.error = undefined;
230
+ context.isSaving = true;
231
+ context.error = '';
84
232
 
85
233
  try {
86
234
  const result = await writeState( {
@@ -92,24 +240,28 @@ const { actions, state } = store( '{{slugKebabCase}}', {
92
240
  : undefined,
93
241
  publicWriteToken:
94
242
  context.persistencePolicy === 'public' &&
95
- typeof context.publicWriteToken === 'string' &&
96
- context.publicWriteToken.length > 0
97
- ? context.publicWriteToken
243
+ clientState.writeToken.length > 0
244
+ ? clientState.writeToken
98
245
  : undefined,
99
246
  resourceKey: context.resourceKey,
100
- }, context.restNonce );
247
+ }, {
248
+ restNonce:
249
+ clientState.writeNonce.length > 0
250
+ ? clientState.writeNonce
251
+ : undefined,
252
+ transportTarget: 'frontend',
253
+ } );
101
254
  if ( ! result.isValid || ! result.data ) {
102
- state.error = result.errors[ 0 ]?.expected ?? 'Unable to update counter';
255
+ context.error = result.errors[ 0 ]?.expected ?? 'Unable to update counter';
103
256
  return;
104
257
  }
105
258
  context.count = result.data.count;
106
259
  context.storage = result.data.storage;
107
- state.count = result.data.count;
108
260
  } catch ( error ) {
109
- state.error =
261
+ context.error =
110
262
  error instanceof Error ? error.message : 'Unknown update error';
111
263
  } finally {
112
- state.isSaving = false;
264
+ context.isSaving = false;
113
265
  }
114
266
  },
115
267
  },
@@ -117,18 +269,29 @@ const { actions, state } = store( '{{slugKebabCase}}', {
117
269
  callbacks: {
118
270
  init() {
119
271
  const context = getContext< {{pascalCase}}Context >();
120
- context.canWrite =
121
- context.canWrite && ! hasExpiredPublicWriteToken( context );
122
- state.canWrite = context.canWrite;
123
- state.isVisible = context.isVisible;
124
- state.count = context.count;
272
+ context.client = {
273
+ bootstrapError: '',
274
+ writeExpiry: 0,
275
+ writeNonce: '',
276
+ writeToken: '',
277
+ };
278
+ context.bootstrapReady = false;
279
+ context.canWrite = false;
280
+ context.count = 0;
281
+ context.error = '';
282
+ context.isBootstrapping = false;
283
+ context.isLoading = false;
284
+ context.isSaving = false;
125
285
  },
126
286
  mounted() {
127
287
  state.isHydrated = true;
128
288
  if ( typeof document !== 'undefined' ) {
129
289
  document.documentElement.dataset[ '{{slugCamelCase}}Hydrated' ] = 'true';
130
290
  }
131
- void actions.loadCounter();
291
+ void Promise.allSettled( [
292
+ actions.loadState(),
293
+ actions.loadBootstrap(),
294
+ ] );
132
295
  },
133
296
  },
134
297
  } );
@@ -0,0 +1,254 @@
1
+ import {
2
+ type EndpointCallOptions,
3
+ resolveRestRouteUrl,
4
+ } from '@wp-typia/rest';
5
+
6
+ export type PersistenceTransportTarget = 'editor' | 'frontend';
7
+ export type PersistenceTransportOperation = 'read' | 'write';
8
+
9
+ export interface PersistenceTransportOptions {
10
+ restNonce?: string;
11
+ transportTarget?: PersistenceTransportTarget;
12
+ }
13
+
14
+ interface EndpointWithPath {
15
+ path: string;
16
+ }
17
+
18
+ type PersistenceTransportResolver = (
19
+ endpoint: EndpointWithPath,
20
+ request?: unknown,
21
+ options?: PersistenceTransportOptions
22
+ ) => EndpointCallOptions;
23
+
24
+ interface PersistenceTransportTargetResolvers {
25
+ read: PersistenceTransportResolver;
26
+ write: PersistenceTransportResolver;
27
+ }
28
+
29
+ // Replace any of these base URLs to route a given target through a contract-compatible proxy or BFF.
30
+ const EDITOR_READ_BASE_URL: string | undefined = undefined;
31
+ const EDITOR_WRITE_BASE_URL: string | undefined = undefined;
32
+ const FRONTEND_READ_BASE_URL: string | undefined = undefined;
33
+ const FRONTEND_WRITE_BASE_URL: string | undefined = undefined;
34
+
35
+ function resolveEndpointUrl(
36
+ endpointPath: string,
37
+ baseUrl?: string
38
+ ): string {
39
+ if ( typeof baseUrl === 'string' && baseUrl.trim().length > 0 ) {
40
+ const origin =
41
+ typeof window !== 'undefined' ? window.location.origin : 'http://localhost';
42
+ const resolvedBaseUrl = new URL( baseUrl.trim(), origin );
43
+ const [ pathWithQuery, hash = '' ] = endpointPath.split( '#', 2 );
44
+ const [ rawPath, rawQuery = '' ] = pathWithQuery.split( '?', 2 );
45
+ const normalizedEndpointPath = rawPath.replace( /^\/+/, '' );
46
+
47
+ if ( resolvedBaseUrl.searchParams.has( 'rest_route' ) ) {
48
+ const restRouteBase = resolvedBaseUrl.searchParams.get( 'rest_route' ) ?? '/';
49
+ const normalizedRestRouteBase = restRouteBase
50
+ .replace( /^\/+/, '' )
51
+ .replace( /\/+$/, '' );
52
+ const nextRestRoute = normalizedRestRouteBase.length > 0
53
+ ? `/${ normalizedRestRouteBase }/${ normalizedEndpointPath }`
54
+ : `/${ normalizedEndpointPath }`;
55
+ resolvedBaseUrl.searchParams.set( 'rest_route', nextRestRoute );
56
+ } else {
57
+ const basePath = resolvedBaseUrl.pathname.endsWith( '/' )
58
+ ? resolvedBaseUrl.pathname
59
+ : `${ resolvedBaseUrl.pathname }/`;
60
+ resolvedBaseUrl.pathname = `${ basePath }${ normalizedEndpointPath }`;
61
+ }
62
+
63
+ for ( const [ key, value ] of new URLSearchParams( rawQuery ) ) {
64
+ resolvedBaseUrl.searchParams.append( key, value );
65
+ }
66
+
67
+ if ( hash ) {
68
+ resolvedBaseUrl.hash = hash;
69
+ }
70
+
71
+ return resolvedBaseUrl.toString();
72
+ }
73
+
74
+ return resolveRestRouteUrl( endpointPath );
75
+ }
76
+
77
+ function isPlainObject(
78
+ value: unknown
79
+ ): value is Record< string, unknown > {
80
+ if ( Object.prototype.toString.call( value ) !== '[object Object]' ) {
81
+ return false;
82
+ }
83
+
84
+ const prototype = Object.getPrototypeOf( value );
85
+ return prototype === null || prototype === Object.prototype;
86
+ }
87
+
88
+ function encodeRequestQuery( request: unknown ): string {
89
+ if ( request === undefined || request === null ) {
90
+ return '';
91
+ }
92
+
93
+ if ( request instanceof URLSearchParams ) {
94
+ return request.toString();
95
+ }
96
+
97
+ if ( ! isPlainObject( request ) ) {
98
+ throw new Error(
99
+ 'Persistence transport read requests must be plain objects or URLSearchParams.'
100
+ );
101
+ }
102
+
103
+ const params = new URLSearchParams();
104
+
105
+ for ( const [ key, value ] of Object.entries( request ) ) {
106
+ if ( value === undefined || value === null ) {
107
+ continue;
108
+ }
109
+
110
+ if ( Array.isArray( value ) ) {
111
+ for ( const item of value ) {
112
+ params.append( key, String( item ) );
113
+ }
114
+ continue;
115
+ }
116
+
117
+ params.set( key, String( value ) );
118
+ }
119
+
120
+ return params.toString();
121
+ }
122
+
123
+ function joinUrlWithQuery(
124
+ url: string,
125
+ query: string
126
+ ): string {
127
+ if ( ! query ) {
128
+ return url;
129
+ }
130
+
131
+ const nextUrl = new URL(
132
+ url,
133
+ typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
134
+ );
135
+
136
+ for ( const [ key, value ] of new URLSearchParams( query ) ) {
137
+ nextUrl.searchParams.append( key, value );
138
+ }
139
+
140
+ return nextUrl.toString();
141
+ }
142
+
143
+ export function resolveRestNonce( fallback?: string ): string | undefined {
144
+ if ( typeof fallback === 'string' && fallback.length > 0 ) {
145
+ return fallback;
146
+ }
147
+
148
+ if ( typeof window === 'undefined' ) {
149
+ return undefined;
150
+ }
151
+
152
+ const wpApiSettings = ( window as typeof window & {
153
+ wpApiSettings?: { nonce?: string };
154
+ } ).wpApiSettings;
155
+
156
+ return typeof wpApiSettings?.nonce === 'string' && wpApiSettings.nonce.length > 0
157
+ ? wpApiSettings.nonce
158
+ : undefined;
159
+ }
160
+
161
+ function buildCallOptions( {
162
+ baseUrl,
163
+ endpoint,
164
+ includeRestNonce,
165
+ operation,
166
+ options,
167
+ request,
168
+ }: {
169
+ baseUrl?: string;
170
+ endpoint: EndpointWithPath;
171
+ includeRestNonce: boolean;
172
+ operation: PersistenceTransportOperation;
173
+ options?: PersistenceTransportOptions;
174
+ request?: unknown;
175
+ } ): EndpointCallOptions {
176
+ const nonce = includeRestNonce ? resolveRestNonce( options?.restNonce ) : undefined;
177
+ const requestOptions: EndpointCallOptions['requestOptions'] = {};
178
+
179
+ if ( nonce ) {
180
+ requestOptions.headers = {
181
+ 'X-WP-Nonce': nonce,
182
+ };
183
+ }
184
+
185
+ if ( typeof baseUrl === 'string' && baseUrl.trim().length > 0 ) {
186
+ const endpointUrl = resolveEndpointUrl( endpoint.path, baseUrl );
187
+ requestOptions.url =
188
+ operation === 'read'
189
+ ? joinUrlWithQuery( endpointUrl, encodeRequestQuery( request ) )
190
+ : endpointUrl;
191
+ }
192
+
193
+ return Object.keys( requestOptions ).length > 0
194
+ ? {
195
+ requestOptions,
196
+ }
197
+ : {};
198
+ }
199
+
200
+ export const persistenceTransportTargets: Record<
201
+ PersistenceTransportTarget,
202
+ PersistenceTransportTargetResolvers
203
+ > = {
204
+ editor: {
205
+ read: ( endpoint, request, options ) =>
206
+ buildCallOptions( {
207
+ baseUrl: EDITOR_READ_BASE_URL,
208
+ endpoint,
209
+ includeRestNonce: {{isAuthenticatedPersistencePolicy}},
210
+ operation: 'read',
211
+ options,
212
+ request,
213
+ } ),
214
+ write: ( endpoint, request, options ) =>
215
+ buildCallOptions( {
216
+ baseUrl: EDITOR_WRITE_BASE_URL,
217
+ endpoint,
218
+ includeRestNonce: {{isAuthenticatedPersistencePolicy}},
219
+ operation: 'write',
220
+ options,
221
+ request,
222
+ } ),
223
+ },
224
+ frontend: {
225
+ read: ( endpoint, request, options ) =>
226
+ buildCallOptions( {
227
+ baseUrl: FRONTEND_READ_BASE_URL,
228
+ endpoint,
229
+ includeRestNonce: false,
230
+ operation: 'read',
231
+ options,
232
+ request,
233
+ } ),
234
+ write: ( endpoint, request, options ) =>
235
+ buildCallOptions( {
236
+ baseUrl: FRONTEND_WRITE_BASE_URL,
237
+ endpoint,
238
+ includeRestNonce: {{isAuthenticatedPersistencePolicy}},
239
+ operation: 'write',
240
+ options,
241
+ request,
242
+ } ),
243
+ },
244
+ };
245
+
246
+ export function resolveTransportCallOptions(
247
+ target: PersistenceTransportTarget,
248
+ operation: PersistenceTransportOperation,
249
+ endpoint: EndpointWithPath,
250
+ request?: unknown,
251
+ options?: PersistenceTransportOptions
252
+ ): EndpointCallOptions {
253
+ return persistenceTransportTargets[ target ][ operation ]( endpoint, request, options );
254
+ }