Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using features property with consumer mode produces a type error #108

Open
dereksdev opened this issue Dec 14, 2023 · 3 comments
Open

Using features property with consumer mode produces a type error #108

dereksdev opened this issue Dec 14, 2023 · 3 comments

Comments

@dereksdev
Copy link

Using the features property in "consumer" or "consumer_partial" mode produces the following type error.

No overload matches this call.
  Overload 2 of 2, '(settings: IBrowserAsyncSettings): IAsyncSDK', gave the following error.
    Object literal may only specify known properties, and 'features' does not exist in type 'IBrowserAsyncSettings'.ts(2769)

Example:

import {
  LocalhostFromObject,
  PluggableStorage,
  SplitFactory,
} from "@splitsoftware/splitio-browserjs";
import { EdgeConfigWrapper } from "@splitsoftware/vercel-integration-utils";
import { createClient } from "@vercel/edge-config";

function factory(): SplitIO.IAsyncClient {
  return SplitFactory({
    core: {
      authorizationKey: "localhost",
      key: "exampleUserKey",
    },
    mode: "consumer",
    storage: PluggableStorage({
      wrapper: EdgeConfigWrapper({
        edgeConfigItemKey: "exampleItemKey",
        edgeConfig: createClient("exampleEdgeConfig"),
      }),
    }),
    features: {}, // this line produces the type error
    sync: {
      localhostMode: LocalhostFromObject(),
    },
  }).client();
}
@EmilianoSanchez
Copy link
Contributor

EmilianoSanchez commented Dec 19, 2023

Hi @dereksdev ,

The features property is used for testing purposes as described here, and is only available for the default mode (a.k.a, standalone or InMemory mode).

So you cannot use it with modes consumer or consumer_partial, and it is enforced via the TypeScript interface IBrowserAsyncSettings.

In summary:

  • When you set the mode to consumer or consumer_partial the config follows the IBrowserAsyncSettings interface. This interface doesn't allow the features property, and requires PluggableStorage as storage.
  • When you don't set the mode, or you set it to its default, which is standalone, the config follows the IBrowserSettings interface that support the features property, and doesn't require an storage (it keeps feature flag definitions in memory).

@dereksdev
Copy link
Author

Thank you @EmilianoSanchez. This helps me understand the intent better.

The features property does actually still function with the mode set to consumer or consumer_partial which led to my confusion.

So in order to test in consumer mode, do we need to create a mock PluggableStorage object? Any guidance here would be appreciated.

@EmilianoSanchez
Copy link
Contributor

EmilianoSanchez commented Dec 22, 2023

Hi @dereksdev ,

The features property does actually still function with the mode set to consumer or consumer_partial which led to my confusion.

If you are using just JavaScript, yes, you can pass the features property, and it will not complain, but it will be ignored in both consumer modes.

So in order to test in consumer mode, do we need to create a mock PluggableStorage object? Any guidance here would be appreciated.

We are going to take a look to it during January. At the moment there is not official support for localhost mode in consumer modes, but I can share with you a code snippet you can try on your side, based on the implementation of a storage wrapper in memory.

However, keep in mind that this might not be the final solution for sure.

// index.js
import { SplitFactory, PluggableStorage } from '@splitsoftware/splitio-browserjs';
import { inMemoryWrapperFactory } from './inMemoryWrapper.js';

const inMemoryWrapper = inMemoryWrapperFactory({
  // `features` format as explained here: https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK#localhost-mode
  features: {
    'feature_1': 'on',
  }
});

const factory = SplitFactory({
  core: {
    authorizationKey: 'anything-except-localhost',
    key: 'user_x'
  },
  mode: 'consumer',
  storage: PluggableStorage({
    wrapper: inMemoryWrapper
  })
});

const client = factory.client();

client.ready().then(async () => {
  console.log(await client.getTreatment('feature_1')); // on

  console.log(inMemoryWrapper._cache);
});
// inMemoryWrapper.js
/**
 * Creates a IPluggableStorageWrapper implementation that stores items in memory.
 * The `_cache` property is the object were items are stored.
 * Intended for testing purposes.
 */
export function inMemoryWrapperFactory({ features, prefix } = {}) {

  let _cache = {};

  const wrapper = {
    _cache,

    get(key) {
      return Promise.resolve(key in _cache ? _cache[key] : null);
    },
    set(key, value) {
      const result = key in _cache;
      _cache[key] = value;
      return Promise.resolve(result);
    },
    getAndSet(key, value) {
      const result = key in _cache ? _cache[key] : null;
      _cache[key] = value;
      return Promise.resolve(result);
    },
    del(key) {
      const result = key in _cache;
      delete _cache[key];
      return Promise.resolve(result);
    },
    getKeysByPrefix(prefix) {
      return Promise.resolve(Object.keys(_cache).filter(key => key.startsWith(prefix)));
    },
    incr(key, increment = 1) {
      if (key in _cache) {
        const count = parseInt(_cache[key]) + increment;
        if (isNaN(count)) return Promise.reject('Given key is not a number');
        _cache[key] = count + '';
        return Promise.resolve(count);
      } else {
        _cache[key] = '' + increment;
        return Promise.resolve(1);
      }
    },
    decr(key, decrement = 1) {
      if (key in _cache) {
        const count = parseInt(_cache[key]) - decrement;
        if (isNaN(count)) return Promise.reject('Given key is not a number');
        _cache[key] = count + '';
        return Promise.resolve(count);
      } else {
        _cache[key] = '-' + decrement;
        return Promise.resolve(-1);
      }
    },
    getMany(keys) {
      return Promise.resolve(keys.map(key => _cache[key] ? _cache[key] : null));
    },
    pushItems(key, items) {
      if (!(key in _cache)) _cache[key] = [];
      const list = _cache[key];
      if (Array.isArray(list)) {
        list.push(...items);
        return Promise.resolve();
      }
      return Promise.reject('key is not a list');
    },
    popItems(key, count) {
      const list = _cache[key];
      return Promise.resolve(Array.isArray(list) ? list.splice(0, count) : []);
    },
    getItemsCount(key) {
      const list = _cache[key];
      return Promise.resolve(Array.isArray(list) ? list.length : 0);
    },
    itemContains(key, item) {
      const set = _cache[key];
      if (!set) return Promise.resolve(false);
      if (set instanceof Set) return Promise.resolve(set.has(item));
      return Promise.reject('key is not a set');
    },
    addItems(key, items) {
      if (!(key in _cache)) _cache[key] = new Set();
      const set = _cache[key];
      if (set instanceof Set) {
        items.forEach(item => set.add(item));
        return Promise.resolve();
      }
      return Promise.reject('key is not a set');
    },
    removeItems(key, items) {
      if (!(key in _cache)) _cache[key] = new Set();
      const set = _cache[key];
      if (set instanceof Set) {
        items.forEach(item => set.delete(item));
        return Promise.resolve();
      }
      return Promise.reject('key is not a set');
    },
    getItems(key) {
      const set = _cache[key];
      if (!set) return Promise.resolve([]);
      if (set instanceof Set) return Promise.resolve(setToArray(set));
      return Promise.reject('key is not a set');
    },

    // always connects and disconnects
    connect() { return Promise.resolve(); },
    disconnect() { return Promise.resolve(); },
  };

  // Add features to wrapper
  if (features) {
    function parseCondition(data) {
      const treatment = data.treatment;

      if (data.keys) {
        return {
          conditionType: 'WHITELIST',
          matcherGroup: {
            combiner: 'AND',
            matchers: [
              {
                keySelector: null,
                matcherType: 'WHITELIST',
                negate: false,
                whitelistMatcherData: {
                  whitelist: typeof data.keys === 'string' ? [data.keys] : data.keys
                }
              }
            ]
          },
          partitions: [
            {
              treatment: treatment,
              size: 100
            }
          ],
          label: `whitelisted ${treatment}`
        };
      } else {
        return {
          conditionType: 'ROLLOUT',
          matcherGroup: {
            combiner: 'AND',
            matchers: [
              {
                keySelector: null,
                matcherType: 'ALL_KEYS',
                negate: false
              }
            ]
          },
          partitions: [
            {
              treatment: treatment,
              size: 100
            }
          ],
          label: 'default rule'
        };
      }
    }

    function splitsParserFromFeatures(features) {
      const splitObjects = {};

      Object.entries(features).forEach(([splitName, data]) => {
        let treatment = data;
        let config = null;

        if (data !== null && typeof data === 'object') {
          treatment = data.treatment;
          config = data.config || config;
        }
        const configurations = {};
        if (config !== null) configurations[treatment] = config;

        splitObjects[splitName] = {
          name: splitName,
          trafficTypeName: 'localhost',
          conditions: [parseCondition({ treatment: treatment })],
          configurations,
          status: 'ACTIVE',
          killed: false,
          trafficAllocation: 100,
          defaultTreatment: 'control',
        };
      });

      return splitObjects;
    };

    const featureFlags = splitsParserFromFeatures(features);
    prefix = prefix ? prefix + '.SPLITIO' : 'SPLITIO';

    Object.entries(featureFlags).forEach(([featureFlagName, featureFlag]) => {
      const featureFlagKey = `${prefix}.split.${featureFlagName}`;
      wrapper.set(featureFlagKey, JSON.stringify(featureFlag))
    });
  }

  return wrapper;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants