ASN.1 Packed Encoding Rules (PER) UNALIGNED variant

NOTE: This article is no longer maintained. Please visit ASN.1 PER UNALIGNED variant summary.

Introduction

This article briefly summarizes the ASN.1 Packed Encoding Rules (PER) UNALIGNED variant used in the 3GPP specifications with examples.

Disclaimer

This is a simplified explanation and not a complete specification. Missing examples used in the 3GPP specifications will be updated as soon as possible.

Encoding rules

Length determinant

If length determinant len is to be encoded as Constrained whole number with lower bound lb and upper bound ub less than 64K (65,536):

  • If range (ub - lb + 1) is equal to 1, no bits are used for length determinant.
  • Otherwise, minimum number of bits to represent ub - lb are used and len - lb is encoded like ‘unsigned integer’ in other languages.
    1
    2
    3
    4
    bit n        1
    +----------+
    | len - lb |
    +----------+

If length determinant len is to be encoded as Normally small length, a single bit indicating whether the value is less than or equal to 64 is inserted and the value is encoded in two ways:

  • If the value is less than or equal to 64, the bit is set to 0 and len - 1 is encoded like 6-bit ‘unsigned integer’ in other languages.
    1
    2
    3
    4
    bit   6       1
    +-+---------+
    |0| len - 1 |
    +-+---------+
  • Otherwise, the bit is set to 1 and the value is encoded as described below.
    1
    2
    3
    +-+------+
    |1| len* |
    +-+------+

If length determinant len is unconstrained or semi-constrained and less than or equal to 127, a single bit 0 is inserted and the value is encoded as 7-bit ‘unsigned integer’ in other languages.

1
2
3
4
bit   7   1
+-+-----+
|0| len |
+-+-----+

If length determinant len is unconstrained or semi-constrained and larger than or equal to 127 and less than 16K (16,384), two bits 10 are inserted and the value is encoded as 14-bit ‘unsigned integer’ in other languages.

1
2
3
4
bit    14  1
+--+-----+
|10| len |
+--+-----+

If length determinant len is unconstrained or semi-constrained and larger than 16K, len is divided into A 64K + B 16K + C. Two bits 11, 000100 and 64K items are inserted and they are repeated A times. If 16K or more items remain, two bits 11, 000001/000010/000011 and corresponding number of items are inserted. Finally, unconstrained or semi-constrained length determinant is inserted and remaining items are inserted.

1
2
3
4
5
6
7
8
9
+--+--------+-------+--+---------+-------+-+-----+----------------+
|11| 000100 | items |11| len (B) | items |0| len | items (<= 127) |
+--+--------+-------+--+---------+-------+-+-----+----------------+
^-A repetition----^ ^-0/1 repetition---^

+--+--------+-------+--+---------+-------+--+-----+-------------------------+
|11| 000100 | items |11| len (B) | items |10| len | items (> 127 and < 16K) |
+--+--------+-------+--+---------+-------+--+-----+-------------------------+
^-A repetition----^ ^-0/1 repetition---^

Boolean type

BOOLEAN
A single bit is set to 1 for TRUE and 0 for FALSE

1
2
3
4
bit 1
+-+
|b|
+-+

Integer type

INTEGER (lb..ub)
Minimum number of bits to represent ub - lb are used and value - lb is encoded like ‘unsigned integer’ in other languages.

1
2
3
4
bit n          1
+------------+
| value - lb |
+------------+

INTEGER (lb..ub, ...)
A single bit indicating whether the value is not within range from lb to ub is inserted at the beginning.

  • If the bit is set to 0, the value is encoded like INTEGER (lb..ub).

    1
    2
    3
    4
    bit   n          1
    +-+------------+
    |0| value - lb |
    +-+------------+
  • If the bit is set to 1, Length determinant (unconstrained) is inserted to represent the number of octets used and the value is encoded like ‘signed integer’ in other languages.

    1
    2
    3
    4
    bit         len*8 1
    +-+-----+-------+
    |1| len | value |
    +-+-----+-------+

Enumerated type

ENUMERATED {item1, item2}
The selected index is encoded like INTEGER (0..ub), where ub is equal to the number of enumeration items - 1.

1
2
3
4
bit n     1
+-------+
| index |
+-------+

ENUMERATED {item1, item2, ..., item3, item4}
A single bit indicating whether the selected enumeration index is not within the enumeration root.

  • If the bit is set to 0, the enumeration index is encoded like ENUMERATED {item1, item2}.
    1
    2
    3
    4
    bit   n     1
    +-+-------+
    |0| index |
    +-+-------+
  • If the bit is set to 1, a single bit indicating whether the selected index is larger than or equal to 64 is inserted and the value is encoded in two ways.
    • If the bit is set to 0, the value is encoded like 6-bit ‘unsigned integer’ in other languages.
      1
      2
      3
      4
      bit     6            1
      +-+-+--------------+
      |1|0| index (< 64) |
      +-+-+--------------+
    • If the bit is set to 1, Length determinant (semi-constrained with lower bound of 0) to represent the number of octets used is inserted and the value is encoded like ‘unsigned integer’ in other languages.
      1
      2
      3
      4
      bit           len*8         1
      +-+-+-----+---------------+
      |1|1| len | index (>= 64) |
      +-+-+-----+---------------+

Bit string type

BIT STRING
Length determinant (unconstrained) to represent the number of bits is inserted and the value is inserted.

1
2
3
4
bit       len   1
+-----+-------+
| len | value |
+-----+-------+

BIT STRING (SIZE(n))
The value is inserted without length determinant.

1
2
3
4
bit n     1
+-------+
| value |
+-------+

BIT STRING (SIZE(lb..ub))
Length determinant is inserted to represent the number of bits used and the value is inserted.

1
2
3
4
bit       n     1
+-----+-------+
| len | value |
+-----+-------+

Octet string type

OCTET STRING
Length determinant (unconstrained) is inserted to represent the number of octets used and the value is inserted.

1
2
3
4
bit       len*8 1
+-----+-------+
| len | value |
+-----+-------+

OCTET STRING (SIZE(n))
If n is less than 64K, the value is inserted without length determinant.

1
2
3
4
bit n*8   1
+-------+
| value |
+-------+

If n is larger than 64K, Length determinant (constrained) to represent the number of octets used is inserted and the value is inserted.

1
2
3
4
bit       len*8 1
+-----+-------+
| len | value |
+-----+-------+

OCTET STRING (SIZE(v1..v2))
Length determinant (constrained) to represent the number of octets used is inserted and the value is inserted.

1
2
3
4
bit       len*8 1
+-----+-------+
| len | value |
+-----+-------+

OCTET STRING (CONTAINING x)
Same as OCTET STRING.

Null type

It is just a placeholder. Not a single bit is used to encode NULL

Sequence type

1
2
3
4
5
SEQUENCE {
item1 Item1
item2 Item2 OPTIONAL,
item3 Item3 DEFAULT value
}

n bits are inserted where n is equal to the number of OPTIONAL and DEFAULT components. Each bit represents presence (1) and absence (0) of a component of a corresponding position. If n is larger than 64K, Length determinant (constrained) is inserted at the beginning.

1
2
3
4
5
6
7
8
9
bit n                1
+------------------+--------+-----+--------+
| bit mask (< 64K) | value1 | ... | valueN |
+------------------+--------+-----+--------+

bit n 1
+-----+-------------------+--------+-----+--------+
| len | bit mask (>= 64K) | value1 | ... | valueN |
+-----+-------------------+--------+-----+--------+
1
2
3
4
5
6
7
8
9
10
SEQUENCE {
item1 Item1,
item2 Item2 OPTIONAL,
...,
item3 Item4,
[[
item5 Item5,
item6 Item,
]]
}

A single bit indicating whether extension additions after the extension marker are present.

  • If the bit is set to 0, the rest of encoding is the same as Sequence type without extension marker and extension additions.
    1
    2
    3
    4
    bit   n        1
    +-+----------+--------+-----+--------+
    |0| bit mask | value1 | ... | valueN |
    +-+----------+--------+-----+--------+
  • If the bit is set to 1, Length determinant (normally small length) to represent the number of extension additions after the extension marker and the same number of bits are inserted. Each bit represents presence (1) and absence (0) of an extension addition of a corresponding position. Each extension addition is encoded as Open type field. An extension addition group enclosed with [[ and ]] is encoded as a Sequence type.
    1
    2
    3
    4
    bit   n        1                               len      1
    +-+----------+--------+-----+--------+-----+----------+--------+-----+--------+
    |0| bit mask | value1 | ... | valueN | len | bit mask | value1 | ... | valueN |
    +-+----------+--------+-----+--------+-----+----------+--------+-----+--------+

Sequence-of type

SEQUENCE (SIZE(v1..v2)) OF
Length determinant (constrained) to represent the number of sequence-of values included is inserted and the values are inserted.

1
2
3
+-----+--------+--------+-----+--------+
| len | value1 | value2 | ... | valueN |
+-----+--------+--------+-----+--------+

Choice type

1
2
3
4
CHOICE {
item1 Item1,
item2 Item2
}

The choice index is encoded like INTEGER (0..ub) where ub is the largest index and the actual value is inserted.

1
2
3
+-------+-------+
| index | value |
+-------+-------+
1
2
3
4
5
6
7
CHOICE {
item1 Item1,
item2 Item2,
...,
item3 Item3,
item4 Item4
}

A single bit indicating whether the selected item is within the the alternative root.

  • If the bit is set to 0, the choice index is encoded like INTEGER (0..ub) where ub is ther largest index and the actual value is inserted.
    1
    2
    3
    +-+-------+-------+
    |0| index | value |
    +-+-------+-------+
  • If the bit is set to 1, a single bit indicating whether the choice index is larger than or equal to 64 is inserted and the value is encoded in two ways.
    • If the bit is set to 0, the choice index is encoded like 6-bit ‘unsigned integer’ in other languages and the actual value is inserted.
      1
      2
      3
      4
      bit     6            1
      +-+-+---------------+-------+
      |1|0| index (< 64) | value |
      +-+-+---------------+-------+
    • If the bit is set to 1, Length determinant (semi-constrained with lower bound of 0) to represent the number of octets used is inserted and the choice index is encoded like ‘unsigned integer’ in other languages. And the actual value is inserted.
      1
      2
      3
      4
      bit           len           1
      +-+-+-----+---------------+-------+
      |1|1| len | index (>= 64) | value |
      +-+-+-----+---------------+-------+

References

  1. ITU-T X.691 ASN.1 encoding rules: Specification of Packed Encoding Rules (PER)

Using word-extractor on browser

Install word-extractor and blob-to-buffer:

1
npm install --save word-extractor blob-to-buffer

Install browserify dependendcies:

1
npm install --save assert buffer events path-browserify process stream-browserify util browserify-zlib

Add webpack fallback configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
// omitted
resolve: {
fallback: {
assert: require.resolve('assert'),
buffer: require.resolve('buffer'),
events: require.resolve('events'),
path: require.resolve('path-browserify'),
process: require.resolve('process/browser'),
stream: require.resolve('stream-browserify'),
util: require.resolve('util'),
zlib: require.resolve('browserify-zlib'),
},
},
};

Read a file with File API and extract from it

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const blobToBuffer = require('blob-to-buffer');

// acquire file (`File` object) somehow
blobToBuffer(file, (error, buffer) => {
if (error) {
// error handling
} else {
const wordExtractor = new WordExtractor();
wordExtractor.extract(buffer).then((doc) => {
// play with the document
}).catch(() => {
// error handling
});
}
});

Running arangod via PM2

ecosystem.config.js:

1
2
3
4
5
6
7
8
module.exports = {
apps : [{
name: 'db',
script: 'arangod',
interpreter: 'none',
cwd: '<ArangoDB installation path>',
args: '-c <arangod.conf path>',
}],

arangod.conf:

1
2
3
4
5
6
7
8
# Omitted unchanged part

[javascript]
# startup-directory = @ROOTDIR@/usr/share/arangodb3/js
# If @ROOTDIR@ does not work so that `startup-directory` is incorrect, use the following:
startup-directory = <ArangoDB installation path>/usr/share/arangodb3/js

# Omitted unchanged part

One thing is unresolved that pm2 stop or pm2 delete are not properly killing arangod. I will update later if there is a way…

Tower of Hanoi with recursion

Problem statement

Assume there are 3 towers and \(D\) disks. And all disks are place at the first tower in a descending order in terms of disk number. It can be illustrated as follows:

1
2
3
4
5
6
7
     [1]             |              |
[ 2 ] | |
: | |
[ D-1 ] | |
[ D ] | |
-----------------------------------------
Tower 1 Tower 2 Tower 3

The objective is to find out the order of movement to move the entire disks to another tower with constrains:

  • One only disk can be moved at a time
  • Disk \(X\) cannot be placed under disk \(Y\), where \(X\) is greater than \(Y\)

Approach

To move disk \(X\) from tower \(A\) to \(C\), three steps as follows:

  1. Move disks 1 to \(X-1\) from tower \(A\) to \(B\)
  2. Move disk \(X\) from tower \(A\) to \(C\)
  3. Move disks 1 to \(X-1\) from tower \(B\) to \(C\)

Steps (1) and (3) are not a single atomic operation and they can be performed recursively:

  1. Move disks 1 to \(X-2\) from tower \(A\) to \(C\)
  2. Move disk \(X-1\) from tower \(A\) to \(B\)
  3. Move disks 1 to \(X-2\) from tower \(C\) to \(B\)

If there is only one disk to move, step (1) and step (3) can be skipped.

Implementation

We can define a function moveDisks() whose inputs are the number of disks to be moved, tower number that the disks are currently located and tower number that the disks to be moved to:

1
void moveDisks(int nDisks, int from, int to);

It can recursively call the function itself:

1
2
3
4
5
6
7
8
9
10
void moveDisks(int nDisks, int from, int to) {
int intermediate = 6 - from - to;
if (nDisks > 1) {
moveDisks(nDisks - 1, from, intermediate);
}
cout << "Move disk " << nDisks << " from tower " << from << " to tower " << to << endl;
if (nDisks > 1) {
moveDisks(nDisks - 1, intermediate, to);
}
}

Here int intermediate = 6 - from - to; is a tricky part. This variable represents the number of intermediate tower to move nDisks - 1 disks. If each tower is numbered with 1, 2, and 3 then relationships between from, to and intermediate can be determined as follows and the forementioned equation reflects these relationships:

From To Intermediate
1 2 3
1 3 2
2 1 3
2 3 2
3 1 2
3 2 3

Test result

1
2
3
4
5
6
7
8
3
Move disk 1 from tower 1 to tower 3
Move disk 2 from tower 1 to tower 2
Move disk 1 from tower 3 to tower 2
Move disk 3 from tower 1 to tower 3
Move disk 1 from tower 2 to tower 1
Move disk 2 from tower 2 to tower 3
Move disk 1 from tower 1 to tower 3

webpack raw-loader with TypeScript

Another struggle of a day. I had difficulties on importing a GraphQL schema definition in TypeScript project with webpack raw-loader.

Declare in webpack.config.js to use raw-loader to import a GraphQL schema definition:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
module: {
rules: [
{
test: /\.graphql$/i, // Extension which you want to load via raw-loader
use: 'raw-loader',
},
// omitted
],
},
resolve: {
extensions: ['.js', '.ts'], // Add extension if you want to raw-load without typing extension
},
// omitted
};

Declare in src/raw-loader.d.ts that an imported GraphQL schema definition is a type of string:

1
2
3
4
declare module "*.graphql" {
const content: string;
export default content;
};

Declare in tsconfig.json so that tsc does not try to compile *.graphql files:

1
2
3
{
"exclude": ["*.graphql"]
}

axios with proxy and custom SSL certificate

This article summarizes what made me struggle days.

Fix https-proxy-agent to support CA

It was originally proposed here by maslakov.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { HttpsProxyAgent, HttpsProxyAgentOptions } from 'https-proxy-agent';
import { ClientRequest, RequestOptions } from 'agent-base';
import { Socket } from 'net';

export class PatchedHttpsProxyAgent extends HttpsProxyAgent {
private ca: any;

constructor(opts: HttpsProxyAgentOptions) {
super(opts);
this.ca = opts.ca;
}

async callback(req: ClientRequest, opts: RequestOptions): Promise<Socket> {
return super.callback(req, Object.assign(opts, { ca: this.ca }));
}
}

Working prototype

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const axios = require('axios');
const HttpsProxyAgent = require('https-proxy-agent');
const ca = require('ssl-root-cas/latest').create();

ca.addFile(certificateFilePath);

const httpsAgent = new HttpsProxyAgent({
protocol: 'http',
host: 'proxy_host',
port: 8080,
ca,
});

axios.defaults.httpsAgent = httpsAgent;

References

  1. PatchedHttpsProxyAgent

NR RRC changes on June 2021

Some interesting changes

R2-2106555 NR-DC Cell Group capability filtering

This CR R2-2106555 introduces cell grouping in UE capability enquiry for NR-DC. In Release 15, NR-DC is limited to FR1 MCG and FR2 SCG. But from Releae 16, this limitation is removed. It introduces ambiguity that which MCG and SCG configuration UE supports. To remove the ambiguity, the network can enquiry UE with CellGrouping:

1
2
3
4
5
6
7
8
9
10
11
12
UE-CapabilityRequestFilterCommon ::=            SEQUENCE {
-- Omitted
[[
requestedCellGrouping-r16 SEQUENCE (SIZE (1..maxCellGroupings-r16)) OF CellGrouping-r16 OPTIONAL -- Cond NRDC
]]
}

CellGrouping-r16 :: SEQUENCE {
mcg-r16 SEQUENCE (SIZE (1..maxBands)) OF FreqBandIndicatorNR,
scg-r16 SEQUENCE (SIZE (1..maxBands)) OF FreqBandIndicatorNR,
mode-r16 ENUMERATED {sync, async}
}

For example, the network can enquiry UE with the following cell grouping filter:

1
2
3
{ mcg: [1, 2, 3], scg: [256, 257], mode:  sync },
{ mcg: [1, 2, 3], scg: [256, 257], mode: async },
{ mcg: [1, 2, 3], scg: [ 66, 77], mode: sync },

UE receiving the filter, it reports supported band combinations with supportedCellGrouping:

1
2
3
CA-ParametersNRDC-v16xy ::=                     SEQUENCE {
supportedCellGrouping-r16 BIT STRING (SIZE (1..maxCellGroupings-r16)) OPTIONAL
}

If the first bit of a bit string is set to 1, a given band combination supports NR-DC with the first cell grouping configuration.

R2-2106712 Correction to ca-ParametersNR-ForDC

I believe the main point of this CR R2-2106712 is to clarify that ca-ParametersNR[-ForDC[-vABCD]] applies separately. I doubt that it is less likely to be misunderstood, but at the same time, I believe it is good to refine the statement.

ca-ParametersNR-forDC (with and without suffix)
If this field is present for a band combination, it reports the UE capabilities when NR-DC is configured with the band combination. If no a version of this field (i.e., with and or without suffix) is present absent for a band combination, the corresponding ca-ParametersNR field versions (with and without suffix) in BandCombination are is applicable to the UE configured with NR-DC for the band combination. If a version of this field (i.e., with or without suffix) is present for a band combination but does not contain any parameters, the UE does not support the corresponding field version when configured with NR-DC for the band combination.

R2-2106754 Clarification on the Timing Reference of PSCell SMTC Configuration

targetCellSMTC-SCG was originally introduced as name smtc in last September, but renamed for more clarity in the next RAN Plenary. It is useful when MCG and SCG have different timing reference. This CR R2-2106754 clarifies how UE applies SMTC in a MCG mobility case.

R2-2105462 Clarification on SCellFrequencies

It was a bug in the specification that ASN.1 structure and description of its interpretation. According to the below structure scellFrequencies can be absent:

1
2
3
4
5
CG-Config-v1590-IEs ::= SEQUENCE {
scellFrequenciesSN-NR SEQUENCE (SIZE (1.. maxNrofServingCells-1)) OF ARFCN-ValueNR OPTIONAL,
scellFrequenciesSN-EUTRA SEQUENCE (SIZE (1.. maxNrofServingCells-1)) OF ARFCN-ValueEUTRA OPTIONAL,
nonCriticalExtension CG-Config-v1610-IEs OPTIONAL
}

However, the below statement specifies that the receiving node shall maintain the previous configuration:

As an exception to this general rule, the absence of the below listed fields means that a receiving node maintains the values informed via the previous message.
(Omitted)

  • scellFrequenciesSN-EUTRA;
  • scellFrequenciesSN-NR.

Comparing the structure and the statement, an obvious inconsistency exists that a sending node may omit the IE if no SCell exists. So this CR R2-2105462 removes two IEs from the statement so that the receiving node shall overwrite the previous configuration with the new one.

Building GitHub Action to publish Hexo post from GitHub Issue

I wanted to write and publish a Hexo post without tedious commands such as git clone, git pull, hexo new, hexo generate, git commit and git push. I saw the feasibility to build an automated publishing system using GitHub Issue and GitHub Actions for this purpose.

Here’s workflow what I thought:

  • A user makes an issue, a draft of a post on GitHub Issue
  • GitHub Workflow is triggered against the issue
  • The workflow converts the issue to a Hexo post

GitHub Issue

I thought that GitHub Issue is a perfect system to draft a post. It has a title, a content and a date/time of update and those information can be a title, a content and a date/time of a post. One thing missing is tags of a post. To achieve this, I got an idea to use labels of an issue as tags of a post.

GitHub Action

I did a research whether there exists GitHub Actions converting an issue to a Hexo post, but I found nothing. So I decided to build a GitHub Action for it.

First of all, it takes two parameters, issue_url and token and extracts endpoint from issue_url:

1
2
3
4
5
6
import * as core from '@actions/core';

const issueUrl = core.getInput('issue_url');
const index = issueUrl.indexOf('/repos');
const endpoint = issueUrl.substring(index);
const token = core.getInput('token');

And then, it initializes Hexo:

1
2
3
4
5
6
7
8
import Hexo from 'hexo';

const hexo = new Hexo(process.cwd(), {});
hexo.init().then(() => {
// Described below
}).catch((reason) => {
core.setFailed(reason);
});

After Hexo is initialized, it retrieves information of an issue:

1
2
3
4
5
6
7
8
9
10
11
12
import { Octokit } from 'octokit';

// After resolving `hexo.init()`
const gh = new Octokit({ auth: token });

console.log(`Converting issue ${endpoint} to Hexo post...`);
gh.request(`GET ${endpoint}`).then((response) => {
const { title, updated_at: date, labels, milestone, body: content } = response.data;
// Described below
}).catch((reason) => {
core.setFailed(reason);
});

If a draft is set to be published, it derives tags and creates a Hexo post:

1
2
3
4
5
6
7
8
9
10
11
12
13
const MILESTONE_PUBLISH = 'publish';

if (milestone.title !== MILESTONE_PUBLISH) {
console.log(`Issue does not have milestone ${MILESTONE_PUBLISH}`);
} else {
const tags = labels.map((label: any) => label.name);
hexo.post.create({
title,
date,
tags,
content,
} as any);
}

At this point, a markdown file is generated and further actions to be prepared to complete publishing a post. The complete implementation of this action can be found here.

Workflow

Now a complete workflow needs to be setup. The workflow below converts an issue to a post and pushes it to the repository:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
name: Issue to Hexo
on:
issues:
# Sufficient to trigger this workflow when an issue is milestoned
types: [ milestoned ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
persist-credentials: 'false'
- uses: internalstability/[email protected]
with:
issue_url: ${{ github.event.issue.url }}
# Personal access token used to get information of Issue
token: ${{ secrets.token }}
# At this point, a markdown file is generated and untracked
# Take further action, e.g. generate (`hexo generate`), commit and push
- name: Commit post
run: |
git add .
git config user.name "issue-to-hexo bot"
git config.user.email "<>"
git commit -m "Add a post"
- name: Push
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.token }}

I have another workflow which generates (hexo generate) a blog from the pushed code. I made two separate workflows because I both use GitHub Issue to draft a post and manually write and push a markdown draft on my PC. Here’s the workflow:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
name: Publish
on:
push:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
persist-credentials: 'false'
submodules: 'true'
- uses: actions/setup-node@v2
with:
node-version: '14'
# This submodule is a directory that contains generated Hexo blog
- name: Checkout submodule
run: |
cd public
git checkout master
- name: Build
run: |
npm install
npm run build
- name: Commit submodule
run: |
cd public
git add .
git config user.name "internalstability/hexo-src bot"
git config user.email "<>"
git commit -m "Publish"
- name: Push submodule
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.token }}
directory: 'public'
repository: 'internalstability/internalstability.github.io'
# These actions commits and pushes the submodule to the main module
- name: Commit main module
run: |
git add .
git config user.name "internalstability/hexo-src bot"
git config user.email "<>"
git commit -m "Publish"
- name: Push main module
uses: ad-m/github-push-action@master

Drag sorting Ant Design Table with dnd kit

Introduction

I was trying to implement Drag sorting with handler for Ant Design Table. I found that react-sortable-hoc is not going to be enhancement further and the author encourages to use dnd kit. This article summarizes what I did to implement it with dnd kit.

Prepare dummy data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function App() {
const columns = [
{
key: "dragHandle", dataIndex: "dragHandle", title: "Drag",
width: 30,
render: () => <MenuOutlined />,
},
{
key: "key", dataIndex: "key", title: "Key",
},
];

const dataSourceRaw = new Array(5).fill({}).map((item, index) => ({
// This will be transformed into `data-row-key` of props.
// Shall be truthy to be draggable. I don't know why.
// To this end, index of number type is converted into string.
key: index.toString(),
}));
const [dataSource, setDataSource] = useState(dataSourceRaw);

return (
<Table
columns={columns}
dataSource={dataSource}
/>
);

The important thing here is that the key used to identify an item must be truthy value. I struggled with a situation that the first item is not draggable. After some minutes of debugging, I found that if the key is falsy value, it is not draggable. But I couldn’t find the root cause for this.

Define a state variable for drag overlay

1
2
// ID to render overlay.
const [activeId, setActiveId] = useState(null);

activeId will be used for determining whether to render drag overlay or not.

Convert Table into sortable preset of dnd kit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);

return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<Table
columns={columns}
dataSource={dataSource}
components={{
body: {
wrapper: DraggableWrapper,
row: DraggableRow,
},
}}
/>
{/* Render overlay component. */}
<DragOverlay>{activeId ? activeId : null}</DragOverlay>
</DndContext>
);

According to sortable single container, enclose Table in DndContext, wrap body (tbody) with SortableContext and implement useSortable in row (tr). In this example, DraggableWrapper implements SortableContext and DraggableRow implements tr with useSortrable.

DragOverlay is rendered only when activeId has a valid ID value.

DraggableWrapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function DraggableWrapper(props: any) {
const { children, ...restProps } = props;
/**
* 'children[1]` is `dataSource`
* Check if `children[1]` is an array
* because antd gives 'No Data' element when `dataSource` is an empty array
*/
return (
<SortableContext
items={children[1] instanceof Array ? children[1].map((child: any) => child.key) : []}
strategy={verticalListSortingStrategy}
{...restProps}
>
<tbody {...restProps}>
{
// This invokes `Table.components.body.row` for each element of `children`.
children
}
</tbody>
</SortableContext>
);
}

DraggableWrapper implements SortableContext. items shall be a list of keys to identify items, not items themselves. Inside tbody, children is a list of rows and each item will invoke Table.components.body.row, which is DraggableRow.

DraggableRow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function DraggableRow(props: any) {
const { attributes, listeners, setNodeRef } = useSortable({
id: props["data-row-key"],
});
const { children, ...restProps } = props;
/**
* 'children[1]` is a row of `dataSource`
* Check if `children[1]` is an array
* because antd gives 'No Data' element when `dataSource` is an empty array
*/
return (
<tr
ref={setNodeRef}
{...attributes}
{...restProps}
>
{
children instanceof Array ? (
children.map((child: any) => {
const { children, key, ...restProps } = child;
return key === "dragHandle" ? (
<td {...listeners} {...restProps}>
{child}
</td>
) : (
<td {...restProps}>{child}</td>
);
})
) : (
children
)
}
</tr>
);
}

DraggableRow implements tr with useSortable, where id must be the same with a key of each item. We want to make a row draggable, so assign setNodRef to tr. And we want to enable dragging only when a user grabs a drag handle, so assign listeners to td containing a drag handle.

handleDragStart

1
2
3
4
function handleDragStart(event: any) {
const { active } = event;
setActiveId(active.id);
}

handleDragEnd

1
2
3
4
5
6
7
8
9
10
11
12
13
function handleDragEnd(event: any) {
const { active, over } = event;
if (active.id !== over.id) {
setDataSource((items) => {
// In this example, find an item, where `item.key` === `useSortable.id`.
const oldIndex = items.findIndex((item) => item.key === active.id);
const newIndex = items.findIndex((item) => item.key === over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
// Stop overlay.
setActiveId(null);
}

handleDragEnd performs swapping two items. Here, to find indexes of two items in data source, we need to compare key of an item and id of useSortable.

You can find demo here.

Finding the number of edges of a document in ArangoDB

Query

1
2
3
FOR d, e IN ANY|INBOUND|OUTBOUND documentId edgeCollectionName
COLLECT AGGREGATE numEdges = LENGTH(1)
RETURN numEdges

Application: Finding a document with no edge

1
2
3
4
5
6
7
8
FOR doc IN collectionName
let numEdges = (
FOR d, e IN ANY|INBOUND|OUTBOUND doc._id edgeCollectionName
COLLECT AGGREGATE numEdges = LENGTH(1)
RETURN numEdges
)[0]
FILTER numEdges == 0
RETURN doc