clientside trie

This commit is contained in:
Dylan Knutson
2023-04-04 16:21:40 -07:00
parent 230bd5757d
commit db4ea55b28
9 changed files with 708 additions and 64 deletions

View File

@@ -5,18 +5,10 @@ class Domain::Fa::ApiController < ApplicationController
def search_users
name = params[:name]
limit = (params[:limit] || 10).to_i.clamp(0, 15)
users = relevant = Domain::Fa::User.where([
"(name ilike :name) OR (url_name ilike :name)",
{ name: "#{ReduxApplicationRecord.sanitize_sql_like(name)}%" },
]).order(name: :asc).
limit(limit).
pluck(:id, :name, :url_name).
users = relevant = users_for_name(name).
map do |id, name, url_name|
{ id: id, name: name, url_name: url_name }
end
if params[:force_sleep]
sleep 2
end
if !Rails.env.production? && name == "error"
render status: 500, json: { error: "an error!" }
else
@@ -24,6 +16,25 @@ class Domain::Fa::ApiController < ApplicationController
end
end
def load_users
infile = Rails.root.join("app/javascript/server/buildUsersTrie.ts").to_s
outfile = infile[0..-(File.extname(infile).length + 1)] + ".js"
if !FileUtils.uptodate?(outfile, [infile])
logger.info("rebuild #{infile} -> #{outfile}...")
cmd = "yarn exec tsc -- --target es2015 --downlevelIteration --moduleResolution nodenext #{infile}"
out = `#{cmd}`
raise out unless $?.success?
logger.info("#{cmd}: #{out}")
end
digest = Digest::SHA256.hexdigest(File.read(outfile))
load_users_blob = Rails.cache.fetch("load_users_#{digest}", expires_in: 24.hours) do
script = ExecJS.compile(File.read(outfile))
all_users = Domain::Fa::User.all.pluck(:id, :name)
script.call("buildUsersTrie", all_users)
end
render plain: load_users_blob, content_type: "application/json"
end
def object_statuses
fa_ids = (params[:fa_ids] || []).map(&:to_i)
url_names = (params[:url_names] || [])
@@ -348,4 +359,168 @@ class Domain::Fa::ApiController < ApplicationController
"never"
end
end
def users_for_name(name)
return USERS if Rails.env.development?
Domain::Fa::User.where([
"(name ilike :name) OR (url_name ilike :name)",
{ name: "#{ReduxApplicationRecord.sanitize_sql_like(name)}%" },
]).order(name: :asc).
limit(limit).
pluck(:id, :name, :url_name)
end
USERS = [
[102661, "Twee", "twee"],
[102836, "Kobrin", "kobrin"],
[106292, "manadspaniel", "manadspaniel"],
[121171, "Sakura_the_wolf", "sakurathewolf"],
[123481, "Kai~skunk", "kai~skunk"],
[131219, "Luna.Moonflower", "luna.moonflower"],
[132018, "HeyImDaddy", "heyimdaddy"],
[132032, "frankenbunny", "frankenbunny"],
[154415, "Gh0stlyFurry", "gh0stlyfurry"],
[158837, "4r_da_Sqauw", "4rdasqauw"],
[173905, "ZombieUnicorn", "zombieunicorn"],
[181599, "space_fruit", "spacefruit"],
[182869, "Ragd360", "ragd360"],
[185649, "IBluefoxI", "ibluefoxi"],
[188437, "Yuricupcakes", "yuricupcakes"],
[190487, "lingaring", "lingaring"],
[197704, "Rayneier", "rayneier"],
[201974, "Clow", "clow"],
[208562, "rdraft", "rdraft"],
[215738, "Kaori_Keia", "kaorikeia"],
[22036, "tiddys", "tiddys"],
[223999, "SwampPossum", "swamppossum"],
[22841, "k-inukai", "k-inukai"],
[232171, "IPfix_da_Firefox", "ipfixdafirefox"],
[235933, "VisibleSuicide", "visiblesuicide"],
[258806, "Wizmas", "wizmas"],
[273285, "furrything", "furrything"],
[273634, "AngieTheOtter", "angietheotter"],
[282615, "catsrulekg", "catsrulekg"],
[285410, "Icyskills", "icyskills"],
[291374, "autumnfoxfur", "autumnfoxfur"],
[293860, "katamarulte", "katamarulte"],
[306985, "RadioactiveF0X", "radioactivef0x"],
[319368, "Soren123", "soren123"],
[349997, "kuroneko---sama", "kuroneko---sama"],
[350090, "SkylesFawkx", "skylesfawkx"],
[355982, "Shirilee", "shirilee"],
[358587, "Shosta", "shosta"],
[363811, "WhiteKIBA", "whitekiba"],
[374231, "starswillscream", "starswillscream"],
[379724, "dub-c", "dub-c"],
[386887, "Kirei-Uchiha", "kirei-uchiha"],
[407214, "MagicOFManga", "magicofmanga"],
[412404, "PoppyPath", "poppypath"],
[412923, "Sebastian_teh_Dober", "sebastiantehdober"],
[416072, "declinedkitty", "declinedkitty"],
[418580, "X-MXNE666", "x-mxne666"],
[418660, "Peelpeelpeel1", "peelpeelpeel1"],
[419237, "Fennixthepawlicker", "fennixthepawlicker"],
[431318, "GregorianBurns", "gregorianburns"],
[436280, "fionnaidh", "fionnaidh"],
[43684, "MLBat-Darkheart", "mlbat-darkheart"],
[437721, "White_Dragon_sisu", "whitedragonsisu"],
[441002, "someSkylah", "someskylah"],
[441268, "Eviscerator", "eviscerator"],
[442275, "HumanistFurs", "humanistfurs"],
[445549, "Ayrich", "ayrich"],
[448023, "NastyCalamari", "nastycalamari"],
[455220, "analfurs", "analfurs"],
[459851, "TotallySafe", "totallysafe"],
[464719, "Shadowfur13", "shadowfur13"],
[46619, "CandyShop", "candyshop"],
[466695, "ElFurroKawaii", "elfurrokawaii"],
[467565, "Brygaht", "brygaht"],
[472867, "Mitryll", "mitryll"],
[479071, "Shamanroach", "shamanroach"],
[497090, "!BlakeTheGamer", "blakethegamer"],
[507923, "Guro-Jans", "guro-jans"],
[520548, "Elseano", "elseano"],
[530399, "PUMPKINLIGHTTHEKITSUNE", "pumpkinlightthekitsune"],
[531212, "Ulti_199", "ulti199"],
[532461, "DozyDoe", "dozydoe"],
[537220, "fentible", "fentible"],
[539148, "StealthyIntheDark", "stealthyinthedark"],
[545990, "odinokiy", "odinokiy"],
[552205, "Namorria", "namorria"],
[557322, "Katana2", "katana2"],
[56360, "t0rrid", "t0rrid"],
[570571, "Zoralth8745", "zoralth8745"],
[570771, "Swagging", "swagging"],
[574502, "AMeanCow", "ameancow"],
[576501, "CaptianYeti", "captianyeti"],
[578205, "cyphurel", "cyphurel"],
[580442, "rraw", "rraw"],
[584446, "HelveluxDragon", "helveluxdragon"],
[592829, "clownshoes", "clownshoes"],
[594599, "AxisthewoIf", "axisthewoif"],
[597726, "GypsyCavarow", "gypsycavarow"],
[598375, "Cloud_Heart_Saber", "cloudheartsaber"],
[608812, "MidnightMapleMoon", "midnightmaplemoon"],
[608859, "pasteldad", "pasteldad"],
[609253, "mezzoforte2", "mezzoforte2"],
[616675, "Lil_Naz", "lilnaz"],
[617918, "tristian", "tristian"],
[629272, "esenya_valenok", "esenyavalenok"],
[637893, "Ink-EyesBae", "ink-eyesbae"],
[638713, "NavigatorTP", "navigatortp"],
[642236, "-EdenArcane-", "-edenarcane-"],
[660560, "Nevera66", "nevera66"],
[678313, "mog17", "mog17"],
[694556, "veroaid", "veroaid"],
[695130, "Furry_Writers", "furrywriters"],
[697418, "Lazywerewolf", "lazywerewolf"],
[711410, "Seas1de", "seas1de"],
[713084, "fuzzmuffin23", "fuzzmuffin23"],
[713759, "alp_Rik", "alprik"],
[716200, "RequiemDreamer", "requiemdreamer"],
[718333, "KhorneSoup", "khornesoup"],
[73267, "splendidguy44", "splendidguy44"],
[738005, "aurokhan", "aurokhan"],
[742214, "velin.art", "velin.art"],
[742375, "vampiretigress", "vampiretigress"],
[752692, "FlairMalin", "flairmalin"],
[756325, "Thorn_Mochag", "thornmochag"],
[75693, "numbsoul", "numbsoul"],
[758113, "leetf0x", "leetf0x"],
[758475, "Naspyta", "naspyta"],
[763834, "dankkushlove420", "dankkushlove420"],
[773150, "Fokatuh", "fokatuh"],
[777382, "Yorrick67", "yorrick67"],
[783391, "lovebigtits94", "lovebigtits94"],
[790219, "ValiantBadger", "valiantbadger"],
[793308, "promqueen192", "promqueen192"],
[794155, "Sola_the_Solgaleo", "solathesolgaleo"],
[802477, "kempo2k", "kempo2k"],
[804770, "vinv", "vinv"],
[808277, "AzartAlien", "azartalien"],
[819527, "curiosity6678", "curiosity6678"],
[820429, "GrayGrim", "graygrim"],
[822085, "rRICHr", "rrichr"],
[835929, "Kovuska", "kovuska"],
[836315, "knotthatbun", "knotthatbun"],
[836547, "1223223", "1223223"],
[841669, "DisheveledSloth", "disheveledsloth"],
[84257, "Naktian-Furs", "naktian-furs"],
[844032, "Fluffy_kozlik", "fluffykozlik"],
[854728, "pierre_dunn", "pierredunn"],
[856538, "onlyheretosponser", "onlyheretosponser"],
[861416, "tailsthefoxrules123", "tailsthefoxrules123"],
[862656, "EVXFY", "evxfy"],
[865110, "LowkeyArt", "lowkeyart"],
[873049, "Tedy", "tedy"],
[875191, "JamesTec666", "jamestec666"],
[875416, "Obscurum", "obscurum"],
[879928, "Solious6233", "solious6233"],
[882664, "princepixelbunny", "princepixelbunny"],
[887058, "Pyrote", "pyrote"],
[90246, "sonupi", "sonupi"],
[92111, "zevialexander", "zevialexander"],
[92831, "foxmacro", "foxmacro"],
]
end

View File

@@ -3,10 +3,11 @@ import * as React from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import Icon from "./Icon";
import ListItem from "./ListItem";
import Trie, { TrieNode } from "../lib/Trie";
// staging
// const HOST = "http://scraper.local:3001";
const HOST = "";
const HOST = "http://scraper.local:3001";
// const HOST = "";
const COMMON_LIST_ELEM_CLASSES = `
w-full p-2 pl-8
text-xl font-light
@@ -32,6 +33,9 @@ interface PropTypes {
interface ServerResponse {
users: { name: string }[];
}
type TrieValue = [number, string];
type TrieType = Trie<TrieValue>;
type TrieNodeType = TrieNode<TrieValue>;
export default function UserSearchBar({ isServerRendered }: PropTypes) {
isServerRendered = !!isServerRendered;
const [userName, setUserName] = useState("");
@@ -42,12 +46,36 @@ export default function UserSearchBar({ isServerRendered }: PropTypes) {
null
);
const [typingSettled, setTypingSettled] = useState(true);
const [trie, setTrie] = useState<TrieType | null>(null);
const inputRef = useRef(null);
const [isFocused, setIsFocused] = useState(
isServerRendered ? false : document.activeElement == inputRef.current
);
useEffect(() => {
const controller = new AbortController();
async function loadUserTrie() {
try {
let req = await fetch(`${HOST}/api/fa/load_users`, {
signal: controller.signal,
});
if (req.status != 200) {
setErrorMessage(`error loading users: ${await req.body}`);
} else {
let trieSerialized = await req.json();
const trie: TrieType = new Trie(trieSerialized);
setErrorMessage("loaded trie");
setTrie(trie);
}
} catch (err) {
console.log("error loading user trie: ", err);
}
}
loadUserTrie();
return () => controller.abort();
}, []);
const clearResults = useCallback(() => {
setUserList([]);
setErrorMessage(null);
@@ -71,68 +99,108 @@ export default function UserSearchBar({ isServerRendered }: PropTypes) {
);
const searchForUser = useCallback(
(userName) => {
setTypingSettled(false);
cancelPendingRequest();
if (isEmpty(userName)) {
(userName: string) => {
// setTypingSettled(false);
// cancelPendingRequest();
if (trie == null) {
return;
} else if (isEmpty(userName)) {
clearResults();
setUserName(userName);
setUserName("");
} else {
searchForUserDebounced(userName);
const { node } = trie.nodeForPrefix(userName);
if (node) {
const users = nodeToList(node, 5).map((node) => ({
name: `${node.value[1]} (${node.value[0]})`,
}));
console.log("Set users", users);
setUserList(users);
}
}
},
[cancelPendingRequest]
[cancelPendingRequest, trie]
);
useEffect(() => {
if (isEmpty(userName)) {
return;
function nodeToList(node: TrieNodeType, limit: number): TrieNodeType[] {
const list = [];
nodeToListImpl(list, node, limit);
return list;
}
function nodeToListImpl(
listOut: TrieNodeType[],
node: TrieNodeType,
limit: number
) {
if (listOut.length >= limit) return;
if (node.terminal) {
listOut.push(node);
if (listOut.length >= limit) return;
}
const controller = new AbortController();
setPendingRequest(controller);
async function doFetch() {
try {
let response = await fetch(
`${HOST}/api/fa/search_users?name=${encodeURIComponent(userName)}`,
{
signal: controller.signal,
}
);
await handleServerResponse(response);
} catch (error) {
if (error.name == "AbortError") {
return;
}
clearResults();
setErrorMessage(error.toString());
} finally {
setPendingRequest(null);
}
for (const [childKey, child] of node.children.entries()) {
nodeToListImpl(listOut, child, limit);
if (listOut.length >= limit) return;
}
doFetch();
return () => {
cancelPendingRequest();
};
}, [userName]);
}
const handleServerResponse = useCallback(async (response) => {
clearResults();
const contentType: string = response.headers.get("Content-Type");
if (response.status == 200) {
const { users }: ServerResponse = await response.json();
setUserList(users);
} else if (
!isEmpty(contentType) &&
contentType.startsWith("application/json")
) {
const { error } = await response.json();
setErrorMessage(`${response.status}: ${error}`);
} else {
const text = await response.text();
setErrorMessage(`${response.status}: ${text}`);
}
}, []);
// useEffect(() => {
// if (isEmpty(userName)) {
// return;
// }
// if (trie == null) {
// return;
// }
// const node = trie.nodeForPrefix(userName);
// setUserList([{ name: `node ${node}` }]);
// // const controller = new AbortController();
// // setPendingRequest(controller);
// // async function doFetch() {
// // try {
// // let response = await fetch(
// // `${HOST}/api/fa/search_users?name=${encodeURIComponent(userName)}`,
// // {
// // signal: controller.signal,
// // }
// // );
// // await handleServerResponse(response);
// // } catch (error) {
// // if (error.name == "AbortError") {
// // return;
// // }
// // clearResults();
// // setErrorMessage(error.toString());
// // } finally {
// // setPendingRequest(null);
// // }
// // }
// // doFetch();
// return () => {
// cancelPendingRequest();
// };
// }, [userName, trie]);
// const handleServerResponse = useCallback(async (response) => {
// clearResults();
// const contentType: string = response.headers.get("Content-Type");
// if (response.status == 200) {
// const { users }: ServerResponse = await response.json();
// setUserList(users);
// } else if (
// !isEmpty(contentType) &&
// contentType.startsWith("application/json")
// ) {
// const { error } = await response.json();
// setErrorMessage(`${response.status}: ${error}`);
// } else {
// const text = await response.text();
// setErrorMessage(`${response.status}: ${text}`);
// }
// }, []);
function onSearchInputKeyDown(event) {
const { code, shiftKey } = event;

View File

@@ -0,0 +1,93 @@
import { exact } from "prop-types";
interface SerializedTrie<T> {
// terminal node?
t: 1 | 0;
// value of the node
v: T;
// optional children
c?: { [s: string]: SerializedTrie<T> };
}
export class TrieNode<T> {
public terminal: boolean;
public value: T;
public children: Map<string, TrieNode<T>>;
public serialized: SerializedTrie<T>;
constructor(ser: SerializedTrie<T>) {
this.terminal = ser.t == 1;
this.value = ser.v;
this.children = new Map();
this.serialized = ser;
if (ser.c != null) {
for (const [key, value] of Object.entries(ser.c)) {
this.children.set(key, new TrieNode(value));
}
}
}
}
export default class Trie<T> {
public root: TrieNode<T>;
constructor(ser: SerializedTrie<T>) {
this.root = new TrieNode(ser);
}
public nodeForPrefix(key: string): {
chain: string[];
node: TrieNode<T> | null;
} {
let chain = [];
let node = this.root;
let remaining = key;
while (node && remaining.length > 0) {
let exactChild = null;
console.log("remaining: ", remaining);
for (const [childKey, child] of node.children.entries()) {
if (remaining.startsWith(childKey)) {
console.log("exact match for: ", childKey);
exactChild = child;
chain.push(childKey);
remaining = remaining.slice(childKey.length);
break;
}
}
// if an exact match was found, continue iterating
if (exactChild) {
node = exactChild;
continue;
}
console.log("looking for partial match for ", remaining);
for (const [childKey, child] of node.children.entries()) {
const startsWith = childKey.startsWith(remaining);
console.log(
"test ",
childKey,
" against ",
remaining,
": ",
startsWith,
" ",
child.serialized
);
if (startsWith) {
console.log("partial match for: ", remaining, ": ", child.serialized);
chain.push(childKey);
return { chain, node: child };
}
}
console.log("did not find partial, bailing!");
return { chain, node: null };
}
// // return remaining.length === 0 && node && node.terminal ? node : null;
console.log("returning child ", node, " for remaining ", remaining);
return { chain, node };
}
}

View File

@@ -0,0 +1,140 @@
function buildUsersTrie(users) {
const rootNode = new trie();
users.forEach(([id, name, urlName]) => {
rootNode.insert(name.toLowerCase(), [id, name, urlName]);
});
return JSON.stringify(rootNode.serialize());
}
class trie_node {
constructor() {
this.terminal = false;
this.children = new Map();
}
serialize() {
const { terminal, value, children } = this;
let mapped = {};
let numChildren = 0;
Object.keys(Object.fromEntries(children)).forEach((childKey) => {
numChildren += 1;
mapped[childKey] = children.get(childKey).serialize();
});
return {
t: this.terminal ? 1 : 0,
v: value,
c: numChildren > 0 ? mapped : undefined,
};
}
}
class trie {
constructor() {
this.root = new trie_node();
this.elements = 0;
}
serialize() {
return this.root.serialize();
}
get length() {
return this.elements;
}
get(key) {
const node = this.getNode(key);
if (node) {
return node.value;
}
return null;
}
contains(key) {
const node = this.getNode(key);
return !!node;
}
insert(key, value) {
let node = this.root;
let remaining = key;
while (remaining.length > 0) {
let child = null;
for (const childKey of node.children.keys()) {
const prefix = this.commonPrefix(remaining, childKey);
if (!prefix.length) {
continue;
}
if (prefix.length === childKey.length) {
// enter child node
child = node.children.get(childKey);
remaining = remaining.slice(childKey.length);
break;
}
else {
// split the child
child = new trie_node();
child.children.set(childKey.slice(prefix.length), node.children.get(childKey));
node.children.delete(childKey);
node.children.set(prefix, child);
remaining = remaining.slice(prefix.length);
break;
}
}
if (!child && remaining.length) {
child = new trie_node();
node.children.set(remaining, child);
remaining = "";
}
node = child;
}
if (!node.terminal) {
node.terminal = true;
this.elements += 1;
}
node.value = value;
}
remove(key) {
const node = this.getNode(key);
if (node) {
node.terminal = false;
this.elements -= 1;
}
}
map(prefix, func) {
const mapped = [];
const node = this.getNode(prefix);
const stack = [];
if (node) {
stack.push([prefix, node]);
}
while (stack.length) {
const [key, node] = stack.pop();
if (node.terminal) {
mapped.push(func(key, node.value));
}
for (const c of node.children.keys()) {
stack.push([key + c, node.children.get(c)]);
}
}
return mapped;
}
getNode(key) {
let node = this.root;
let remaining = key;
while (node && remaining.length > 0) {
let child = null;
for (let i = 1; i <= remaining.length; i += 1) {
child = node.children.get(remaining.slice(0, i));
if (child) {
remaining = remaining.slice(i);
break;
}
}
node = child;
}
return remaining.length === 0 && node && node.terminal ? node : null;
}
commonPrefix(a, b) {
const shortest = Math.min(a.length, b.length);
let i = 0;
for (; i < shortest; i += 1) {
if (a[i] !== b[i]) {
break;
}
}
return a.slice(0, i);
}
}

View File

@@ -0,0 +1,163 @@
type UserRow = [number, string];
function buildUsersTrie(users: UserRow[]): string {
const rootNode = new trie<[number, string]>();
users.forEach(([id, name]) => {
rootNode.insert(name.toLowerCase(), [id, name]);
});
return JSON.stringify(rootNode.serialize());
}
class trie_node<T> {
public terminal: boolean;
public value: T;
public children: Map<string, trie_node<T>>;
constructor() {
this.terminal = false;
this.children = new Map();
}
public serialize(): Object {
const { terminal, value, children } = this;
let mapped = {};
let numChildren = 0;
Object.keys(Object.fromEntries(children)).forEach((childKey) => {
numChildren += 1;
mapped[childKey] = children.get(childKey).serialize();
});
return {
t: this.terminal ? 1 : 0,
v: value,
c: numChildren > 0 ? mapped : undefined,
};
}
}
class trie<T> {
public root: trie_node<T>;
public elements: number;
constructor() {
this.root = new trie_node<T>();
this.elements = 0;
}
public serialize(): Object {
return this.root.serialize();
}
public get length(): number {
return this.elements;
}
public get(key: string): T | null {
const node = this.getNode(key);
if (node) {
return node.value;
}
return null;
}
public contains(key: string): boolean {
const node = this.getNode(key);
return !!node;
}
public insert(key: string, value: T): void {
let node = this.root;
let remaining = key;
while (remaining.length > 0) {
let child: trie_node<T> = null;
for (const childKey of node.children.keys()) {
const prefix = this.commonPrefix(remaining, childKey);
if (!prefix.length) {
continue;
}
if (prefix.length === childKey.length) {
// enter child node
child = node.children.get(childKey);
remaining = remaining.slice(childKey.length);
break;
} else {
// split the child
child = new trie_node<T>();
child.children.set(
childKey.slice(prefix.length),
node.children.get(childKey)
);
node.children.delete(childKey);
node.children.set(prefix, child);
remaining = remaining.slice(prefix.length);
break;
}
}
if (!child && remaining.length) {
child = new trie_node<T>();
node.children.set(remaining, child);
remaining = "";
}
node = child;
}
if (!node.terminal) {
node.terminal = true;
this.elements += 1;
}
node.value = value;
}
public remove(key: string): void {
const node = this.getNode(key);
if (node) {
node.terminal = false;
this.elements -= 1;
}
}
public map<U>(prefix: string, func: (key: string, value: T) => U): U[] {
const mapped = [];
const node = this.getNode(prefix);
const stack: [string, trie_node<T>][] = [];
if (node) {
stack.push([prefix, node]);
}
while (stack.length) {
const [key, node] = stack.pop();
if (node.terminal) {
mapped.push(func(key, node.value));
}
for (const c of node.children.keys()) {
stack.push([key + c, node.children.get(c)]);
}
}
return mapped;
}
private getNode(key: string): trie_node<T> | null {
let node = this.root;
let remaining = key;
while (node && remaining.length > 0) {
let child = null;
for (let i = 1; i <= remaining.length; i += 1) {
child = node.children.get(remaining.slice(0, i));
if (child) {
remaining = remaining.slice(i);
break;
}
}
node = child;
}
return remaining.length === 0 && node && node.terminal ? node : null;
}
private commonPrefix(a: string, b: string): string {
const shortest = Math.min(a.length, b.length);
let i = 0;
for (; i < shortest; i += 1) {
if (a[i] !== b[i]) {
break;
}
}
return a.slice(0, i);
}
}

View File

@@ -4,6 +4,8 @@ Rails.application.configure do
config.hosts << "localhost"
config.hosts << "scraper.local"
config.middleware.insert_after ActionDispatch::Static, Rack::Deflater
# Code is not reloaded between requests.
config.cache_classes = true

View File

@@ -3,6 +3,7 @@ Rails.application.config.middleware.insert_before 0, Rack::Cors, debug: true do
allow do
origins "localhost:3000"
resource "/api/fa/search_users", headers: :any, methods: [:get, :options]
resource "/api/fa/load_users", headers: :any, methods: [:get, :options]
end
end
end

View File

@@ -6,6 +6,7 @@ Rails.application.routes.draw do
namespace :fa do
get :similar_users, to: "/domain/fa/api#similar_users"
get :search_users, to: "/domain/fa/api#search_users"
get :load_users, to: "/domain/fa/api#load_users"
end
end

View File

@@ -9,7 +9,8 @@
"sourceMap": true,
"target": "es5",
"jsx": "react",
"noEmit": true
"noEmit": true,
"downlevelIteration": true
},
"include": ["app/javascript/**/*"],
"exclude": ["**/*.spec.ts", "node_modules", "vendor", "public"],