Skip to content
/ megamap Public

Extended JavaScript Map with caching, loading, expiry features... and more

Notifications You must be signed in to change notification settings

L422Y/megamap

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

MegaMap

A TypeScript-first map implementation that handles loading, caching, searching, filtering, and reactivity.

wakatime License: MIT Build Status

MegaMap is a TypeScript library designed for advanced key-value mapping. It extends the native JavaScript Record object and integrates additional features such as caching, loadability, and automatic expiry of entries. MegaMap is built upon two foundational classes, LoadableMap and CachedLoadableMap, each offering distinct capabilities. This library is ideal for sophisticated mapping solutions where enhanced control over data storage and retrieval is needed.

Ideally, this library is meant to be used in conjunction with a data store (e.g. a database, API, etc.), and/or state management solution, such as pinia.

Demo

I've started creating a demo on usage of the library, you can find the source at megamap-demo

There's a live demo at megamap-demo.pages.dev

Quick Example

import { MegaMap, ReactiveMegaMap } from 'megamap'

// Basic loading and caching
const posts = new MegaMap({
  loadOne: (id) => fetch(`/api/posts/${id}`).then(r => r.json()),
  loadAll: () => fetch('/api/posts').then(r => r.json()),
  expiryInterval: 5000, // Cache expires after 5 seconds
})

// Load and cache automatically
await posts.get('123')  // Fetches from API
await posts.get('123')  // Returns from cache
await posts.get('123')  // After 5s, fetches fresh data

// Enable search functionality
const searchablePosts = new MegaMap({
  ...postsConfig,
  searchableFields: ['title', 'content', 'tags']
})

// Fuzzy search across all loaded items
const results = searchablePosts.searchItems('typescript')

// Maintain filtered sublists automatically
const organizedPosts = new MegaMap({
  ...postsConfig,
  subListFilters: {
    published: post => post.status === 'published',
    draft: post => post.status === 'draft',
    recent: post => isWithinLast7Days(post.date)
  }
})

// Access filtered lists
console.log(organizedPosts.subLists.published)  // Array of published posts
console.log(organizedPosts.subLists.draft)     // Array of drafts
console.log(organizedPosts.subLists.recent)    // Recent posts

// Make it reactive (Vue.js)
const reactivePosts = new ReactiveMegaMap({
  loadOne: (id) => fetch(`/api/posts/${id}`).then(r => r.json()),
  loadAll: () => fetch('/api/posts').then(r => r.json()),
  subListFilters: {
    published: post => post.status === 'published',
    draft: post => post.status === 'draft'
  }
})

// Now it's Vue reactive - changes trigger component updates
reactivePosts.set('123', { title: 'Updated' })

Vue.js Example

<!-- PostManager.vue -->
<template>
  <div class="space-y-4">
    <!-- Search -->
    <input v-model="search" placeholder="Search posts..." />
    
    <!-- Posts Lists -->
    <div class="grid grid-cols-2 gap-4">
      <div>
        <h2>Published ({{ postsMap.subLists.published.length }})</h2>
        <div v-for="post in postsMap.subLists.published" :key="post.id">
          {{ post.title }}
        </div>
      </div>
      <div>
        <h2>Drafts ({{ postsMap.subLists.draft.length }})</h2>
        <div v-for="post in postsMap.subLists.draft" :key="post.id">
          {{ post.title }}
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ReactiveMegaMap } from 'megamap'

interface Post {
  id: string
  title: string
  status: 'draft' | 'published'
}

const search = ref('')
const postsMap = new ReactiveMegaMap<string, Post>({
  loadOne: (id) => fetch(`/api/posts/${id}`).then(r => r.json()),
  loadAll: () => fetch('/api/posts').then(r => r.json()),
  searchableFields: ['title'],
  subListFilters: {
    published: post => post.status === 'published',
    draft: post => post.status === 'draft'
  }
})

onMounted(() => postsMap.getAll())

// Update a post - UI automatically updates
const publishPost = (id: string) => {
  postsMap.set(id, { ...postsMap.value[id], status: 'published' })
}
</script>

This example shows:

  • πŸ”„ Automatic reactive sublists
  • πŸ” Built-in search
  • ⚑️ TypeScript support
  • πŸ“± Simple grid layout

Features

TL;DR:

  • πŸš€ Automatic loading & caching
  • πŸ” Built-in fuzzy search (via Fuse.js)
  • πŸ”„ Auto-maintained filtered lists
  • ⚑️ Vue.js reactivity (via ReactiveMegaMap)
  • πŸ“¦ TypeScript-first

MegaMap provides a suite of TypeScript classes for advanced key-value mapping, each offering unique capabilities:

  1. LoadableMap: This class extends basic map functionality with dynamic loading capabilities. Key features include:

    • Asynchronous data loading for individual keys (loadOne).
    • Optional bulk data loading (loadAll).
    • Custom key properties (keyProperty) for complex data types.
    • Callbacks for map updates (onUpdated).
  2. CachedLoadableMap: Building on LoadableMap, this class adds caching features with automatic expiry. It includes:

    • All LoadableMap features.
    • Configurable expiry intervals for cached data (expiryInterval).
  3. MegaMap: This is a comprehensive class combining loading, caching, and advanced manipulation. It includes:

    • All CachedLoadableMap features.
    • Support for secondary maps (secondaryMaps) for additional organizational capabilities.
    • Customizable item filters (filter).
    • Integration with Fuse.js for advanced search capabilities.
  4. ReactiveMegaMap: A specialized version of MegaMap designed for reactive frameworks like Vue.js. It offers:

    • All MegaMap features.
    • Reactive map properties, enabling seamless integration with reactive UI components.

Installation

npm install megamap

Usage

In the examples below we are implementing things outside of state management, and in a vue template directly. This is not recommended for production use, but is done here for the sake of simplicity. We are also using some fake data loader functions, check those out here.

Named Queries

We'll also set up some named queries, these are basically stored functions that can be called on the MegaMap instance, allowing you to easily retrieve data from the map without having to write the same code over and over again.

The format is meant to be clean and simple, so that you can easily read and understand what the query is doing.

 const namedQueries = {
    byAuthor: async (authorId) => {
        return fetch(`/api/posts/by-author/${authorId}`).then(res => res.json())
    },
    byTag: async (tag) => {
        return fetch(`/api/posts/by-tag/${tag}`).then(res => res.json())
    },
}

Sub-lists / Filters

...and some filters that will be used in the subListFilters option to populate the subLists property of our MegaMap:

const subListFilters = {
    active: (item: any) => item.status === "active",
    draft: (item: any) => item.status === "draft",
    inactive: (item: any) => item.status === "inactive",
}

We'll refer to these functions in the examples below.

MegaMap/ReactiveMegaMap Classes

Currently, these two classes are almost identical. The only difference is that ReactiveMegaMap uses Vue reactive properties, which enables reactivity for the sub-lists inside components/templates.

For MegaMap:

import { MegaMap } from "megamap"

// create a new MegaMap
const allPosts = new MegaMap({
    loadOne: fakeRecordLoad, // see above
    loadAll: fakeMultipleRecordLoad, // see above,
    subListFilters, // see above
    namedQueries, // see above
})

// another MegaMap that will be populated with user's "posts"
const myPosts = MegaMap({
    loadOne: fakeRecordLoad, // see above
    loadAll: fakeMultipleRecordLoad, // see above,
    subListFilters, // see above
    secondaryMaps: [allPosts] // see below
   
})

// simulate a record load every second
setInterval(async () => {
    await megaMap.get(`key${Math.floor(Math.random() * 1000) + 1}`)
}, 1000)

The MegaMap class has a secondaryMaps option that allows you to specify other MegaMaps that new items should be added to. This is useful for creating multiple views of the same data, for example, a "My Posts" view and an "All Posts"

This way, when a new item is loaded into myPosts, it will also be added to allPosts.

and for ReactiveMegaMap in Vue:

<template>
   <fieldset>
      <legend>ACTIVE</legend>
      <div>
         <div v-for="item in megaMap.subLists.active" :key="item._id">
            {{ item.data }}
         </div>
      </div>
   </fieldset>
</template>
<script setup>

   import {ReactiveMegaMap} from "megamap"

   const megaMap = new ReactiveMegaMap({
      loadOne: fakeRecordLoad, // see above
      loadAll: fakeMultipleRecordLoad, // see above,
      subListFilters, // see above
      namedQueries, // see above
   })

   // using the predefined named queries
   const authorPosts = ref(await megaMap.query.byAuthor("00001"))
   const taggedPosts = ref(await megaMap.query.byTag("fake"))

   // simulate a record load every second
   setInterval(async () => {
      await megaMap.get(`key${Math.floor(Math.random() * 1008) + 1}`)
   }, 1000)

</script>

Additional Classes

You can also utilize the following classes independently of MegaMap:

LoadableMap Class

This class extends the native JavaScript Record object with dynamic loading capabilities. It does not include any of the advanced features of CachedLoadableMap or MegaMap, but is useful for loading data into maps instead of handling this manually.

import { LoadableMap } from "megamap"

const loadableMap = new LoadableMap<string, TFakePost>({
    loadOne: fakeRecordLoad, // see above
    loadAll: fakeMultipleRecordLoad, // see above,
    keyProperty: "_id", // default is "_id"
    onUpdated: (updatedItem) => {
        console.log(`Updated item: ${updatedItem.id}`)
    }
})

CachedLoadableMap Class

Adds caching and expiry features to LoadableMap, will automatically expire cached items after the specified interval, so that they will be reloaded on the next access.

This class is useful for caching data that is accessed often, but not expected to change frequently, but may need to be refreshed.

import { CachedLoadableMap } from 'megamap';

const cachedMap = new CachedLoadableMap<string, DataType>({
    loadOne: fakeRecordLoad, // see above
    loadAll: fakeMultipleRecordLoad, // see above,
    keyProperty: "_id", // default is "_id"
    expiryInterval: 1000 * 60 * 60 * 24, // 24 hours
    onUpdated: (updatedItem) => {
        console.log(`Updated item: ${updatedItem.id}`)
    }
});

Contributing

Contributions to MegaMap are welcome. Please feel free to submit pull requests or suggest features.

License

This project is licensed under the MIT License.