Skip to content

Commit b279ce9

Browse files
committed
chore: PR comments
1 parent dd6f4f6 commit b279ce9

7 files changed

Lines changed: 255 additions & 15 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import * as fs from 'fs/promises';
2+
import * as os from 'os';
3+
import * as path from 'path';
4+
5+
import { createClient } from '../src';
6+
import { resetNodeStorage } from '../src/platform/NodeStorage';
7+
import { createMockLogger } from './testHelpers';
8+
9+
let tmpRoot: string;
10+
let logger: ReturnType<typeof createMockLogger>;
11+
12+
beforeEach(async () => {
13+
tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'node-client-bootstrap-test-'));
14+
resetNodeStorage();
15+
logger = createMockLogger();
16+
});
17+
18+
afterEach(async () => {
19+
resetNodeStorage();
20+
await fs.rm(tmpRoot, { recursive: true, force: true });
21+
});
22+
23+
const goodBootstrapData = {
24+
'string-flag': 'is bob',
25+
'my-boolean-flag': false,
26+
$flagsState: {
27+
'string-flag': {
28+
variation: 1,
29+
version: 3,
30+
},
31+
'my-boolean-flag': {
32+
variation: 1,
33+
version: 11,
34+
},
35+
},
36+
$valid: true,
37+
};
38+
39+
const bootstrapDataWithReasons = {
40+
json: ['a', 'b', 'c', 'd'],
41+
$flagsState: {
42+
json: {
43+
variation: 1,
44+
version: 3,
45+
reason: { kind: 'OFF' },
46+
},
47+
},
48+
$valid: true,
49+
};
50+
51+
it('start with bootstrap data resolves and exposes flags', async () => {
52+
const client = createClient('client-side-id', { kind: 'user', key: 'bob' }, {
53+
initialConnectionMode: 'offline',
54+
sendEvents: false,
55+
diagnosticOptOut: true,
56+
localStoragePath: tmpRoot,
57+
logger,
58+
});
59+
60+
const result = await client.start({ bootstrap: goodBootstrapData });
61+
62+
expect(result.status).toBe('complete');
63+
expect(client.stringVariation('string-flag', 'default')).toBe('is bob');
64+
expect(client.boolVariation('my-boolean-flag', true)).toBe(false);
65+
});
66+
67+
it('exposes evaluation reasons from bootstrap data', async () => {
68+
const client = createClient('client-side-id', { kind: 'user', key: 'bob' }, {
69+
initialConnectionMode: 'offline',
70+
sendEvents: false,
71+
diagnosticOptOut: true,
72+
localStoragePath: tmpRoot,
73+
logger,
74+
});
75+
76+
await client.start({ bootstrap: bootstrapDataWithReasons });
77+
78+
expect(client.jsonVariationDetail('json', undefined)).toEqual({
79+
reason: { kind: 'OFF' },
80+
value: ['a', 'b', 'c', 'd'],
81+
variationIndex: 1,
82+
});
83+
});
84+
85+
it('re-identifying with new bootstrap data replaces previous flags', async () => {
86+
const newBootstrapData = {
87+
'string-flag': 'is alice',
88+
'my-boolean-flag': true,
89+
$flagsState: {
90+
'string-flag': { variation: 1, version: 4 },
91+
'my-boolean-flag': { variation: 0, version: 12 },
92+
},
93+
$valid: true,
94+
};
95+
96+
const client = createClient('client-side-id', { kind: 'user', key: 'bob' }, {
97+
initialConnectionMode: 'offline',
98+
sendEvents: false,
99+
diagnosticOptOut: true,
100+
localStoragePath: tmpRoot,
101+
logger,
102+
});
103+
104+
await client.start({ bootstrap: goodBootstrapData });
105+
expect(client.stringVariation('string-flag', 'default')).toBe('is bob');
106+
107+
await client.identify({ kind: 'user', key: 'alice' }, { bootstrap: newBootstrapData });
108+
expect(client.stringVariation('string-flag', 'default')).toBe('is alice');
109+
expect(client.boolVariation('my-boolean-flag', false)).toBe(true);
110+
});
111+
112+
it('returns defaults when no bootstrap data is provided', async () => {
113+
const client = createClient('client-side-id', { kind: 'user', key: 'bob' }, {
114+
initialConnectionMode: 'offline',
115+
sendEvents: false,
116+
diagnosticOptOut: true,
117+
localStoragePath: tmpRoot,
118+
logger,
119+
});
120+
121+
await client.start();
122+
123+
expect(client.stringVariation('string-flag', 'default')).toBe('default');
124+
expect(client.boolVariation('my-boolean-flag', true)).toBe(true);
125+
});
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import * as fs from 'fs/promises';
2+
import * as os from 'os';
3+
import * as path from 'path';
4+
5+
import { createClient } from '../src';
6+
import { resetNodeStorage } from '../src/platform/NodeStorage';
7+
import { createMockLogger } from './testHelpers';
8+
9+
let tmpRoot: string;
10+
let logger: ReturnType<typeof createMockLogger>;
11+
12+
beforeEach(async () => {
13+
tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'node-client-test-'));
14+
resetNodeStorage();
15+
logger = createMockLogger();
16+
});
17+
18+
afterEach(async () => {
19+
resetNodeStorage();
20+
await fs.rm(tmpRoot, { recursive: true, force: true });
21+
});
22+
23+
it('createClient returns the documented LDClient surface', () => {
24+
const client = createClient('client-side-id', { kind: 'user', key: 'bob' }, {
25+
initialConnectionMode: 'offline',
26+
sendEvents: false,
27+
diagnosticOptOut: true,
28+
localStoragePath: tmpRoot,
29+
logger,
30+
});
31+
32+
expect(typeof client.start).toBe('function');
33+
expect(typeof client.identify).toBe('function');
34+
expect(typeof client.close).toBe('function');
35+
expect(typeof client.variation).toBe('function');
36+
expect(typeof client.variationDetail).toBe('function');
37+
expect(typeof client.boolVariation).toBe('function');
38+
expect(typeof client.boolVariationDetail).toBe('function');
39+
expect(typeof client.stringVariation).toBe('function');
40+
expect(typeof client.stringVariationDetail).toBe('function');
41+
expect(typeof client.numberVariation).toBe('function');
42+
expect(typeof client.numberVariationDetail).toBe('function');
43+
expect(typeof client.jsonVariation).toBe('function');
44+
expect(typeof client.jsonVariationDetail).toBe('function');
45+
expect(typeof client.allFlags).toBe('function');
46+
expect(typeof client.track).toBe('function');
47+
expect(typeof client.flush).toBe('function');
48+
expect(typeof client.on).toBe('function');
49+
expect(typeof client.off).toBe('function');
50+
expect(typeof client.addHook).toBe('function');
51+
expect(typeof client.waitForInitialization).toBe('function');
52+
expect(typeof client.setConnectionMode).toBe('function');
53+
expect(typeof client.getConnectionMode).toBe('function');
54+
expect(typeof client.isOffline).toBe('function');
55+
expect(client.logger).toBeDefined();
56+
});
57+
58+
it('isOffline reflects initialConnectionMode', () => {
59+
const offline = createClient('client-side-id', { kind: 'user', key: 'bob' }, {
60+
initialConnectionMode: 'offline',
61+
sendEvents: false,
62+
diagnosticOptOut: true,
63+
localStoragePath: tmpRoot,
64+
logger,
65+
});
66+
expect(offline.isOffline()).toBe(true);
67+
expect(offline.getConnectionMode()).toBe('offline');
68+
});
69+
70+
it('setConnectionMode round-trips to offline', async () => {
71+
const client = createClient('client-side-id', { kind: 'user', key: 'bob' }, {
72+
initialConnectionMode: 'offline',
73+
sendEvents: false,
74+
diagnosticOptOut: true,
75+
localStoragePath: tmpRoot,
76+
logger,
77+
});
78+
79+
expect(client.getConnectionMode()).toBe('offline');
80+
expect(client.isOffline()).toBe(true);
81+
82+
// Setting the same mode is a no-op but should not throw.
83+
await client.setConnectionMode('offline');
84+
expect(client.getConnectionMode()).toBe('offline');
85+
});
86+
87+
it('start completes in offline mode without performing network identify', async () => {
88+
const client = createClient('client-side-id', { kind: 'user', key: 'bob' }, {
89+
initialConnectionMode: 'offline',
90+
sendEvents: false,
91+
diagnosticOptOut: true,
92+
localStoragePath: tmpRoot,
93+
logger,
94+
});
95+
96+
const result = await client.start({ timeout: 5 });
97+
expect(result.status).toBe('complete');
98+
});

packages/sdk/node-client/__tests__/index.test.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

packages/sdk/node-client/src/LDClient.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,22 @@ export interface LDClient extends Omit<LDClientBase, 'identify'> {
3131
*/
3232
start(options?: LDStartOptions): Promise<LDWaitForInitializationResult>;
3333

34+
/**
35+
* Sets the data source connection mode.
36+
*
37+
* Pass `'offline'` to stop the streaming or polling connection and disable analytics event
38+
* delivery. Pass `'streaming'` or `'polling'` to (re)establish the connection using the
39+
* current context. The returned promise resolves once the mode change has been applied.
40+
*/
3441
setConnectionMode(mode: ConnectionMode): Promise<void>;
3542

43+
/**
44+
* Returns the current data source connection mode.
45+
*/
3646
getConnectionMode(): ConnectionMode;
3747

48+
/**
49+
* Returns true if the client is in offline mode.
50+
*/
3851
isOffline(): boolean;
3952
}

packages/sdk/node-client/src/NodeDataManager.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,36 +64,46 @@ export default class NodeDataManager extends BaseDataManager {
6464
}
6565
this.context = context;
6666

67+
let identifyResolved = false;
6768
if (identifyOptions?.bootstrap) {
6869
this._finishIdentifyFromBootstrap(context, identifyOptions, identifyResolve);
70+
identifyResolved = true;
6971
}
70-
const resolvedFromBootstrap = !!identifyOptions?.bootstrap;
7172

7273
const offline = this.connectionMode === 'offline';
7374
const waitForNetworkResults = !!identifyOptions?.waitForNetworkResults && !offline;
7475

7576
const loadedFromCache = await this.flagManager.loadCached(context);
76-
if (loadedFromCache && !waitForNetworkResults && !resolvedFromBootstrap) {
77+
if (loadedFromCache && !waitForNetworkResults && !identifyResolved) {
7778
this._debugLog('Identify completing with cached flags');
7879
identifyResolve();
80+
identifyResolved = true;
7981
}
8082
if (loadedFromCache && waitForNetworkResults) {
8183
this._debugLog(
8284
'Identify - Flags loaded from cache, but identify was requested with "waitForNetworkResults"',
8385
);
8486
}
8587

86-
if (this.connectionMode === 'offline') {
88+
if (offline) {
8789
if (loadedFromCache) {
8890
this._debugLog('Offline identify - using cached flags.');
8991
} else {
9092
this._debugLog(
9193
'Offline identify - no cached flags, using defaults or already loaded flags.',
9294
);
93-
if (!resolvedFromBootstrap) {
95+
if (!identifyResolved) {
9496
identifyResolve();
9597
}
9698
}
99+
return;
100+
}
101+
102+
// Online path. Pass identify callbacks only if not already resolved -- otherwise
103+
// the streaming/polling processor would receive stale handles and bootstrap/cache
104+
// resolution would race with network resolution.
105+
if (identifyResolved) {
106+
this._setupConnection(context);
97107
} else {
98108
this._setupConnection(context, identifyResolve, identifyReject);
99109
}
@@ -156,7 +166,10 @@ export default class NodeDataManager extends BaseDataManager {
156166
);
157167
break;
158168
default:
159-
break;
169+
this.logger.warn(
170+
`${logTag} _setupConnection called with unsupported connectionMode '${this.connectionMode}'.`,
171+
);
172+
return;
160173
}
161174
this.updateProcessor!.start();
162175
}

packages/sdk/node-client/src/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ export type {
2929

3030
export { basicLogger };
3131

32-
export const version = '0.0.1'; // x-release-please-version
33-
3432
/**
3533
* Creates a LaunchDarkly client. The client is not ready until {@link LDClient.start}
3634
* is called -- after which the first identify with `initialContext` runs and the returned

release-please-config.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,7 @@
148148
"packages/sdk/node-client": {
149149
"bump-minor-pre-major": true,
150150
"extra-files": [
151-
"src/platform/NodeInfo.ts",
152-
"src/index.ts"
151+
"src/platform/NodeInfo.ts"
153152
]
154153
},
155154
"packages/sdk/server-node": {

0 commit comments

Comments
 (0)